Merge pull request #10825 from edx/kill-psycho
Remove psychometrics app
This commit is contained in:
@@ -1125,9 +1125,6 @@ class CapaMixin(CapaFields):
|
||||
self.attempts,
|
||||
)
|
||||
|
||||
if hasattr(self.runtime, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.runtime.psychometrics_handler(self.get_state_for_lcp())
|
||||
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
|
||||
@@ -1375,10 +1372,6 @@ class CapaMixin(CapaFields):
|
||||
event_info['attempts'] = self.attempts
|
||||
self.track_function_unmask('problem_rescore', event_info)
|
||||
|
||||
# psychometrics should be called on rescoring requests in the same way as check-problem
|
||||
if hasattr(self.runtime, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.runtime.psychometrics_handler(self.get_state_for_lcp())
|
||||
|
||||
return {'success': success}
|
||||
|
||||
def save_problem(self, data):
|
||||
|
||||
@@ -2528,20 +2528,6 @@ CREATE TABLE `programs_programsapiconfig` (
|
||||
CONSTRAINT `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `psychometrics_psychometricdata`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `psychometrics_psychometricdata` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`done` tinyint(1) NOT NULL,
|
||||
`attempts` int(11) NOT NULL,
|
||||
`checktimes` longtext,
|
||||
`studentmodule_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `studentmodule_id` (`studentmodule_id`),
|
||||
CONSTRAINT `D758b867e6fa9161734bd9cb58b9a485` FOREIGN KEY (`studentmodule_id`) REFERENCES `courseware_studentmodule` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `self_paced_selfpacedconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
|
||||
@@ -56,7 +56,6 @@ from openedx.core.lib.xblock_utils import (
|
||||
wrap_xblock,
|
||||
request_token as xblock_request_token,
|
||||
)
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from student.models import anonymous_id_for_user, user_by_anonymous_id
|
||||
from student.roles import CourseBetaTesterRole
|
||||
from xblock.core import XBlock
|
||||
@@ -760,11 +759,6 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
|
||||
position = None
|
||||
|
||||
system.set('position', position)
|
||||
if settings.FEATURES.get('ENABLE_PSYCHOMETRICS') and user.is_authenticated():
|
||||
system.set(
|
||||
'psychometrics_handler', # set callback for updating PsychometricsData
|
||||
make_psychometrics_data_update_handler(course_id, user, descriptor.location)
|
||||
)
|
||||
|
||||
system.set(u'user_is_staff', user_is_staff)
|
||||
system.set(u'user_is_admin', bool(has_access(user, u'staff', 'global')))
|
||||
|
||||
@@ -1767,17 +1767,6 @@ class TestRebindModule(TestSubmittingProblems):
|
||||
self.assertEqual(module.scope_ids.user_id, user2.id)
|
||||
self.assertEqual(module.descriptor.scope_ids.user_id, user2.id)
|
||||
|
||||
@patch('courseware.module_render.make_psychometrics_data_update_handler')
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PSYCHOMETRICS': True})
|
||||
def test_psychometrics_anonymous(self, psycho_handler):
|
||||
"""
|
||||
Make sure that noauth modules with anonymous users don't have
|
||||
the psychometrics callback bound.
|
||||
"""
|
||||
module = self.get_module_for_user(self.anon_user)
|
||||
module.system.rebind_noauth_module_to_user(module, self.anon_user)
|
||||
self.assertFalse(psycho_handler.called)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
|
||||
@@ -48,7 +48,6 @@ from instructor_task.api import (
|
||||
from instructor_task.views import get_task_completion_info
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
from class_dashboard import dashboard_data
|
||||
from psychometrics import psychoanalyze
|
||||
from student.models import (
|
||||
CourseEnrollment,
|
||||
CourseEnrollmentAllowed,
|
||||
@@ -97,7 +96,7 @@ def instructor_dashboard(request, course_id):
|
||||
plots = []
|
||||
datatable = {}
|
||||
|
||||
# the instructor dashboard page is modal: grades, psychometrics, admin
|
||||
# the instructor dashboard page is modal: grades, admin
|
||||
# keep that state in request.session (defaults to grades mode)
|
||||
idash_mode = request.POST.get('idash_mode', '')
|
||||
idash_mode_key = u'idash_mode:{0}'.format(course_id)
|
||||
@@ -319,18 +318,6 @@ def instructor_dashboard(request, course_id):
|
||||
ret = _do_enroll_students(course, course_key, students, secure=secure, overload=overload)
|
||||
datatable = ret['datatable']
|
||||
|
||||
#----------------------------------------
|
||||
# psychometrics
|
||||
|
||||
elif action == 'Generate Histogram and IRT Plot':
|
||||
problem = request.POST['Problem']
|
||||
nmsg, plots = psychoanalyze.generate_plots_for_problem(problem)
|
||||
msg += nmsg
|
||||
track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard")
|
||||
|
||||
if idash_mode == 'Psychometrics':
|
||||
problems = psychoanalyze.problems_with_psychometric_data(course_key)
|
||||
|
||||
#----------------------------------------
|
||||
# analytics
|
||||
def get_analytics_result(analytics_name):
|
||||
@@ -435,8 +422,6 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
'show_email_tab': show_email_tab, # email
|
||||
|
||||
'problems': problems, # psychometrics
|
||||
'plots': plots, # psychometrics
|
||||
'course_errors': modulestore().get_course_errors(course.id),
|
||||
'instructor_tasks': instructor_tasks,
|
||||
'offline_grade_log': offline_grades_available(course_key),
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
'''
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from psychometrics.models import PsychometricData
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(PsychometricData)
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# generate pyschometrics data from tracking logs and student module data
|
||||
|
||||
import json
|
||||
|
||||
from courseware.models import StudentModule
|
||||
from track.models import TrackingLog
|
||||
from psychometrics.models import PsychometricData
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
#db = "ocwtutor" # for debugging
|
||||
#db = "default"
|
||||
|
||||
db = getattr(settings, 'DATABASE_FOR_PSYCHOMETRICS', 'default')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "initialize PsychometricData tables from StudentModule instances (and tracking data, if in SQL)."
|
||||
help += "Note this is done for all courses for which StudentModule instances exist."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# delete all pmd
|
||||
|
||||
#PsychometricData.objects.all().delete()
|
||||
#PsychometricData.objects.using(db).all().delete()
|
||||
|
||||
smset = StudentModule.objects.using(db).exclude(max_grade=None)
|
||||
|
||||
for sm in smset:
|
||||
usage_key = sm.module_state_key
|
||||
if not usage_key.block_type == "problem":
|
||||
continue
|
||||
try:
|
||||
state = json.loads(sm.state)
|
||||
done = state['done']
|
||||
except:
|
||||
print "Oops, failed to eval state for %s (state=%s)" % (sm, sm.state)
|
||||
continue
|
||||
|
||||
if done: # only keep if problem completed
|
||||
try:
|
||||
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
|
||||
except PsychometricData.DoesNotExist:
|
||||
pmd = PsychometricData(studentmodule=sm)
|
||||
|
||||
pmd.done = done
|
||||
pmd.attempts = state['attempts']
|
||||
|
||||
# get attempt times from tracking log
|
||||
uname = sm.student.username
|
||||
tset = TrackingLog.objects.using(db).filter(username=uname, event_type__contains='problem_check')
|
||||
tset = tset.filter(event_source='server')
|
||||
tset = tset.filter(event__contains="'%s'" % usage_key)
|
||||
checktimes = [x.dtcreated for x in tset]
|
||||
pmd.checktimes = checktimes
|
||||
if not len(checktimes) == pmd.attempts:
|
||||
print "Oops, mismatch in number of attempts and check times for %s" % pmd
|
||||
|
||||
#print pmd
|
||||
pmd.save(using=db)
|
||||
|
||||
print "%d PMD entries" % PsychometricData.objects.using(db).all().count()
|
||||
@@ -1,24 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('courseware', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PsychometricData',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('done', models.BooleanField(default=False)),
|
||||
('attempts', models.IntegerField(default=0)),
|
||||
('checktimes', models.TextField(null=True, blank=True)),
|
||||
('studentmodule', models.OneToOneField(to='courseware.StudentModule')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
#
|
||||
# db model for psychometrics data
|
||||
#
|
||||
# this data is collected in real time
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from courseware.models import StudentModule
|
||||
|
||||
|
||||
class PsychometricData(models.Model):
|
||||
"""
|
||||
This data is a table linking student, module, and module performance,
|
||||
including number of attempts, grade, max grade, and time of checks.
|
||||
|
||||
Links to instances of StudentModule, but only those for capa problems.
|
||||
|
||||
Note that StudentModule.module_state_key is a :class:`Location` instance.
|
||||
|
||||
checktimes is extracted from tracking logs, or added by capa module via psychometrics callback.
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
app_label = "psychometrics"
|
||||
|
||||
studentmodule = models.OneToOneField(StudentModule, db_index=True) # contains student, module_state_key, course_id
|
||||
|
||||
done = models.BooleanField(default=False)
|
||||
attempts = models.IntegerField(default=0) # extracted from studentmodule.state
|
||||
checktimes = models.TextField(null=True, blank=True) # internally stored as list of datetime objects
|
||||
|
||||
# keep in mind
|
||||
# grade = studentmodule.grade
|
||||
# max_grade = studentmodule.max_grade
|
||||
# student = studentmodule.student
|
||||
# course_id = studentmodule.course_id
|
||||
# location = studentmodule.module_state_key
|
||||
|
||||
def __unicode__(self):
|
||||
sm = self.studentmodule
|
||||
return "[PsychometricData] %s url=%s, grade=%s, max=%s, attempts=%s, ct=%s" % (sm.student,
|
||||
sm.module_state_key,
|
||||
sm.grade,
|
||||
sm.max_grade,
|
||||
self.attempts,
|
||||
self.checktimes)
|
||||
@@ -1,362 +0,0 @@
|
||||
#
|
||||
# File: psychometrics/psychoanalyze.py
|
||||
#
|
||||
# generate pyschometrics plots from PsychometricData
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
import math
|
||||
import numpy as np
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from scipy.optimize import curve_fit
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Sum, Max
|
||||
from psychometrics.models import PsychometricData
|
||||
from courseware.models import StudentModule
|
||||
from pytz import UTC
|
||||
|
||||
log = logging.getLogger("edx.psychometrics")
|
||||
|
||||
#db = "ocwtutor" # for debugging
|
||||
#db = "default"
|
||||
|
||||
db = getattr(settings, 'DATABASE_FOR_PSYCHOMETRICS', 'default')
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# fit functions
|
||||
|
||||
|
||||
def func_2pl(x, a, b):
|
||||
"""
|
||||
2-parameter logistic function
|
||||
"""
|
||||
D = 1.7
|
||||
edax = np.exp(D * a * (x - b))
|
||||
return edax / (1 + edax)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# statistics class
|
||||
|
||||
|
||||
class StatVar(object):
|
||||
"""
|
||||
Simple statistics on floating point numbers: avg, sdv, var, min, max
|
||||
"""
|
||||
def __init__(self, unit=1):
|
||||
self.sum = 0
|
||||
self.sum2 = 0
|
||||
self.cnt = 0
|
||||
self.unit = unit
|
||||
self.min = None
|
||||
self.max = None
|
||||
|
||||
def add(self, x):
|
||||
if x is None:
|
||||
return
|
||||
if self.min is None:
|
||||
self.min = x
|
||||
else:
|
||||
if x < self.min:
|
||||
self.min = x
|
||||
if self.max is None:
|
||||
self.max = x
|
||||
else:
|
||||
if x > self.max:
|
||||
self.max = x
|
||||
self.sum += x
|
||||
self.sum2 += x ** 2
|
||||
self.cnt += 1
|
||||
|
||||
def avg(self):
|
||||
if self.cnt is None:
|
||||
return 0
|
||||
return self.sum / 1.0 / self.cnt / self.unit
|
||||
|
||||
def var(self):
|
||||
if self.cnt is None:
|
||||
return 0
|
||||
return (self.sum2 / 1.0 / self.cnt / (self.unit ** 2)) - (self.avg() ** 2)
|
||||
|
||||
def sdv(self):
|
||||
v = self.var()
|
||||
if v > 0:
|
||||
return math.sqrt(v)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __str__(self):
|
||||
return 'cnt=%d, avg=%f, sdv=%f' % (self.cnt, self.avg(), self.sdv())
|
||||
|
||||
def __add__(self, x):
|
||||
self.add(x)
|
||||
return self
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# histogram generator
|
||||
|
||||
|
||||
def make_histogram(ydata, bins=None):
|
||||
'''
|
||||
Generate histogram of ydata using bins provided, or by default bins
|
||||
from 0 to 100 by 10. bins should be ordered in increasing order.
|
||||
|
||||
returns dict with keys being bins, and values being counts.
|
||||
special: hist['bins'] = bins
|
||||
'''
|
||||
if bins is None:
|
||||
bins = range(0, 100, 10)
|
||||
|
||||
nbins = len(bins)
|
||||
hist = dict(zip(bins, [0] * nbins))
|
||||
for y in ydata:
|
||||
for b in bins[::-1]: # in reverse order
|
||||
if y > b:
|
||||
hist[b] += 1
|
||||
break
|
||||
# hist['bins'] = bins
|
||||
return hist
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def problems_with_psychometric_data(course_id):
|
||||
'''
|
||||
Return dict of {problems (location urls): count} for which psychometric data is available.
|
||||
Does this for a given course_id.
|
||||
'''
|
||||
pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id)
|
||||
plist = [p['studentmodule__module_state_key'] for p in pmdset.values('studentmodule__module_state_key').distinct()]
|
||||
problems = dict(
|
||||
(
|
||||
p,
|
||||
pmdset.filter(
|
||||
studentmodule__module_state_key=BlockUsageLocator.from_string(p)
|
||||
).count()
|
||||
) for p in plist
|
||||
)
|
||||
|
||||
return problems
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def generate_plots_for_problem(problem):
|
||||
|
||||
pmdset = PsychometricData.objects.using(db).filter(
|
||||
studentmodule__module_state_key=BlockUsageLocator.from_string(problem)
|
||||
)
|
||||
nstudents = pmdset.count()
|
||||
msg = ""
|
||||
plots = []
|
||||
|
||||
if nstudents < 2:
|
||||
msg += "%s nstudents=%d --> skipping, too few" % (problem, nstudents)
|
||||
return msg, plots
|
||||
|
||||
max_grade = pmdset[0].studentmodule.max_grade
|
||||
|
||||
agdat = pmdset.aggregate(Sum('attempts'), Max('attempts'))
|
||||
max_attempts = agdat['attempts__max']
|
||||
total_attempts = agdat['attempts__sum'] # not used yet
|
||||
|
||||
msg += "max attempts = %d" % max_attempts
|
||||
|
||||
xdat = range(1, max_attempts + 1)
|
||||
dataset = {'xdat': xdat}
|
||||
|
||||
# compute grade statistics
|
||||
grades = [pmd.studentmodule.grade for pmd in pmdset]
|
||||
gsv = StatVar()
|
||||
for g in grades:
|
||||
gsv += g
|
||||
msg += "<br><p><font color='blue'>Grade distribution: %s</font></p>" % gsv
|
||||
|
||||
# generate grade histogram
|
||||
ghist = []
|
||||
|
||||
axisopts = """{
|
||||
xaxes: [{
|
||||
axisLabel: 'Grade'
|
||||
}],
|
||||
yaxes: [{
|
||||
position: 'left',
|
||||
axisLabel: 'Count'
|
||||
}]
|
||||
}"""
|
||||
|
||||
if gsv.max > max_grade:
|
||||
msg += "<br/><p><font color='red'>Something is wrong: max_grade=%s, but max(grades)=%s</font></p>" % (max_grade, gsv.max)
|
||||
max_grade = gsv.max
|
||||
|
||||
if max_grade > 1:
|
||||
ghist = make_histogram(grades, np.linspace(0, max_grade, max_grade + 1))
|
||||
ghist_json = json.dumps(ghist.items())
|
||||
|
||||
plot = {'title': "Grade histogram for %s" % problem,
|
||||
'id': 'histogram',
|
||||
'info': '',
|
||||
'data': "var dhist = %s;\n" % ghist_json,
|
||||
'cmd': '[ {data: dhist, bars: { show: true, align: "center" }} ], %s' % axisopts,
|
||||
}
|
||||
plots.append(plot)
|
||||
else:
|
||||
msg += "<br/>Not generating histogram: max_grade=%s" % max_grade
|
||||
|
||||
# histogram of time differences between checks
|
||||
# Warning: this is inefficient - doesn't scale to large numbers of students
|
||||
dtset = [] # time differences in minutes
|
||||
dtsv = StatVar()
|
||||
for pmd in pmdset:
|
||||
try:
|
||||
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
|
||||
except:
|
||||
continue
|
||||
if len(checktimes) < 2:
|
||||
continue
|
||||
ct0 = checktimes[0]
|
||||
for ct in checktimes[1:]:
|
||||
dt = (ct - ct0).total_seconds() / 60.0
|
||||
if dt < 20: # ignore if dt too long
|
||||
dtset.append(dt)
|
||||
dtsv += dt
|
||||
ct0 = ct
|
||||
if dtsv.cnt > 2:
|
||||
msg += "<br/><p><font color='brown'>Time differences between checks: %s</font></p>" % dtsv
|
||||
bins = np.linspace(0, 1.5 * dtsv.sdv(), 30)
|
||||
dbar = bins[1] - bins[0]
|
||||
thist = make_histogram(dtset, bins)
|
||||
thist_json = json.dumps(sorted(thist.items(), key=lambda(x): x[0]))
|
||||
|
||||
axisopts = """{ xaxes: [{ axisLabel: 'Time (min)'}], yaxes: [{position: 'left',axisLabel: 'Count'}]}"""
|
||||
|
||||
plot = {'title': "Histogram of time differences between checks",
|
||||
'id': 'thistogram',
|
||||
'info': '',
|
||||
'data': "var thist = %s;\n" % thist_json,
|
||||
'cmd': '[ {data: thist, bars: { show: true, align: "center", barWidth:%f }} ], %s' % (dbar, axisopts),
|
||||
}
|
||||
plots.append(plot)
|
||||
|
||||
# one IRT plot curve for each grade received (TODO: this assumes integer grades)
|
||||
for grade in range(1, int(max_grade) + 1):
|
||||
yset = {}
|
||||
gset = pmdset.filter(studentmodule__grade=grade)
|
||||
ngset = gset.count()
|
||||
if ngset == 0:
|
||||
continue
|
||||
ydat = []
|
||||
ylast = 0
|
||||
for x in xdat:
|
||||
y = gset.filter(attempts=x).count() / ngset
|
||||
ydat.append(y + ylast)
|
||||
ylast = y + ylast
|
||||
yset['ydat'] = ydat
|
||||
|
||||
if len(ydat) > 3: # try to fit to logistic function if enough data points
|
||||
try:
|
||||
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
|
||||
yset['fitparam'] = cfp
|
||||
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
|
||||
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
|
||||
fitx = np.linspace(xdat[0], xdat[-1], 100)
|
||||
yset['fitx'] = fitx
|
||||
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
|
||||
except Exception as err:
|
||||
log.debug('Error in psychoanalyze curve fitting: %s', err)
|
||||
|
||||
dataset['grade_%d' % grade] = yset
|
||||
|
||||
axisopts = """{
|
||||
xaxes: [{
|
||||
axisLabel: 'Number of Attempts'
|
||||
}],
|
||||
yaxes: [{
|
||||
max:1.0,
|
||||
position: 'left',
|
||||
axisLabel: 'Probability of correctness'
|
||||
}]
|
||||
}"""
|
||||
|
||||
# generate points for flot plot
|
||||
for grade in range(1, int(max_grade) + 1):
|
||||
jsdata = ""
|
||||
jsplots = []
|
||||
gkey = 'grade_%d' % grade
|
||||
if gkey in dataset:
|
||||
yset = dataset[gkey]
|
||||
jsdata += "var d%d = %s;\n" % (grade, json.dumps(zip(xdat, yset['ydat'])))
|
||||
jsplots.append('{ data: d%d, lines: { show: false }, points: { show: true}, color: "red" }' % grade)
|
||||
if 'fitpts' in yset:
|
||||
jsdata += 'var fit = %s;\n' % (json.dumps(zip(yset['fitx'], yset['fity'])))
|
||||
jsplots.append('{ data: fit, lines: { show: true }, color: "blue" }')
|
||||
(a, b) = yset['fitparam'][0]
|
||||
irtinfo = "(2PL: D=1.7, a=%6.3f, b=%6.3f)" % (a, b)
|
||||
else:
|
||||
irtinfo = ""
|
||||
|
||||
plots.append({'title': 'IRT Plot for grade=%s %s' % (grade, irtinfo),
|
||||
'id': "irt%s" % grade,
|
||||
'info': '',
|
||||
'data': jsdata,
|
||||
'cmd': '[%s], %s' % (','.join(jsplots), axisopts),
|
||||
})
|
||||
|
||||
#log.debug('plots = %s' % plots)
|
||||
return msg, plots
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_psychometrics_data_update_handler(course_id, user, module_state_key):
|
||||
"""
|
||||
Construct and return a procedure which may be called to update
|
||||
the PsychometricData instance for the given StudentModule instance.
|
||||
"""
|
||||
sm, status = StudentModule.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_state_key=module_state_key,
|
||||
defaults={'state': '{}', 'module_type': 'problem'},
|
||||
)
|
||||
|
||||
try:
|
||||
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
|
||||
except PsychometricData.DoesNotExist:
|
||||
pmd = PsychometricData(studentmodule=sm)
|
||||
|
||||
def psychometrics_data_update_handler(state):
|
||||
"""
|
||||
This function may be called each time a problem is successfully checked
|
||||
(eg on save_problem_check events in capa_module).
|
||||
|
||||
state = instance state (a nice, uniform way to interface - for more future psychometric feature extraction)
|
||||
"""
|
||||
try:
|
||||
state = json.loads(sm.state)
|
||||
done = state['done']
|
||||
except:
|
||||
log.exception("Oops, failed to eval state for %s (state=%s)", sm, sm.state)
|
||||
return
|
||||
|
||||
pmd.done = done
|
||||
try:
|
||||
pmd.attempts = state.get('attempts', 0)
|
||||
except:
|
||||
log.exception("no attempts for %s (state=%s)", sm, sm.state)
|
||||
|
||||
try:
|
||||
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
|
||||
except:
|
||||
checktimes = []
|
||||
checktimes.append(datetime.datetime.now(UTC))
|
||||
pmd.checktimes = checktimes
|
||||
try:
|
||||
pmd.save()
|
||||
except:
|
||||
log.exception("Error in updating psychometrics data for %s", sm)
|
||||
|
||||
return psychometrics_data_update_handler
|
||||
@@ -116,8 +116,6 @@ FEATURES = {
|
||||
# in their emails, and they will have no way to resubscribe.
|
||||
'ENABLE_DISCUSSION_EMAIL_DIGEST': False,
|
||||
|
||||
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
|
||||
|
||||
'ENABLE_DJANGO_ADMIN_SITE': True, # set true to enable django's admin site, even on prod (e.g. for course ops)
|
||||
'ENABLE_SQL_TRACKING_LOGS': False,
|
||||
'ENABLE_LMS_MIGRATION': False,
|
||||
@@ -1833,7 +1831,6 @@ INSTALLED_APPS = (
|
||||
'instructor',
|
||||
'instructor_task',
|
||||
'open_ended_grading',
|
||||
'psychometrics',
|
||||
'licenses',
|
||||
'openedx.core.djangoapps.course_groups',
|
||||
'bulk_email',
|
||||
|
||||
@@ -24,7 +24,6 @@ FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--othe
|
||||
FEATURES['SUBDOMAIN_BRANDING'] = True
|
||||
FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST)
|
||||
FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
|
||||
FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
|
||||
FEATURES['ENABLE_SERVICE_STATUS'] = True
|
||||
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
|
||||
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
|
||||
|
||||
@@ -148,9 +148,6 @@ function goto( mode)
|
||||
%endif
|
||||
|
||||
<h2 class="navbar">[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> |
|
||||
%if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'):
|
||||
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">${_("Psychometrics")}</a> |
|
||||
%endif
|
||||
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">${_("Admin")}</a> |
|
||||
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">${_("Forum Admin")}</a> |
|
||||
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">${_("Enrollment")}</a> |
|
||||
@@ -267,27 +264,6 @@ function goto( mode)
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics'):
|
||||
|
||||
<p>${_("Select a problem and an action:")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<select name="Problem">
|
||||
%for problem, count in sorted(problems.items(), key=lambda x: x[0]):
|
||||
<option value="${problem}">${problem} [${count}]</option>
|
||||
%endfor
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Generate Histogram and IRT Plot">
|
||||
</p>
|
||||
|
||||
<p></p>
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
|
||||
@@ -398,7 +374,7 @@ function goto( mode)
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if datatable and modeflag.get('Psychometrics') is None:
|
||||
%if datatable:
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -491,32 +467,6 @@ function goto( mode)
|
||||
</p>
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics'):
|
||||
|
||||
%for plot in plots:
|
||||
<br/>
|
||||
<h3>${plot['title']}</h3>
|
||||
<br/>
|
||||
<p>${plot['info']}</p>
|
||||
<br/>
|
||||
<div id="plot_${plot['id']}" style="width:600px;height:300px;"></div>
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
${plot['data']}
|
||||
$.plot($("#plot_${plot['id']}"), ${plot['cmd']} );
|
||||
});
|
||||
</script>
|
||||
<br/>
|
||||
<br/>
|
||||
%endfor
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
## always show msg
|
||||
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
% if course_errors is not UNDEFINED:
|
||||
|
||||
Reference in New Issue
Block a user