From ad5b3a334203394792c90b0d1bfe2dda8efe13b3 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 10:30:18 -0400 Subject: [PATCH 01/26] add admin interface for tracking logs --- common/djangoapps/track/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 common/djangoapps/track/admin.py diff --git a/common/djangoapps/track/admin.py b/common/djangoapps/track/admin.py new file mode 100644 index 0000000000..1f19c59a93 --- /dev/null +++ b/common/djangoapps/track/admin.py @@ -0,0 +1,8 @@ +''' +django admin pages for courseware model +''' + +from track.models import * +from django.contrib import admin + +admin.site.register(TrackingLog) From ab0a58fb7a01d7167906d8fe911128e743cf8977 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 22:31:45 -0400 Subject: [PATCH 02/26] add psychometrics - grade histograms, check time diffs, and IRT plots --- common/lib/xmodule/xmodule/capa_module.py | 3 + lms/djangoapps/courseware/module_render.py | 4 + lms/djangoapps/instructor/views.py | 34 +++++- lms/envs/common.py | 3 + lms/envs/dev.py | 2 + .../courseware/instructor_dashboard.html | 105 ++++++++++++++++-- 6 files changed, 142 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d186bcc39c..8e2d12d5e9 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -509,6 +509,9 @@ class CapaModule(XModule): event_info['success'] = success self.system.track_function('save_problem_check', event_info) + if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback + self.system.psychometrics_handler(self.get_instance_state()) + # render problem into HTML html = self.get_problem_html(encapsulate=False) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 88368c4a80..b033660c17 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -16,6 +16,7 @@ from capa.xqueue_interface import XQueueInterface from courseware.access import has_access from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache +from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from static_replace import replace_urls from xmodule.errortracker import exc_info_to_str from xmodule.exceptions import NotFoundError @@ -230,6 +231,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) + if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS') and instance_module is not None: + system.set('psychometrics_handler', # set callback for updating PsychometricsData + make_psychometrics_data_update_handler(instance_module)) try: module = descriptor.xmodule_constructor(system)(instance_state, shared_state) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f4e9c27991..74f186b689 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -27,6 +27,7 @@ from django.views.decorators.cache import cache_control from courseware import grades from courseware.access import has_access, get_access_group_name from courseware.courses import (get_course_with_access, get_courses_by_university) +from psychometrics import psychoanalyze from student.models import UserProfile from student.models import UserTestGroup, CourseEnrollment @@ -51,7 +52,18 @@ def instructor_dashboard(request, course_id): instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists msg = '' - # msg += ('POST=%s' % dict(request.POST)).replace('<','<') + #msg += ('POST=%s' % dict(request.POST)).replace('<','<') + + problems = [] + plots = [] + + # the instructor dashboard page is modal: grades, psychometrics, admin + # keep that state in request.session (defaults to grades mode) + idash_mode = request.POST.get('idash_mode','') + if idash_mode: + request.session['idash_mode'] = idash_mode + else: + idash_mode = request.session.get('idash_mode','Grades') def escape(s): """escape HTML special characters in string""" @@ -149,6 +161,9 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') return return_csv('answer_dist_%s.csv' % course_id, get_answers_distribution(request, course_id)) + #---------------------------------------- + # Admin + elif 'List course staff' in action: group = get_staff_group(course) msg += 'Staff group = %s' % group.name @@ -187,13 +202,28 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff %s' % user, {}, page='idashboard') - # For now, mostly a static page + #---------------------------------------- + # psychometrics + + elif action == 'Generate Histogram and IRT Plot': + problem = request.POST['Problem'] + nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) + msg += nmsg + + if idash_mode=='Psychometrics': + problems = psychoanalyze.problems_with_psychometric_data(course_id) + + #---------------------------------------- + # context for rendering context = {'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, 'instructor_access': instructor_access, 'datatable': datatable, 'msg': msg, + 'modeflag': {idash_mode: 'selectedmode'}, + 'problems': problems, # psychometrics + 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), } diff --git a/lms/envs/common.py b/lms/envs/common.py index 63301d420b..3cfaae940d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -71,6 +71,8 @@ MITX_FEATURES = { 'ENABLE_DISCUSSION' : False, 'ENABLE_DISCUSSION_SERVICE': True, + 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) + 'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_LMS_MIGRATION': False, 'ENABLE_MANUAL_GIT_RELOAD': False, @@ -619,6 +621,7 @@ INSTALLED_APPS = ( 'util', 'certificates', 'instructor', + 'psychometrics', #For the wiki 'wiki', # The new django-wiki from benjaoming diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 50befeb875..5a7e019e55 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -20,6 +20,8 @@ MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains- MITX_FEATURES['SUBDOMAIN_BRANDING'] = True MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True +MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = True # real-time psychometrics (eg item response theory analysis in instructor dashboard) + WIKI_ENABLED = True diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 8568490e5e..d73eda1ed7 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -4,6 +4,8 @@ <%block name="headextra"> <%static:css group='course'/> + + <%include file="/courseware/course_navigation.html" args="active_page='instructor'" /> @@ -31,37 +33,89 @@ table.stat_table td { border-color: #666666; background-color: #ffffff; } + +a.selectedmode { background-color: yellow; } + + +
+

Instructor Dashboard

-
- +

[ Grades | + %if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): + Psychometrics | + %endif + Admin ] +

+ + + + +##----------------------------------------------------------------------------- +%if modeflag.get('Grades'):

Gradebook +

Grade summary +

+

+

+

+

+ %endif -%if instructor_access: +##----------------------------------------------------------------------------- +%if modeflag.get('Psychometrics'): + +

Select a problem and an action: +

+ +

+ +

+

+ +

+ +

+ +%endif + +##----------------------------------------------------------------------------- +%if modeflag.get('Admin'): + %if instructor_access:

@@ -69,16 +123,20 @@ table.stat_table td {


- %endif + %endif -%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access: + %if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:

+ %endif %endif

+##----------------------------------------------------------------------------- +%if modeflag.get('Psychometrics') is None: +

@@ -99,14 +157,45 @@ table.stat_table td { %endfor

+%endif + +##----------------------------------------------------------------------------- +%if modeflag.get('Psychometrics'): + + %for plot in plots: +
+

${plot['title']}

+
+

${plot['info']}

+
+
+ +
+
+ %endfor + +%endif + +##----------------------------------------------------------------------------- +## always show msg %if msg:

${msg}

%endif -% if course_errors is not UNDEFINED: +##----------------------------------------------------------------------------- +%if modeflag.get('Admin'): + % if course_errors is not UNDEFINED:

Course errors

+ %if not course_errors: + None + %else:
    % for (summary, err) in course_errors:
  • ${summary | h} @@ -118,8 +207,10 @@ table.stat_table td {
  • % endfor
+ %endif
-% endif + % endif +%endif
From 0bf85992da090b624fb915cb87a1323736b6a67c Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 22:32:28 -0400 Subject: [PATCH 03/26] psychometrics djangoapp --- lms/djangoapps/psychometrics/__init__.py | 0 lms/djangoapps/psychometrics/admin.py | 8 + .../psychometrics/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/init_psychometrics.py | 66 ++++ lms/djangoapps/psychometrics/models.py | 45 +++ lms/djangoapps/psychometrics/psychoanalyze.py | 312 ++++++++++++++++++ 7 files changed, 431 insertions(+) create mode 100644 lms/djangoapps/psychometrics/__init__.py create mode 100644 lms/djangoapps/psychometrics/admin.py create mode 100644 lms/djangoapps/psychometrics/management/__init__.py create mode 100644 lms/djangoapps/psychometrics/management/commands/__init__.py create mode 100644 lms/djangoapps/psychometrics/management/commands/init_psychometrics.py create mode 100644 lms/djangoapps/psychometrics/models.py create mode 100644 lms/djangoapps/psychometrics/psychoanalyze.py diff --git a/lms/djangoapps/psychometrics/__init__.py b/lms/djangoapps/psychometrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/psychometrics/admin.py b/lms/djangoapps/psychometrics/admin.py new file mode 100644 index 0000000000..ff1a14d722 --- /dev/null +++ b/lms/djangoapps/psychometrics/admin.py @@ -0,0 +1,8 @@ +''' +django admin pages for courseware model +''' + +from psychometrics.models import * +from django.contrib import admin + +admin.site.register(PsychometricData) diff --git a/lms/djangoapps/psychometrics/management/__init__.py b/lms/djangoapps/psychometrics/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/psychometrics/management/commands/__init__.py b/lms/djangoapps/psychometrics/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py new file mode 100644 index 0000000000..b7c9779d08 --- /dev/null +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# +# generate pyschometrics data from tracking logs and student module data + +import os, sys, string +import datetime +import json + +from courseware.models import * +from track.models import * +from psychometrics.models import * +from xmodule.modulestore import Location + +from django.core.management.base import BaseCommand + +#db = "ocwtutor" # for debugging +db = "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: + url = sm.module_state_key + location = Location(url) + if not location.category=="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='save_problem_check') + tset = tset.filter(event_source='server') + tset = tset.filter(event__contains="'%s'" % url) + checktimes = [x.dtcreated for x in tset] + pmd.checktimes = json.dumps(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() diff --git a/lms/djangoapps/psychometrics/models.py b/lms/djangoapps/psychometrics/models.py new file mode 100644 index 0000000000..4ffdf59120 --- /dev/null +++ b/lms/djangoapps/psychometrics/models.py @@ -0,0 +1,45 @@ +# +# 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 nominally a Location instance (url string). + That means it is of the form {tag}://{org}/{course}/{category}/{name}[@{revision}] + and for capa problems, category = "problem". + + checktimes is extracted from tracking logs, or added by capa module via psychometrics callback. + """ + + studentmodule = models.ForeignKey(StudentModule, db_index=True, unique=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) + diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py new file mode 100644 index 0000000000..e8dd7b4684 --- /dev/null +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -0,0 +1,312 @@ +# +# 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 scipy.optimize import curve_fit + +from django.db.models import Sum, Max +from psychometrics.models import * +from xmodule.modulestore import Location + +log = logging.getLogger("mitx.psychometrics") + +#db = "ocwtutor" # for debugging +db = "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 xself.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=p).count()) for p in plist ) + + return problems + +#----------------------------------------------------------------------------- + +def generate_plots_for_problem(problem): + + pmdset = PsychometricData.objects.using(db).filter(studentmodule__module_state_key=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} + + # generate grade histogram + ghist = [] + + axisopts = """{ + xaxes: [{ + axisLabel: 'Grade' + }], + yaxes: [{ + position: 'left', + axisLabel: 'Count' + }] + }""" + + if max_grade > 1: + ghist = make_histogram([pmd.studentmodule.grade for pmd in pmdset],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 }} ], %s" % axisopts, + } + plots.append(plot) + else: + msg += "
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 += "
time differences between checks: %s" % 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, 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)>5: # try to fit to logistic function if enough data points + 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]) + + 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(studentmodule): + """ + Construct and return a procedure which may be called to update + the PsychometricsData instance for the given StudentModule instance. + """ + sm = studentmodule + 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 + pmd.attempts = state['attempts'] + try: + checktimes = eval(pmd.checktimes) # update log of attempt timestamps + except: + checktimes = [] + checktimes.append(datetime.datetime.now()) + pmd.checktimes = checktimes + try: + pmd.save() + except: + log.exception("Error in updating psychometrics data for %s" % sm) + + return psychometrics_data_update_handler From b4a3591310203732ef510488ba7b0da1158b8e50 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 23:21:13 -0400 Subject: [PATCH 04/26] add jquery flot axislabels plugin --- .../js/vendor/flot/jquery.flot.axislabels.js | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 common/static/js/vendor/flot/jquery.flot.axislabels.js diff --git a/common/static/js/vendor/flot/jquery.flot.axislabels.js b/common/static/js/vendor/flot/jquery.flot.axislabels.js new file mode 100644 index 0000000000..797f82ec9f --- /dev/null +++ b/common/static/js/vendor/flot/jquery.flot.axislabels.js @@ -0,0 +1,412 @@ +/* +Axis Labels Plugin for flot. +http://github.com/markrcote/flot-axislabels + +Original code is Copyright (c) 2010 Xuan Luo. +Original code was released under the GPLv3 license by Xuan Luo, September 2010. +Original code was rereleased under the MIT license by Xuan Luo, April 2012. + +Improvements by Mark Cote. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + */ +(function ($) { + var options = { }; + + function canvasSupported() { + return !!document.createElement('canvas').getContext; + } + + function canvasTextSupported() { + if (!canvasSupported()) { + return false; + } + var dummy_canvas = document.createElement('canvas'); + var context = dummy_canvas.getContext('2d'); + return typeof context.fillText == 'function'; + } + + function css3TransitionSupported() { + var div = document.createElement('div'); + return typeof div.style.MozTransition != 'undefined' // Gecko + || typeof div.style.OTransition != 'undefined' // Opera + || typeof div.style.webkitTransition != 'undefined' // WebKit + || typeof div.style.transition != 'undefined'; + } + + + function AxisLabel(axisName, position, padding, plot, opts) { + this.axisName = axisName; + this.position = position; + this.padding = padding; + this.plot = plot; + this.opts = opts; + this.width = 0; + this.height = 0; + } + + + CanvasAxisLabel.prototype = new AxisLabel(); + CanvasAxisLabel.prototype.constructor = CanvasAxisLabel; + function CanvasAxisLabel(axisName, position, padding, plot, opts) { + AxisLabel.prototype.constructor.call(this, axisName, position, padding, + plot, opts); + } + + CanvasAxisLabel.prototype.calculateSize = function() { + if (!this.opts.axisLabelFontSizePixels) + this.opts.axisLabelFontSizePixels = 14; + if (!this.opts.axisLabelFontFamily) + this.opts.axisLabelFontFamily = 'sans-serif'; + + var textWidth = this.opts.axisLabelFontSizePixels + this.padding; + var textHeight = this.opts.axisLabelFontSizePixels + this.padding; + if (this.position == 'left' || this.position == 'right') { + this.width = this.opts.axisLabelFontSizePixels + this.padding; + this.height = 0; + } else { + this.width = 0; + this.height = this.opts.axisLabelFontSizePixels + this.padding; + } + }; + + CanvasAxisLabel.prototype.draw = function(box) { + var ctx = this.plot.getCanvas().getContext('2d'); + ctx.save(); + ctx.font = this.opts.axisLabelFontSizePixels + 'px ' + + this.opts.axisLabelFontFamily; + var width = ctx.measureText(this.opts.axisLabel).width; + var height = this.opts.axisLabelFontSizePixels; + var x, y, angle = 0; + if (this.position == 'top') { + x = box.left + box.width/2 - width/2; + y = box.top + height*0.72; + } else if (this.position == 'bottom') { + x = box.left + box.width/2 - width/2; + y = box.top + box.height - height*0.72; + } else if (this.position == 'left') { + x = box.left + height*0.72; + y = box.height/2 + box.top + width/2; + angle = -Math.PI/2; + } else if (this.position == 'right') { + x = box.left + box.width - height*0.72; + y = box.height/2 + box.top - width/2; + angle = Math.PI/2; + } + ctx.translate(x, y); + ctx.rotate(angle); + ctx.fillText(this.opts.axisLabel, 0, 0); + ctx.restore(); + }; + + + HtmlAxisLabel.prototype = new AxisLabel(); + HtmlAxisLabel.prototype.constructor = HtmlAxisLabel; + function HtmlAxisLabel(axisName, position, padding, plot, opts) { + AxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + } + + HtmlAxisLabel.prototype.calculateSize = function() { + var elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + // store height and width of label itself, for use in draw() + this.labelWidth = elem.outerWidth(true); + this.labelHeight = elem.outerHeight(true); + elem.remove(); + + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelWidth + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + HtmlAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove(); + var elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + if (this.position == 'top') { + elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px'); + elem.css('top', box.top + 'px'); + } else if (this.position == 'bottom') { + elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px'); + elem.css('top', box.top + box.height - this.labelHeight + 'px'); + } else if (this.position == 'left') { + elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px'); + elem.css('left', box.left + 'px'); + } else if (this.position == 'right') { + elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px'); + elem.css('left', box.left + box.width - this.labelWidth + 'px'); + } + }; + + + CssTransformAxisLabel.prototype = new HtmlAxisLabel(); + CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel; + function CssTransformAxisLabel(axisName, position, padding, plot, opts) { + HtmlAxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + } + + CssTransformAxisLabel.prototype.calculateSize = function() { + HtmlAxisLabel.prototype.calculateSize.call(this); + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelHeight + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + var stransforms = { + '-moz-transform': '', + '-webkit-transform': '', + '-o-transform': '', + '-ms-transform': '' + }; + if (x != 0 || y != 0) { + var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)'; + stransforms['-moz-transform'] += stdTranslate; + stransforms['-webkit-transform'] += stdTranslate; + stransforms['-o-transform'] += stdTranslate; + stransforms['-ms-transform'] += stdTranslate; + } + if (degrees != 0) { + var rotation = degrees / 90; + var stdRotate = ' rotate(' + degrees + 'deg)'; + stransforms['-moz-transform'] += stdRotate; + stransforms['-webkit-transform'] += stdRotate; + stransforms['-o-transform'] += stdRotate; + stransforms['-ms-transform'] += stdRotate; + } + var s = 'top: 0; left: 0; '; + for (var prop in stransforms) { + if (stransforms[prop]) { + s += prop + ':' + stransforms[prop] + ';'; + } + } + s += ';'; + return s; + }; + + CssTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = { x: 0, y: 0, degrees: 0 }; + if (this.position == 'bottom') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top + box.height - this.labelHeight; + } else if (this.position == 'top') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top; + } else if (this.position == 'left') { + offsets.degrees = -90; + offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } else if (this.position == 'right') { + offsets.degrees = 90; + offsets.x = box.left + box.width - this.labelWidth/2 + - this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } + return offsets; + }; + + CssTransformAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find("." + this.axisName + "Label").remove(); + var offsets = this.calculateOffsets(box); + var elem = $('
' + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + }; + + + IeTransformAxisLabel.prototype = new CssTransformAxisLabel(); + IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel; + function IeTransformAxisLabel(axisName, position, padding, plot, opts) { + CssTransformAxisLabel.prototype.constructor.call(this, axisName, + position, padding, + plot, opts); + this.requiresResize = false; + } + + IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + // I didn't feel like learning the crazy Matrix stuff, so this uses + // a combination of the rotation transform and CSS positioning. + var s = ''; + if (degrees != 0) { + var rotation = degrees/90; + while (rotation < 0) { + rotation += 4; + } + s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); '; + // see below + this.requiresResize = (this.position == 'right'); + } + if (x != 0) { + s += 'left: ' + x + 'px; '; + } + if (y != 0) { + s += 'top: ' + y + 'px; '; + } + return s; + }; + + IeTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call( + this, box); + // adjust some values to take into account differences between + // CSS and IE rotations. + if (this.position == 'top') { + // FIXME: not sure why, but placing this exactly at the top causes + // the top axis label to flip to the bottom... + offsets.y = box.top + 1; + } else if (this.position == 'left') { + offsets.x = box.left; + offsets.y = box.height/2 + box.top - this.labelWidth/2; + } else if (this.position == 'right') { + offsets.x = box.left + box.width - this.labelHeight; + offsets.y = box.height/2 + box.top - this.labelWidth/2; + } + return offsets; + }; + + IeTransformAxisLabel.prototype.draw = function(box) { + CssTransformAxisLabel.prototype.draw.call(this, box); + if (this.requiresResize) { + var elem = this.plot.getPlaceholder().find("." + this.axisName + "Label"); + // Since we used CSS positioning instead of transforms for + // translating the element, and since the positioning is done + // before any rotations, we have to reset the width and height + // in case the browser wrapped the text (specifically for the + // y2axis). + elem.css('width', this.labelWidth); + elem.css('height', this.labelHeight); + } + }; + + + function init(plot) { + // This is kind of a hack. There are no hooks in Flot between + // the creation and measuring of the ticks (setTicks, measureTickLabels + // in setupGrid() ) and the drawing of the ticks and plot box + // (insertAxisLabels in setupGrid() ). + // + // Therefore, we use a trick where we run the draw routine twice: + // the first time to get the tick measurements, so that we can change + // them, and then have it draw it again. + var secondPass = false; + + var axisLabels = {}; + var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 }; + + var defaultPadding = 2; // padding between axis and tick labels + plot.hooks.draw.push(function (plot, ctx) { + if (!secondPass) { + // MEASURE AND SET OPTIONS + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options // Flot 0.7 + || plot.getOptions()[axisName]; // Flot 0.6 + if (!opts || !opts.axisLabel || !axis.show) + return; + + var renderer = null; + + if (!opts.axisLabelUseHtml && + navigator.appName == 'Microsoft Internet Explorer') { + var ua = navigator.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) { + rv = parseFloat(RegExp.$1); + } + if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { + renderer = CssTransformAxisLabel; + } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { + renderer = IeTransformAxisLabel; + } else if (opts.axisLabelUseCanvas) { + renderer = CanvasAxisLabel; + } else { + renderer = HtmlAxisLabel; + } + } else { + if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) { + renderer = HtmlAxisLabel; + } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) { + renderer = CanvasAxisLabel; + } else { + renderer = CssTransformAxisLabel; + } + } + + var padding = opts.axisLabelPadding === undefined ? + defaultPadding : opts.axisLabelPadding; + + axisLabels[axisName] = new renderer(axisName, + axis.position, padding, + plot, opts); + + // flot interprets axis.labelHeight and .labelWidth as + // the height and width of the tick labels. We increase + // these values to make room for the axis label and + // padding. + + axisLabels[axisName].calculateSize(); + + // AxisLabel.height and .width are the size of the + // axis label and padding. + axis.labelHeight += axisLabels[axisName].height; + axis.labelWidth += axisLabels[axisName].width; + opts.labelHeight = axis.labelHeight; + opts.labelWidth = axis.labelWidth; + }); + // re-draw with new label widths and heights + secondPass = true; + plot.setupGrid(); + plot.draw(); + } else { + // DRAW + $.each(plot.getAxes(), function(axisName, axis) { + var opts = axis.options // Flot 0.7 + || plot.getOptions()[axisName]; // Flot 0.6 + if (!opts || !opts.axisLabel || !axis.show) + return; + + axisLabels[axisName].draw(axis.box); + }); + } + }); + } + + + $.plot.plugins.push({ + init: init, + options: options, + name: 'axisLabels', + version: '2.0b0' + }); +})(jQuery); From 2045500c6e724300089d5ef98273533cf3951b3b Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 8 Sep 2012 23:21:34 -0400 Subject: [PATCH 05/26] make external_auth table searchable in django admin; fix missed instance of login link which should have been behind DISABLE_LOGIN_BUTTON --- common/djangoapps/external_auth/admin.py | 6 +++++- lms/templates/portal/course_about.html | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/external_auth/admin.py b/common/djangoapps/external_auth/admin.py index 343492bca7..e93325bcb2 100644 --- a/common/djangoapps/external_auth/admin.py +++ b/common/djangoapps/external_auth/admin.py @@ -5,4 +5,8 @@ django admin pages for courseware model from external_auth.models import * from django.contrib import admin -admin.site.register(ExternalAuthMap) +class ExternalAuthMapAdmin(admin.ModelAdmin): + search_fields = ['external_id','user__username'] + date_hierarchy = 'dtcreated' + +admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin) diff --git a/lms/templates/portal/course_about.html b/lms/templates/portal/course_about.html index ac7b9090d0..8d2d45117d 100644 --- a/lms/templates/portal/course_about.html +++ b/lms/templates/portal/course_about.html @@ -76,7 +76,11 @@
%endif %else: - Register for ${course.number} + Log In +% endif + to enroll.'>Register for ${course.number} %endif From 31dd119e79fe3de694e3719302449a1272592155 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 9 Sep 2012 17:50:11 -0400 Subject: [PATCH 06/26] settings.DATABASE_FOR_PSYCHOMETRICS overrides db for psychometrics --- .../management/commands/init_psychometrics.py | 8 ++++++-- lms/djangoapps/psychometrics/psychoanalyze.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index b7c9779d08..5e782df595 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -11,10 +11,14 @@ from track.models import * from psychometrics.models import * from xmodule.modulestore import Location +from django.conf import settings from django.core.management.base import BaseCommand #db = "ocwtutor" # for debugging -db = "default" +#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)." @@ -56,7 +60,7 @@ class Command(BaseCommand): tset = tset.filter(event_source='server') tset = tset.filter(event__contains="'%s'" % url) checktimes = [x.dtcreated for x in tset] - pmd.checktimes = json.dumps(checktimes) + pmd.checktimes = checktimes if not len(checktimes)==pmd.attempts: print "Oops, mismatch in number of attempts and check times for %s" % pmd diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index e8dd7b4684..c798c73609 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -12,6 +12,7 @@ import math import numpy as np from scipy.optimize import curve_fit +from django.conf import settings from django.db.models import Sum, Max from psychometrics.models import * from xmodule.modulestore import Location @@ -19,7 +20,9 @@ from xmodule.modulestore import Location log = logging.getLogger("mitx.psychometrics") #db = "ocwtutor" # for debugging -db = "default" +#db = "default" + +db = getattr(settings,'DATABASE_FOR_PSYCHOMETRICS','default') #----------------------------------------------------------------------------- # fit functions From ccbbb5a99388ace2f1295cb46125bb4a095e4f4f Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 9 Sep 2012 19:39:11 -0400 Subject: [PATCH 07/26] commit_id may be specified for modulestore reload - for multi-worker configs also show pid in migration and instructor dashboard views for thread debugging --- lms/djangoapps/instructor/views.py | 1 + lms/djangoapps/lms_migration/migrate.py | 38 ++++++++++++++++--- .../courseware/instructor_dashboard.html | 2 + lms/urls.py | 1 + 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 74f186b689..f78a1160de 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -225,6 +225,7 @@ def instructor_dashboard(request, course_id): 'problems': problems, # psychometrics 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), + 'djangopid' : os.getpid(), } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index a3a1c595be..403063f983 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -35,7 +35,17 @@ def getip(request): ip = request.META.get('REMOTE_ADDR','None') return ip -def manage_modulestores(request,reload_dir=None): + +def get_commit_id(course): + return course.metadata.get('GIT_COMMIT_ID','No commit id') + # getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id') + + +def set_commit_id(course,commit_id): + course.metadata['GIT_COMMIT_ID'] = commit_id + # setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id) + +def manage_modulestores(request, reload_dir=None, commit_id=None): ''' Manage the static in-memory modulestores. @@ -52,8 +62,9 @@ def manage_modulestores(request,reload_dir=None): ip = getip(request) if LOCAL_DEBUG: - html += '

IP address: %s ' % ip - html += '

User: %s ' % request.user + html += '

IP address: %s

' % ip + html += '

User: %s

' % request.user + html += '

My pid: %s

' % os.getpid() log.debug('request from ip=%s, user=%s' % (ip,request.user)) if not (ip in ALLOWED_IPS or 'any' in ALLOWED_IPS): @@ -66,14 +77,27 @@ def manage_modulestores(request,reload_dir=None): return HttpResponse(html, status=403) #---------------------------------------- - # reload course if specified + # reload course if specified; handle optional commit_id if reload_dir is not None: if reload_dir not in def_ms.courses: html += '

Error: "%s" is not a valid course directory

' % reload_dir else: - html += '

Reloaded course directory "%s"

' % reload_dir - def_ms.try_load_course(reload_dir) + # reloading based on commit_id is needed when running mutiple worker threads, + # so that a given thread doesn't reload the same commit multiple times + current_commit_id = get_commit_id(def_ms.courses[reload_dir]) + log.debug('commit_id="%s"' % commit_id) + log.debug('current_commit_id="%s"' % current_commit_id) + + if (commit_id is not None) and (commit_id==current_commit_id): + html += "

Already at commit id %s for %s

" % (commit_id, reload_dir) + else: + html += '

Reloaded course directory "%s"

' % reload_dir + def_ms.try_load_course(reload_dir) + gdir = settings.DATA_DIR / reload_dir + new_commit_id = os.popen('cd %s; git log -n 1 | head -1' % gdir).read().strip().split(' ')[1] + set_commit_id(def_ms.courses[reload_dir], new_commit_id) + html += '

commit_id=%s

' % new_commit_id #---------------------------------------- @@ -94,6 +118,8 @@ def manage_modulestores(request,reload_dir=None): html += '
' html += '

Course: %s (%s)

' % (course.display_name,cdir) + html += '

commit_id=%s

' % get_commit_id(course) + for field in dumpfields: data = getattr(course,field) html += '

%s

' % field diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index d73eda1ed7..e822f05f92 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -59,6 +59,8 @@ function goto( mode) Admin ] +
${djangopid}
+
diff --git a/lms/urls.py b/lms/urls.py index 8484ccc40b..49febaf84e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -237,6 +237,7 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): urlpatterns += ( url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'), url(r'^migrate/reload/(?P[^/]+)$', 'lms_migration.migrate.manage_modulestores'), + url(r'^migrate/reload/(?P[^/]+)/(?P[^/]+)$', 'lms_migration.migrate.manage_modulestores'), url(r'^gitreload$', 'lms_migration.migrate.gitreload'), url(r'^gitreload/(?P[^/]+)$', 'lms_migration.migrate.gitreload'), ) From 2f2e71e0d12c69c1f652ab65ee13ebc702ae2535 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 9 Sep 2012 21:08:31 -0400 Subject: [PATCH 08/26] add tracking log entries for modulestore reload --- lms/djangoapps/lms_migration/migrate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index 403063f983..ecde31d6dd 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -91,6 +91,12 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): if (commit_id is not None) and (commit_id==current_commit_id): html += "

Already at commit id %s for %s

" % (commit_id, reload_dir) + track.views.server_track(request, + 'reload %s skipped already at %s (pid=%s)' % (reload_dir, + commit_id, + os.getpid(), + ), + {}, page='migrate') else: html += '

Reloaded course directory "%s"

' % reload_dir def_ms.try_load_course(reload_dir) @@ -98,6 +104,9 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): new_commit_id = os.popen('cd %s; git log -n 1 | head -1' % gdir).read().strip().split(' ')[1] set_commit_id(def_ms.courses[reload_dir], new_commit_id) html += '

commit_id=%s

' % new_commit_id + track.views.server_track(request, 'reloaded %s now at %s (pid=%s)' % (reload_dir, + new_commit_id, + os.getpid()), {}, page='migrate') #---------------------------------------- From c1d92f9351de610984fd83ab445a9927ca66caed Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 16:57:47 -0400 Subject: [PATCH 09/26] track psychometrics requests; add grade statistics, check for wrong value of max_grade (something not right about StudentModule.max_grade) --- lms/djangoapps/instructor/views.py | 1 + lms/djangoapps/psychometrics/psychoanalyze.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f78a1160de..d812791c3d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -209,6 +209,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg + track.views.server_track(request, 'psychometrics %s' % problem, {}, page='idashboard') if idash_mode=='Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index c798c73609..bb2a6ba6a8 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -146,6 +146,13 @@ def generate_plots_for_problem(problem): 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 += "

Grade distribution: %s

" % gsv + # generate grade histogram ghist = [] @@ -159,8 +166,12 @@ def generate_plots_for_problem(problem): }] }""" + if gsv.max > max_grade: + msg += "

Something is wrong: max_grade=%s, but max(grades)=%s

" % (max_grade, gsv.max) + max_grade = gsv.max + if max_grade > 1: - ghist = make_histogram([pmd.studentmodule.grade for pmd in pmdset],np.linspace(0,max_grade,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, @@ -192,7 +203,7 @@ def generate_plots_for_problem(problem): dtsv += dt ct0 = ct if dtsv.cnt > 2: - msg += "
time differences between checks: %s" % dtsv + msg += "

Time differences between checks: %s

" % dtsv bins = np.linspace(0,1.5*dtsv.sdv(),30) dbar = bins[1]-bins[0] thist = make_histogram(dtset,bins) @@ -223,7 +234,7 @@ def generate_plots_for_problem(problem): ylast = y + ylast yset['ydat'] = ydat - if len(ydat)>5: # try to fit to logistic function if enough data points + if len(ydat)>3: # try to fit to logistic function if enough data points 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]) From e62e58dc6eae4eb32e4730643c721ba90329cd53 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 20:33:56 -0400 Subject: [PATCH 10/26] log attempts in tracking --- common/lib/xmodule/xmodule/capa_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 8e2d12d5e9..8bf1a56404 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -507,6 +507,7 @@ class CapaModule(XModule): # 'success' will always be incorrect event_info['correct_map'] = correct_map.get_dict() event_info['success'] = success + event_info['attempts'] = self.attempts self.system.track_function('save_problem_check', event_info) if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback From 97a32e64eef6fcfca93876f04580fed8207e877f Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:33:54 -0400 Subject: [PATCH 11/26] fix tooltip titles in seq_module for non-new-xml-format courses --- common/lib/xmodule/xmodule/seq_module.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 65f692957c..26bdd286c3 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -75,7 +75,7 @@ class SequenceModule(XModule): contents = [] for child in self.get_display_items(): progress = child.get_progress() - contents.append({ + childinfo = { 'content': child.get_html(), 'title': "\n".join( grand_child.display_name.strip() @@ -85,7 +85,10 @@ class SequenceModule(XModule): 'progress_status': Progress.to_js_status_str(progress), 'progress_detail': Progress.to_js_detail_str(progress), 'type': child.get_icon_class(), - }) + } + if childinfo['title']=='': + childinfo['title'] = child.metadata['display_name'] + contents.append(childinfo) params = {'items': contents, 'element_id': self.location.html_id(), From d8c8c85041b30a95ef16272235a242fca787fc71 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:42:44 -0400 Subject: [PATCH 12/26] tabs to spaces --- common/lib/xmodule/xmodule/seq_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 26bdd286c3..e7e97626db 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -75,7 +75,7 @@ class SequenceModule(XModule): contents = [] for child in self.get_display_items(): progress = child.get_progress() - childinfo = { + childinfo = { 'content': child.get_html(), 'title': "\n".join( grand_child.display_name.strip() @@ -86,7 +86,7 @@ class SequenceModule(XModule): 'progress_detail': Progress.to_js_detail_str(progress), 'type': child.get_icon_class(), } - if childinfo['title']=='': + if childinfo['title']=='': childinfo['title'] = child.metadata['display_name'] contents.append(childinfo) From c6f794547fd77dd99fa9ad8b64ada3fdb9bfbb61 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:45:38 -0400 Subject: [PATCH 13/26] more tabs -> spaces --- common/lib/xmodule/xmodule/seq_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index e7e97626db..607c1e500e 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -87,7 +87,7 @@ class SequenceModule(XModule): 'type': child.get_icon_class(), } if childinfo['title']=='': - childinfo['title'] = child.metadata['display_name'] + childinfo['title'] = child.metadata['display_name'] contents.append(childinfo) params = {'items': contents, From 56d44ec7f39fe47cebb1c6e50f555e7cb259b2a4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 21:55:58 -0400 Subject: [PATCH 14/26] display_name may be empty (eg in tests) --- common/lib/xmodule/xmodule/seq_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 607c1e500e..b05ea36e50 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -87,7 +87,7 @@ class SequenceModule(XModule): 'type': child.get_icon_class(), } if childinfo['title']=='': - childinfo['title'] = child.metadata['display_name'] + childinfo['title'] = child.metadata.get('display_name','') contents.append(childinfo) params = {'items': contents, From bffd9ac38d799e7adb447c018391c2734bbe9840 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 10 Sep 2012 22:28:37 -0400 Subject: [PATCH 15/26] center histogram bars; pep8 --- lms/djangoapps/psychometrics/psychoanalyze.py | 120 ++++++++++-------- 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index bb2a6ba6a8..dd7d328278 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -1,7 +1,7 @@ # # File: psychometrics/psychoanalyze.py # -# generate pyschometrics plots from PsychometricData +# generate pyschometrics plots from PsychometricData from __future__ import division @@ -19,98 +19,108 @@ from xmodule.modulestore import Location log = logging.getLogger("mitx.psychometrics") -#db = "ocwtutor" # for debugging +#db = "ocwtutor" # for debugging #db = "default" -db = getattr(settings,'DATABASE_FOR_PSYCHOMETRICS','default') +db = getattr(settings, 'DATABASE_FOR_PSYCHOMETRICS', 'default') #----------------------------------------------------------------------------- # fit functions -def func_2pl(x,a,b): + +def func_2pl(x, a, b): """ 2-parameter logistic function """ D = 1.7 - edax = np.exp(D*a*(x-b)) - return edax / (1+edax) + 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): + 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): + + def add(self, x): if x is None: return if self.min is None: self.min = x else: - if xself.max: + 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): + 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): + +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) - + bins = range(0, 100, 10) + nbins = len(bins) - hist = dict(zip(bins,[0] * nbins)) + hist = dict(zip(bins, [0] * nbins)) for y in ydata: - for b in bins[::-1]: # in reverse order + 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. @@ -118,36 +128,37 @@ def problems_with_psychometric_data(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=p).count()) for p in plist ) + problems = dict( (p, pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist ) return problems #----------------------------------------------------------------------------- + def generate_plots_for_problem(problem): - + pmdset = PsychometricData.objects.using(db).filter(studentmodule__module_state_key=problem) nstudents = pmdset.count() msg = "" plots = [] if nstudents < 2: - msg += "%s nstudents=%d --> skipping, too few" % (problem,nstudents) + 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 + total_attempts = agdat['attempts__sum'] # not used yet msg += "max attempts = %d" % max_attempts - xdat = range(1,max_attempts+1) + xdat = range(1, max_attempts + 1) dataset = {'xdat': xdat} # compute grade statistics - grades = [pmd.studentmodule.grade for pmd in pmdset] + grades = [pmd.studentmodule.grade for pmd in pmdset] gsv = StatVar() for g in grades: gsv += g @@ -171,14 +182,14 @@ def generate_plots_for_problem(problem): max_grade = gsv.max if max_grade > 1: - ghist = make_histogram(grades, np.linspace(0,max_grade,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 }} ], %s" % axisopts, + 'cmd': '[ {data: dhist, bars: { show: true, align: "center" }} ], %s' % axisopts, } plots.append(plot) else: @@ -186,27 +197,27 @@ def generate_plots_for_problem(problem): # histogram of time differences between checks # Warning: this is inefficient - doesn't scale to large numbers of students - dtset = [] # time differences in minutes + dtset = [] # time differences in minutes dtsv = StatVar() for pmd in pmdset: try: - checktimes = eval(pmd.checktimes) # update log of attempt timestamps + checktimes = eval(pmd.checktimes) # update log of attempt timestamps except: continue - if len(checktimes)<2: + 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 + 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 += "

Time differences between checks: %s

" % dtsv - bins = np.linspace(0,1.5*dtsv.sdv(),30) - dbar = bins[1]-bins[0] - thist = make_histogram(dtset,bins) + 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'}]}""" @@ -215,33 +226,33 @@ def generate_plots_for_problem(problem): 'id': 'thistogram', 'info': '', 'data': "var thist = %s;\n" % thist_json, - 'cmd': "[ {data: thist, bars: { show: true, barWidth:%f }} ], %s" % (dbar, axisopts), + '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): + for grade in range(1, int(max_grade) + 1): yset = {} gset = pmdset.filter(studentmodule__grade=grade) ngset = gset.count() - if ngset==0: + if ngset == 0: continue ydat = [] ylast = 0 for x in xdat: - y = gset.filter(attempts=x).count()/ngset + 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 - cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts/2.0]) + if len(ydat) > 3: # try to fit to logistic function if enough data points + 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['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]) + yset['fity'] = func_2pl(np.array(fitx), *cfp[0]) dataset['grade_%d' % grade] = yset @@ -257,27 +268,27 @@ def generate_plots_for_problem(problem): }""" # generate points for flot plot - for grade in range(1,int(max_grade)+1): + 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']))) + 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']))) + 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) + (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), + plots.append({'title': 'IRT Plot for grade=%s %s' % (grade, irtinfo), 'id': "irt%s" % grade, 'info': '', 'data': jsdata, - 'cmd' : '[%s], %s' % (','.join(jsplots), axisopts), + 'cmd': '[%s], %s' % (','.join(jsplots), axisopts), }) #log.debug('plots = %s' % plots) @@ -285,6 +296,7 @@ def generate_plots_for_problem(problem): #----------------------------------------------------------------------------- + def make_psychometrics_data_update_handler(studentmodule): """ Construct and return a procedure which may be called to update @@ -307,13 +319,13 @@ def make_psychometrics_data_update_handler(studentmodule): state = json.loads(sm.state) done = state['done'] except: - log.exception("Oops, failed to eval state for %s (state=%s)" % (sm,sm.state)) + log.exception("Oops, failed to eval state for %s (state=%s)" % (sm, sm.state)) return pmd.done = done pmd.attempts = state['attempts'] try: - checktimes = eval(pmd.checktimes) # update log of attempt timestamps + checktimes = eval(pmd.checktimes) # update log of attempt timestamps except: checktimes = [] checktimes.append(datetime.datetime.now()) From 3fd640aef326f96241384e339f79f767748d5db1 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 10:22:03 -0400 Subject: [PATCH 16/26] Removed display none for histogram --- lms/static/sass/course/courseware/_courseware.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 0532f04b42..863bcad139 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -80,7 +80,6 @@ div.course-wrapper { } .histogram { - display: none; width: 200px; height: 150px; } From 064b5e793252c2afb7cc2f2a1722ab0f26f9ca55 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 11:25:54 -0400 Subject: [PATCH 17/26] Remove widows in accordion header --- lms/templates/courseware/courseware.html | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 29b9be3789..a433ddc9fc 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -12,6 +12,7 @@ + ## codemirror @@ -21,6 +22,7 @@ ## ## + <%static:js group='courseware'/> <%static:js group='discussion'/> @@ -35,6 +37,22 @@ From 4732e39cb4d0da12c28f6078bb950a089798ffb0 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 12:09:42 -0400 Subject: [PATCH 18/26] info-title margin fixed --- lms/static/sass/course/_info.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index e25bb9d8c4..80db054afd 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -25,11 +25,6 @@ div.info-wrapper { margin-bottom: lh(); padding-bottom: lh(.5); - &:first-child { - margin: 0 (-(lh(.5))) lh(); - padding: lh(.5); - } - ol, ul { margin: 0; list-style-type: disk; From 7a248f0154bdba45874dba0cec5c6b4baa2f3b1f Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 11 Sep 2012 14:05:07 -0400 Subject: [PATCH 19/26] Added fixes to some style and one Cale fix to get things working properly --- cms/djangoapps/contentstore/views.py | 2 +- cms/static/sass/_base.scss | 2 ++ cms/static/sass/_calendar.scss | 5 ----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 490f49a41c..d701db33a3 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -53,7 +53,7 @@ def index(request): """ courses = modulestore().get_items(['i4x', None, None, 'course', None]) return render_to_response('index.html', { - 'courses': [(course.metadata['display_name'], + 'courses': [(course.metadata.get('display_name'), reverse('course_index', args=[ course.location.org, course.location.course, diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 410f74ee07..90a9629351 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -14,9 +14,11 @@ $yellow: #fff8af; $cream: #F6EFD4; $border-color: #ddd; + // edX colors $blue: rgb(29,157,217); $pink: rgb(182,37,104); +$error-red: rgb(253, 87, 87); @mixin hide-text { background-color: transparent; diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss index 35609b2d56..4c007bb561 100644 --- a/cms/static/sass/_calendar.scss +++ b/cms/static/sass/_calendar.scss @@ -330,11 +330,6 @@ section.cal { &:hover { opacity: 1; - width: flex-grid(5) + flex-gutter(); - - + section.main-content { - width: flex-grid(7); - } } > header { From f35412f3f4e3fd8c048632db83b277014b41f011 Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 11 Sep 2012 16:29:17 -0400 Subject: [PATCH 20/26] Don't bind update_schematics to global window.onload --- common/lib/xmodule/xmodule/js/src/capa/schematic.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/schematic.js b/common/lib/xmodule/xmodule/js/src/capa/schematic.js index e07b98d63c..b01f6e12e8 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/schematic.js +++ b/common/lib/xmodule/xmodule/js/src/capa/schematic.js @@ -2023,7 +2023,16 @@ function add_schematic_handler(other_onload) { update_schematics(); } } -window.onload = add_schematic_handler(window.onload); +/* + * THK: Attaching update_schematic to window.onload is rather presumptuous... + * The function is called for EVERY page load, whether in courseware or in + * course info, in 6.002x or the public health course. It is also redundant + * because courseware includes an explicit call to update_schematic after + * each ajax exchange. In this case, calling update_schematic twice appears + * to contribute to a bug in Firefox that does not render the schematic + * properly depending on timing. + */ +//window.onload = add_schematic_handler(window.onload); // ask each schematic input widget to update its value field for submission function prepare_schematics() { From 5e4a498cfe0745396b2f2a88c3c310a97e8bd86f Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 11 Sep 2012 15:09:49 -0700 Subject: [PATCH 21/26] Provide a reasonable seeding mechanism for jsresponses --- common/lib/capa/capa/javascript_problem_generator.js | 4 +--- common/lib/capa/capa/responsetypes.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/javascript_problem_generator.js b/common/lib/capa/capa/javascript_problem_generator.js index 8c8d39b19f..1cd4616c5a 100644 --- a/common/lib/capa/capa/javascript_problem_generator.js +++ b/common/lib/capa/capa/javascript_problem_generator.js @@ -11,13 +11,11 @@ importAll("xproblem"); generatorModulePath = process.argv[2]; dependencies = JSON.parse(process.argv[3]); -seed = process.argv[4]; +seed = JSON.parse(process.argv[4]); params = JSON.parse(process.argv[5]); if(seed==null){ seed = 4; -}else{ - seed = parseInt(seed); } for(var i = 0; i < dependencies.length; i++){ diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b803452b8c..d9216f06d6 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -408,7 +408,7 @@ class JavascriptResponse(LoncapaResponse): output = self.call_node([generator_file, self.generator, json.dumps(self.generator_dependencies), - json.dumps(str(self.system.seed)), + json.dumps(self.context['random'].getrandbits(9)), json.dumps(self.params)]).strip() return json.loads(output) From bd5fc64462449fe0085c931bb75e1227a13cd587 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 11 Sep 2012 18:08:27 -0700 Subject: [PATCH 22/26] make seeds consistent with other responsetypes --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index d9216f06d6..7f1ff32f67 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -408,7 +408,7 @@ class JavascriptResponse(LoncapaResponse): output = self.call_node([generator_file, self.generator, json.dumps(self.generator_dependencies), - json.dumps(self.context['random'].getrandbits(9)), + json.dumps(str(self.context['the_lcp'].seed)), json.dumps(self.params)]).strip() return json.loads(output) From 05c3e028c2d9b37587de4080a549ad1b72d787f7 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 09:32:24 -0400 Subject: [PATCH 23/26] Added new press releases --- .../images/press/baltsun_logo_178x138.jpg | Bin 0 -> 3779 bytes .../images/press/bostinno_logo_178x138.jpg | Bin 0 -> 3865 bytes .../images/press/bostonmag_logo_178x138.jpg | Bin 0 -> 3441 bytes .../images/press/csmonitor_logo_178x138.jpg | Bin 0 -> 5130 bytes .../press/insidehighered_logo_178x138.jpg | Bin 0 -> 5268 bytes .../images/press/itbriefing_logo_178x138.jpg | Bin 0 -> 2904 bytes .../images/press/radioboston_logo_178x138.jpg | Bin 0 -> 5558 bytes .../images/press/smartplanet_logo_178x138.jpg | Bin 0 -> 2855 bytes .../images/press/techreview_logo_178x138.jpg | Bin 0 -> 4261 bytes .../images/press/thetech_logo_178x138.jpg | Bin 0 -> 3958 bytes lms/static/images/press/time_logo_178x138.jpg | Bin 0 -> 4067 bytes lms/templates/press.json | 183 ++++++++++++++++++ lms/templates/static_templates/press.html | 1 - 13 files changed, 183 insertions(+), 1 deletion(-) create mode 100755 lms/static/images/press/baltsun_logo_178x138.jpg create mode 100644 lms/static/images/press/bostinno_logo_178x138.jpg create mode 100755 lms/static/images/press/bostonmag_logo_178x138.jpg create mode 100755 lms/static/images/press/csmonitor_logo_178x138.jpg create mode 100755 lms/static/images/press/insidehighered_logo_178x138.jpg create mode 100755 lms/static/images/press/itbriefing_logo_178x138.jpg create mode 100755 lms/static/images/press/radioboston_logo_178x138.jpg create mode 100755 lms/static/images/press/smartplanet_logo_178x138.jpg create mode 100755 lms/static/images/press/techreview_logo_178x138.jpg create mode 100755 lms/static/images/press/thetech_logo_178x138.jpg create mode 100755 lms/static/images/press/time_logo_178x138.jpg diff --git a/lms/static/images/press/baltsun_logo_178x138.jpg b/lms/static/images/press/baltsun_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..3dc619ac7b3caba28efcb41da9d0c22db1461106 GIT binary patch literal 3779 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 Df5!ho literal 0 HcmV?d00001 diff --git a/lms/static/images/press/bostonmag_logo_178x138.jpg b/lms/static/images/press/bostonmag_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..aa4f8a357718b71544654bf419351a867dde4dc8 GIT binary patch literal 3441 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 OudU$!#pTY!p8N;7J~1c& literal 0 HcmV?d00001 diff --git a/lms/static/images/press/csmonitor_logo_178x138.jpg b/lms/static/images/press/csmonitor_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..d389b7a6f11c6eea545de1f0abac49a86c8f0b68 GIT binary patch literal 5130 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 A!vFvP literal 0 HcmV?d00001 diff --git a/lms/static/images/press/insidehighered_logo_178x138.jpg b/lms/static/images/press/insidehighered_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..ad23f11a963b5ad4064b081964dd1a5256fcb38a GIT binary patch literal 5268 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 G8T&VQH1uu& literal 0 HcmV?d00001 diff --git a/lms/static/images/press/radioboston_logo_178x138.jpg b/lms/static/images/press/radioboston_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..e29949a31f5d718da0c097b9727d18956ab5167b GIT binary patch literal 5558 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 Ctr9x` literal 0 HcmV?d00001 diff --git a/lms/static/images/press/time_logo_178x138.jpg b/lms/static/images/press/time_logo_178x138.jpg new file mode 100755 index 0000000000000000000000000000000000000000..5d884fe0fe58b875b9c498dabcfd7d30c2bab884 GIT binary patch literal 4067 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

_zo2qY58Um@o`X6W* zOSc<(-I&KtzeX364g?dn5tb%4s^R5}(K6G~2%4VxWZog6?XSI{0|`AisO!}WrWgNu z%&Al>6dwIIv=aEAw371=t+0R~zeQTM9#To#W$mgOK5fN&!GE{fKTpa!K1ipH*28U* zE``XH?_=?yfZ#~LmOh0|CRAiLAeGE#P-cQrWkcmR6obQN+SM@( zbW?XWPpK9EfPs~(ndZVJ6*z<@^k+$j_=@@9#}JXYl&#(J)EH1x1=b&JT-S<5FTz5z z&aziM8AQ4u=it`U>9D08KzWhhU=JlkcBaK%OA+Pd(cdY#je>@~?OJrdP0as3PURTR zD&s^BYr&%|TDqDFyH>`UsU~e_Y9?^oJ7mc>==x)TOlYB)YgNY=bSx??VwLXL_`W%+ z<3+lNy(}%$G>{WjQT&DYM^Tei1Ss=R|9MZ8-uZZkgc?lpb|DC^lRciI55Z*%mLIq2g6V5JbbgD} z*{>h+e{M7qr(Vr6m*hHKiSYt7Wa$wTFMv zbx7fBn{%p<6X8(y#m6T~<-gm)>Z^B7GPQ0CEI1eB6HSktha(2pL`Fsc%c4w!<#0w$ z1ZO_lrPOXTD|4&&igSSFbJ|}#pA8-`!u1-jNExAq#;4)Z<4}i~CntZfYpfrbc3w`d zG#p7Na``RO@PiN4qh8*KxEFkVKNd}|Q7VB(Ntsj*^&<0{r;v*tshnZq_t;Ls>kES> zyKGR=bW;{O9gV{bH<33|y~~`0H|4N|#)`8;Ci^~cRH_K3gpc{<}6g25bx2>V(M z?mdNvMPLn@s!R}u9#UO4H3f82R>&_WJY)l&ylZ}zJNwE#6U*Pj{lGoeia5+1TPjV0 zM|1VfX5W$X_edOD%~8B9)q8=V6eiC1?v{|5t;5#n)XW1yg`qIa&_$=5!cP#YaItKu eWzWH7OnAzGhFVFN{Cx#=f6?gwlZEYg>c0TM(SDf# literal 0 HcmV?d00001 diff --git a/lms/templates/press.json b/lms/templates/press.json index 16942f06fb..f523f430af 100644 --- a/lms/templates/press.json +++ b/lms/templates/press.json @@ -1,4 +1,187 @@ [ + + { + "title": "Is MIT Giving Away the Farm?", + "url": "http://www.technologyreview.com/mitnews/428698/is-mit-giving-away-the-farm/", + "author": "Larry Hardesty", + "image": "techreview_logo_178x138.jpg", + "deck": "The surprising logic of MIT's free online education program.", + "publication": "Technology Review", + "publish_date": "September/October 2012" + }, + { + "title": "School’s Out, Forever", + "url": "http://www.bostonmagazine.com/articles/2012/08/edx-online-classes-schools-ou
t-forever/", + "author": "Chris Vogel", + "image": "bostonmag_logo_178x138.jpg", + "deck": "A new online education program from Harvard and MIT is poised to transform what it means to go to college.", + "publication": "Boston Magazine", + "publish_date": "September 2012" + }, + { + "title": "Q&A: Anant Agarwal, edX’s president and first professor", + "url": "http://www.bostonmagazine.com/articles/2012/08/edx-online-classes-schools-ou
t-forever/", + "author": " Molly Petrilla ", + "image": "smartplanet_logo_178x138.jpg", + "deck": "", + "publication": "Smart Planet", + "publish_date": "September 3, 2012" + }, + + { + "title": "EdX To Offer Proctored Final Exam For One Course", + "url": "http://www.thecrimson.com/article/2012/9/7/edx-offer-proctored-exams/", + "author": "Samuel Y. Weinstock", + "image": "harvardcrimson_logo_178x138.jpeg", + "deck": "", + "publication": "Harvard Crimson", + "publish_date": "September 7, 2012" + }, + { + "title": "MOOCing On Site", + "url": "http://www.insidehighered.com/news/2012/09/07/site-based-testing-deals-strengthen-case-granting-credit-mooc-students", + "author": "Steve Kolowich ", + "image": "insidehighered_logo_178x138.jpg", + "deck": "", + "publication": "Inside Higher Education", + "publish_date": "September 7, 2012" + }, + { + "title": "edX Curbs the Downfalls of Online Education By Announcing Supervised Final Exams", + "url": "http://bostinno.com/2012/09/07/edx-pearson-proctored-exams/", + "author": "Lauren Landry", + "image": "bostinno_logo_178x138.jpg", + "deck": "", + "publication": "Bostinno", + "publish_date": "September 7, 2012" + }, + { + "title": "Harvard and MIT online courses get 'real world' exams", + "url": "http://www.bbc.co.uk/news/education-19505776", + "author": "Sean Coughlan", + "image": "bbc_logo_178x138.jpeg", + "deck": "", + "publication": "BBC", + "publish_date": "September 6, 2012" + }, + { + "title": "Harvard-MIT Online School EdX to Offer Supervised Final Exams", + "url": "http://www.businessweek.com/news/2012-09-06/harvard-mit-online-school-edx-to-offer-supervised-final-exams", + "author": "Oliver Staley", + "image": "bloomberg_logo_178x138.jpeg", + "deck": "", + "publication": "Bloomberg Business Week", + "publish_date": "September 6, 2012" + }, + { + "title": "Colorado State to Offer Credits for Online Class", + "url": "http://www.nytimes.com/2012/09/07/education/colorado-state-to-offer-credits-for-online-class.html?_r=3", + "author": "Tamar Lewin", + "image": "nyt_logo_178x138.jpeg", + "deck": "", + "publication": "New York Times", + "publish_date": "September 6, 2012" + }, + { + "title": "edX Offers Proctored Exams for Open Online Course", + "url": "http://chronicle.com/blogs/wiredcampus/edx-offers-proctored-exams-for-open-online-course/39656", + "author": " Marc Parry", + "image": "chroniclehighered_logo_178x138.jpeg", + "deck": "", + "publication": "Chronicle of Higher Education", + "publish_date": "September 6, 2012" + }, + { + "title": "edX Offers Proctored Exams for Open Online Course", + "url": "http://itbriefing.net/modules.php?op=modload&name=News&file=article&sid=323229&newlang=eng&topic=15&catid=37", + "author": "", + "image": "itbriefing_logo_178x138.jpg", + "deck": "", + "publication": "ITBriefing.net", + "publish_date": "September 6, 2012" + }, + + { + "title": "Student Loans: Debt for Life", + "url": "http://www.businessweek.com/articles/2012-09-06/student-loans-debt-for-life#p3", + "author": "Peter Coy", + "image": "bloomberg_logo_178x138.jpeg", + "deck": "", + "publication": "Bloomberg Business Week", + "publish_date": "September 6, 2012" + }, + { + "title": "Straighterline wants to help professors expand reach, while students save", + "url": "http://www.baltimoresun.com/business/technology/blog/bs-bz-straighterline-college-professors-20120904,0,6114022.story", + "author": "Gus G. Sentementes", + "image": "baltsun_logo_178x138.jpg", + "deck": "", + "publication": "The Baltimore Sun", + "publish_date": "September 4, 2012" + }, + { + "title": "Want to be a reporter? Learn to code", + "url": "http://gigaom.com/cloud/want-to-be-a-reporter-learn-to-code/", + "author": "Barb Darrow", + "image": "gigaom_logo_178x138.jpeg", + "deck": "", + "publication": "GigaOM", + "publish_date": "September 4, 2012" + }, + { + "title": "MOOC Brigade: Will Massive, Open Online Courses Revolutionize Higher Education?", + "url": "http://nation.time.com/2012/09/04/mooc-brigade-will-massive-open-online-courses-revolutionize-higher-education/", + "author": "Kayla Webley", + "image": "time_logo_178x138.jpg", + "deck": "", + "publication": "Time", + "publish_date": "September 4, 2012" + }, + { + "title": "Ivy walls lower with free online classes from Coursera and edX ", + "url": "http://www.csmonitor.com/Innovation/Pioneers/2012/0903/Ivy-walls-lower-with-free-online-classes-from-Coursera-and-edX", + "author": "Chris Gaylord", + "image": "csmonitor_logo_178x138.jpg", + "deck": "", + "publication": "Christian Science Monitor", + "publish_date": "September 3, 2012" + }, + { + "title": "Summer recap. RLADs, new edX partner, Institute files amicus brief", + "url": "http://tech.mit.edu/V132/N34/summer.html", + "author": "", + "image": "thetech_logo_178x138.jpg", + "deck": "", + "publication": "The Tech", + "publish_date": "September 4, 2012" + }, + { + "title": "Into the Future With MOOC's", + "url": "http://chronicle.com/article/Into-the-Future-With-MOOCs/134080/", + "author": "Kevin Carey", + "image": "chroniclehighered_logo_178x138.jpeg", + "deck": "", + "publication": "The Chronicle of Higher Education", + "publish_date": "September 3, 2012" + }, + { + "title": "The Future Of Higher Education", + "url": "http://radioboston.wbur.org/2012/08/20/higher-education-online", + "author": "", + "image": "radioboston_logo_178x138.jpg", + "deck": "", + "publication": "NPR/Radio Boston", + "publish_date": "August 20, 2012" + }, + { + "title": "Berkeley Joins edX", + "url": "http://www.insidehighered.com/quicktakes/2012/07/24/berkeley-joins-edx", + "author": "Tamar Lewin", + "image": "insidehighered_logo_178x138.jpg", + "deck": "", + "publication": "Inside Higher Ed", + "publish_date": "July 24, 2012" + }, { "title": "Berkeley to Join the Free Online Learning Partnership EdX", "url": "http://www.nytimes.com/2012/07/24/education/berkeley-to-offer-free-online-classes-on-edx.html?_r=1", diff --git a/lms/templates/static_templates/press.html b/lms/templates/static_templates/press.html index 6294b346a9..277cb91bd2 100644 --- a/lms/templates/static_templates/press.html +++ b/lms/templates/static_templates/press.html @@ -37,4 +37,3 @@ % endfor

- From 596d926eacc455815c86f095f2943b311d47d728 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 11:06:01 -0400 Subject: [PATCH 24/26] Added fix so that sequnce nave doesn't go over full screen videos --- common/lib/xmodule/xmodule/css/sequence/display.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index 25d2c26dda..0533465298 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -32,7 +32,7 @@ nav.sequence-nav { .sequence-list-wrapper { position: relative; - z-index: 9999; + z-index: 99; border: 1px solid #ccc; height: 44px; margin: 0 30px; From 2f8a7d0744df7330ea6d526f06cb8c438be74365 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 12:37:19 -0400 Subject: [PATCH 25/26] Removes unused widowfix script --- lms/templates/courseware/courseware.html | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index a433ddc9fc..67719eacc9 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -12,7 +12,6 @@ - ## codemirror From ec9521a9f543c6d03c3e30140b7340a2b90b9c41 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 12 Sep 2012 13:01:10 -0400 Subject: [PATCH 26/26] removed list bullets from tutorials --- lms/static/sass/course/courseware/_courseware.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 863bcad139..0b79ec6a6b 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -116,6 +116,7 @@ div.course-wrapper { margin: 0; @include clearfix(); padding: 0; + list-style: none; li { width: flex-grid(3, 9);