From 9b3d7efb3f60d3b0913c49e01847f9be38892b6b Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 30 Jan 2013 18:14:53 -0500 Subject: [PATCH 01/58] add first pass at testcenter exam --- lms/djangoapps/courseware/views.py | 135 ++++++++++++++++ lms/templates/courseware/testcenter_exam.html | 152 ++++++++++++++++++ lms/templates/main_nonav.html | 46 ++++++ lms/urls.py | 4 + 4 files changed, 337 insertions(+) create mode 100644 lms/templates/courseware/testcenter_exam.html create mode 100644 lms/templates/main_nonav.html diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 5d65d7c632..c7838156cb 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -2,6 +2,7 @@ import logging import urllib from functools import partial +from time import time from django.conf import settings from django.core.context_processors import csrf @@ -297,6 +298,140 @@ def index(request, course_id, chapter=None, section=None, return result +@login_required +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def testcenter_exam(request, course_id, chapter, section): + """ + Displays only associated content. If course, chapter, + and section are all specified, renders the page, or returns an error if they + are invalid. + + Returns an error if these are not all specified and correct. + + Arguments: + + - request : HTTP request + - course_id : course id (str: ORG/course/URL_NAME) + - chapter : chapter url_name (str) + - section : section url_name (str) + + Returns: + + - HTTPresponse + """ + course = get_course_with_access(request.user, course_id, 'load', depth=2) + staff_access = has_access(request.user, course, 'staff') + registered = registered_for_course(course, request.user) + if not registered: + log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url())) + raise # error + try: + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( + course.id, request.user, course, depth=2) + + # Has this student been in this course before? + # first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None + + # Load the module for the course + course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id) + if course_module is None: + log.warning('If you see this, something went wrong: if we got this' + ' far, should have gotten a course module for this user') + # return redirect(reverse('about_course', args=[course.id])) + raise # error + + if chapter is None: + # return redirect_to_course_position(course_module, first_time) + raise # error + + # BW: add this test earlier, and remove later clause + if section is None: + # return redirect_to_course_position(course_module, first_time) + raise # error + + context = { + 'csrf': csrf(request)['csrf_token'], + # 'accordion': render_accordion(request, course, chapter, section), + 'COURSE_TITLE': course.title, + 'course': course, + 'init': '', + 'content': '', + 'staff_access': staff_access, + 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa') + } + + chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter) + if chapter_descriptor is not None: + instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) + save_child_position(course_module, chapter, instance_module) + else: + raise Http404 + + chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter) + if chapter_module is None: + # User may be trying to access a chapter that isn't live yet + raise Http404 + + section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section) + if section_descriptor is None: + # Specifically asked-for section doesn't exist + raise Http404 + + # Load all descendents of the section, because we're going to display its + # html, which in general will need all of its children + section_module = get_module(request.user, request, section_descriptor.location, + student_module_cache, course.id, position=None, depth=None) + if section_module is None: + # User may be trying to be clever and access something + # they don't have access to. + raise Http404 + + # Save where we are in the chapter +# instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) +# save_child_position(chapter_module, section, instance_module) + + + context['content'] = section_module.get_html() + + # figure out when the exam should end. Going forward, this is determined by getting a "normal" + # duration from the test, then doing some math to modify the duration based on accommodations, + # and then use that value as the end. Once we have calculated this, it should be sticky -- we + # use the same value for future requests, unless it's a tester. + + # Let's try 600s for now... + context['end_date'] = (time() + 600) * 1000 + + result = render_to_response('courseware/testcenter_exam.html', context) + except Exception as e: + if isinstance(e, Http404): + # let it propagate + raise + + # In production, don't want to let a 500 out for any reason + if settings.DEBUG: + raise + else: + log.exception("Error in exam view: user={user}, course={course}," + " chapter={chapter} section={section}" + "position={position}".format( + user=request.user, + course=course, + chapter=chapter, + section=section + )) + try: + result = render_to_response('courseware/courseware-error.html', + {'staff_access': staff_access, + 'course' : course}) + except: + # Let the exception propagate, relying on global config to at + # at least return a nice error message + log.exception("Error while rendering courseware-error page") + raise + + return result + @ensure_csrf_cookie def jump_to(request, course_id, location): ''' diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html new file mode 100644 index 0000000000..66adfefcff --- /dev/null +++ b/lms/templates/courseware/testcenter_exam.html @@ -0,0 +1,152 @@ +<%inherit file="/main_nonav.html" /> +<%namespace name='static' file='/static_content.html'/> +<%block name="bodyclass">courseware ${course.css_class} +<%block name="title">${course.number} Exam + +<%block name="headextra"> + <%static:css group='course'/> + <%include file="../discussion/_js_head_dependencies.html" /> + + +<%block name="js_extra"> + + + + ## codemirror + + + ## alternate codemirror + ## + ## + ## + + + <%static:js group='courseware'/> + <%static:js group='discussion'/> + + <%include file="../discussion/_js_body_dependencies.html" /> + % if staff_access: + <%include file="xqa_interface.html"/> + % endif + + + + + + + + + + + + +  + +
+
+
+ ${content} +
+
+
+ + +% if course.show_calculator: +
+ Calculator + +
+
+
+ + +
+ Hints +
+
Suffixes:
+
%kMGTcmunp
+
Operations:
+
^ * / + - ()
+
Functions:
+
sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs
+
Constants
+
e, pi
+ + +
+
+
+ + +
+ +
+
+% endif diff --git a/lms/templates/main_nonav.html b/lms/templates/main_nonav.html new file mode 100644 index 0000000000..f2b87ef348 --- /dev/null +++ b/lms/templates/main_nonav.html @@ -0,0 +1,46 @@ +<%namespace name='static' file='static_content.html'/> + + + + <%block name="title">edX + + + + + <%static:css group='application'/> + + <%static:js group='main_vendor'/> + <%block name="headextra"/> + + + + + + + + % if not course: + <%include file="google_analytics.html" /> + % endif + + + + + + +
+ ${self.body()} + <%block name="bodyextra"/> +
+ + + + <%static:js group='application'/> + <%static:js group='module-js'/> + + <%block name="js_extra"/> + + diff --git a/lms/urls.py b/lms/urls.py index f92b63aac2..2c5db07d00 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -217,6 +217,10 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$', 'courseware.views.course_about', name="about_course"), + # testcenter exam: + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/testcenter_exam/(?P[^/]*)/(?P
[^/]*)/$', + 'courseware.views.testcenter_exam', name="testcenter_exam"), + #Inside the course url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/$', 'courseware.views.course_info', name="course_root"), From 07638440ac93d63893ea2d0b3a67f799bfeb1596 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Thu, 31 Jan 2013 18:33:22 -0500 Subject: [PATCH 02/58] rename testcenter_exam to timed_exam, and read duration from metadata (policy.json) --- lms/djangoapps/courseware/views.py | 17 ++++++++++++----- lms/templates/courseware/testcenter_exam.html | 3 ++- lms/urls.py | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index c7838156cb..1ac7cebd4b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -301,7 +301,7 @@ def index(request, course_id, chapter=None, section=None, @login_required @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -def testcenter_exam(request, course_id, chapter, section): +def timed_exam(request, course_id, chapter, section): """ Displays only associated content. If course, chapter, and section are all specified, renders the page, or returns an error if they @@ -387,20 +387,27 @@ def testcenter_exam(request, course_id, chapter, section): # they don't have access to. raise Http404 - # Save where we are in the chapter + # Save where we are in the chapter NOT! # instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) # save_child_position(chapter_module, section, instance_module) context['content'] = section_module.get_html() - # figure out when the exam should end. Going forward, this is determined by getting a "normal" + # figure out when the timed exam should end. Going forward, this is determined by getting a "normal" # duration from the test, then doing some math to modify the duration based on accommodations, # and then use that value as the end. Once we have calculated this, it should be sticky -- we # use the same value for future requests, unless it's a tester. + + # get value for duration from the section's metadata: + if 'duration' not in section_descriptor.metadata: + raise Http404 + + # for now, assume that the duration is set as an integer value, indicating the number of seconds: + duration = int(section_descriptor.metadata.get('duration')) - # Let's try 600s for now... - context['end_date'] = (time() + 600) * 1000 + # This value should be UTC time as number of milliseconds since epoch. + context['end_date'] = (time() + duration) * 1000 result = render_to_response('courseware/testcenter_exam.html', context) except Exception as e: diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html index 66adfefcff..638778f7cd 100644 --- a/lms/templates/courseware/testcenter_exam.html +++ b/lms/templates/courseware/testcenter_exam.html @@ -67,7 +67,8 @@ return ( num < 10 ? "0" : "" ) + num; } - // set the end time when the template is rendered + // set the end time when the template is rendered. + // This value should be UTC time as number of milliseconds since epoch. var endTime = new Date(${end_date}); var currentTime = new Date(); var remaining_secs = Math.floor((endTime - currentTime)/1000); diff --git a/lms/urls.py b/lms/urls.py index 2c5db07d00..021079333a 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -217,9 +217,9 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$', 'courseware.views.course_about', name="about_course"), - # testcenter exam: - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/testcenter_exam/(?P[^/]*)/(?P
[^/]*)/$', - 'courseware.views.testcenter_exam', name="testcenter_exam"), + # timed exam: + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/timed_exam/(?P[^/]*)/(?P
[^/]*)/$', + 'courseware.views.timed_exam', name="timed_exam"), #Inside the course url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/$', From 9d98b7055de26a4f70f0e987c8f682786564441d Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 1 Feb 2013 17:52:14 -0500 Subject: [PATCH 03/58] add course navigation back into timed exam. Add initial timer styling. --- lms/djangoapps/courseware/views.py | 12 +++++--- lms/static/sass/course.scss | 2 +- lms/static/sass/course/layout/_timer.scss | 11 +++++++ lms/templates/courseware/testcenter_exam.html | 29 +++++++++++++++++-- lms/urls.py | 6 ++++ 5 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 lms/static/sass/course/layout/_timer.scss diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 1ac7cebd4b..47942f3a63 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -352,7 +352,6 @@ def timed_exam(request, course_id, chapter, section): context = { 'csrf': csrf(request)['csrf_token'], - # 'accordion': render_accordion(request, course, chapter, section), 'COURSE_TITLE': course.title, 'course': course, 'init': '', @@ -361,6 +360,11 @@ def timed_exam(request, course_id, chapter, section): 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa') } + # in general, we may want to disable accordion display on timed exams. + provide_accordion = True + if provide_accordion: + context['accordion'] = render_accordion(request, course, chapter, section) + chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter) if chapter_descriptor is not None: instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) @@ -387,9 +391,9 @@ def timed_exam(request, course_id, chapter, section): # they don't have access to. raise Http404 - # Save where we are in the chapter NOT! -# instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) -# save_child_position(chapter_module, section, instance_module) + # Save where we are in the chapter: + instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) + save_child_position(chapter_module, section, instance_module) context['content'] = section_module.get_html() diff --git a/lms/static/sass/course.scss b/lms/static/sass/course.scss index e900e589b2..7c2522a194 100644 --- a/lms/static/sass/course.scss +++ b/lms/static/sass/course.scss @@ -22,7 +22,7 @@ @import 'course/courseware/sidebar'; @import 'course/courseware/amplifier'; @import 'course/layout/calculator'; - +@import 'course/layout/timer'; // course-specific courseware (all styles in these files should be gated by a // course-specific class). This should be replaced with a better way of diff --git a/lms/static/sass/course/layout/_timer.scss b/lms/static/sass/course/layout/_timer.scss new file mode 100644 index 0000000000..01d62d87c7 --- /dev/null +++ b/lms/static/sass/course/layout/_timer.scss @@ -0,0 +1,11 @@ +div.timer-main { + position: fixed; + z-index: 99; + width: 100%; + + div#timer_wrapper { + position: relative; + float: right; + margin-right: 10px; + } +} diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html index 638778f7cd..d2f74ab296 100644 --- a/lms/templates/courseware/testcenter_exam.html +++ b/lms/templates/courseware/testcenter_exam.html @@ -78,6 +78,7 @@ // TBD... if (remaining_secs <= 0) { return "00:00:00"; + // do we just set window.location = value? } // count down in terms of hours, minutes, and seconds: @@ -104,19 +105,41 @@ - +
+
+
Time Remaining:
 
+
+
+ +% if accordion: + <%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> +% endif - 
+
+ +% if accordion: +
+
+ close +
+ + +
+% endif +
${content}
- % if course.show_calculator:
Calculator diff --git a/lms/urls.py b/lms/urls.py index 021079333a..f6819d05a2 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -220,6 +220,12 @@ if settings.COURSEWARE_ENABLED: # timed exam: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/timed_exam/(?P[^/]*)/(?P
[^/]*)/$', 'courseware.views.timed_exam', name="timed_exam"), + # (handle hard-coded 6.002x exam explicitly as a timed exam, but without changing the URL. + # not only because Pearson doesn't want us to change its location, but because we also include it + # in the navigation accordion we display with this exam (so students can see what work they have already + # done). Those are generated automatically using reverse(courseware_section). + url(r'^courses/(?PMITx/6.002x/2012_Fall)/courseware/(?PFinal_Exam)/(?P
Final_Exam_Fall_2012)/$', + 'courseware.views.timed_exam'), #Inside the course url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/$', From ddc8bf2b425f5b9effefc54e1092c2b9d4ba8971 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 4 Feb 2013 04:23:23 +0000 Subject: [PATCH 04/58] add instructor dashboard feature to download profile & problem state --- lms/djangoapps/instructor/views.py | 40 +++++++++++++++++++ .../courseware/instructor_dashboard.html | 15 +++++++ 2 files changed, 55 insertions(+) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index a707506045..5ce256b275 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -383,6 +383,46 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard') + #---------------------------------------- + # DataDump + + elif 'Download CSV of all student profile data' in action: + enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username') + profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', + 'mailing_address', 'goals'] + datatable = {'header': ['username', 'email'] + profkeys} + def getdat(u): + p = u.profile + return [u.username, u.email] + [getattr(p,x,'') for x in profkeys] + + datatable['data'] = [getdat(u) for u in enrolled_students] + datatable['title'] = 'Student profile data for course %s' % course_id + return return_csv('profiledata_%s.csv' % course_id, datatable) + + + elif 'Download CSV of all responses to problem' in action: + problem_to_dump = request.POST.get('problem_to_dump','') + + if problem_to_dump[-4:]==".xml": + problem_to_dump=problem_to_dump[:-4] + try: + (org, course_name, run)=course_id.split("/") + module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump + smdat = StudentModule.objects.filter(course_id=course_id, + module_state_key=module_state_key) + smdat = smdat.order_by('student') + msg+="Found module to reset. " + except Exception as err: + msg+="Couldn't find module with that urlname. " + msg += "
%s
" % escape(err) + smdat = [] + + if smdat: + datatable = {'header': ['username', 'state']} + datatable['data'] = [ [x.student.username, x.state] for x in smdat ] + datatable['title'] = 'Student state for problem %s' % problem_to_dump + return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable) + #---------------------------------------- # Group management diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 7b177c6b6c..a31ee0025e 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -64,6 +64,7 @@ function goto( mode) Admin | Forum Admin | Enrollment | + DataDump | Manage Groups ] @@ -269,6 +270,20 @@ function goto( mode) ##----------------------------------------------------------------------------- +%if modeflag.get('Data'): +
+

+ +

+

Problem urlname: + + +

+
+%endif + +##----------------------------------------------------------------------------- + %if modeflag.get('Manage Groups'): %if instructor_access:
From 1685f302ab7721be80634ae2f68154d74d452cd2 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 4 Feb 2013 02:22:24 -0500 Subject: [PATCH 05/58] add TimerModule to courseware --- .../migrations/0006_add_timed_module.py | 119 ++++++++++++++++++ lms/djangoapps/courseware/models.py | 88 ++++++++++++- lms/djangoapps/courseware/views.py | 50 +++++++- 3 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 lms/djangoapps/courseware/migrations/0006_add_timed_module.py diff --git a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py new file mode 100644 index 0000000000..89b63cf659 --- /dev/null +++ b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'TimedModule' + db.create_table('courseware_timedmodule', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('module_state_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='module_id', db_index=True)), + ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('accommodation_code', self.gf('django.db.models.fields.CharField')(default='NONE', max_length=12, db_index=True)), + ('beginning_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), + ('ending_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), + ('modified_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), + )) + db.send_create_signal('courseware', ['TimedModule']) + + # Adding unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id'] + db.create_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id'] + db.delete_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id']) + + # Deleting model 'TimedModule' + db.delete_table('courseware_timedmodule') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'courseware.offlinecomputedgrade': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'courseware.offlinecomputedgradelog': { + 'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'courseware.studentmodule': { + 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}), + 'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}), + 'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}), + 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'courseware.timedmodule': { + 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'TimedModule'}, + 'accommodation_code': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '12', 'db_index': 'True'}), + 'beginning_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'ending_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['courseware'] \ No newline at end of file diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 21ef8b3d66..bd2da02027 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -12,15 +12,12 @@ file and check it in at the same time as your model changes. To do that, ASSUMPTIONS: modules have unique IDs, even across different module_types """ +from datetime import datetime, timedelta +from calendar import timegm + from django.db import models -#from django.core.cache import cache from django.contrib.auth.models import User -#from cache_toolbox import cache_model, cache_relation - -#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours - - class StudentModule(models.Model): """ Keeps student state for a particular module in a particular course. @@ -214,3 +211,82 @@ class OfflineComputedGradeLog(models.Model): def __unicode__(self): return "[OCGLog] %s: %s" % (self.course_id, self.created) + +class TimedModule(models.Model): + """ + Keeps student state for a timed activity in a particular course. + Includes information about time accommodations granted, + time started, and ending time. + """ + ## These three are the key for the object + + # Key used to share state. By default, this is the module_id, + # but for abtests and the like, this can be set to a shared value + # for many instances of the module. + # Filename for homeworks, etc. + module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id') + student = models.ForeignKey(User, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + class Meta: + unique_together = (('student', 'module_state_key', 'course_id'),) + + # For a timed activity, we are only interested here + # in time-related accommodations, and these should be disjoint. + # (For proctored exams, it is possible to have multiple accommodations + # apply to an exam, so they require accommodating a multi-choice.) + TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'), + ('ADDHALFTIME', 'Extra Time - 1 1/2 Time'), + ('ADD30MIN', 'Extra Time - 30 Minutes'), + ('DOUBLE', 'Extra Time - Double Time'), + ) + accommodation_code = models.CharField(max_length=12, choices=TIME_ACCOMMODATION_CODES, default='NONE', db_index=True) + + def _get_accommodated_duration(self, duration): + ''' + Get duration for activity, as adjusted for accommodations. + Input and output are expressed in seconds. + ''' + if self.accommodation_code == 'NONE': + return duration + elif self.accommodation_code == 'ADDHALFTIME': + # TODO: determine what type to return + return int(duration * 1.5) + elif self.accommodation_code == 'ADD30MIN': + return (duration + (30 * 60)) + elif self.accommodation_code == 'DOUBLE': + return (duration * 2) + + # store state: + + beginning_at = models.DateTimeField(null=True, db_index=True) + ending_at = models.DateTimeField(null=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + modified_at = models.DateTimeField(auto_now=True, db_index=True) + + @property + def has_begun(self): + return self.beginning_at is not None + + @property + def has_ended(self): + if not self.ending_at: + return False + return self.ending_at < datetime.utcnow() + + def begin(self, duration): + ''' + Sets the starting time and ending time for the activity, + based on the duration provided (in seconds). + ''' + self.beginning_at = datetime.utcnow() + modified_duration = self._get_accommodated_duration(duration) + datetime_duration = timedelta(seconds=modified_duration) + self.ending_at = self.beginning_at + datetime_duration + + def get_end_time_in_ms(self): + return (timegm(self.ending_at.timetuple()) * 1000) + + def __unicode__(self): + return '/'.join([self.course_id, self.student.username, self.module_state_key]) + diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 47942f3a63..b44887dbfd 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -2,7 +2,6 @@ import logging import urllib from functools import partial -from time import time from django.conf import settings from django.core.context_processors import csrf @@ -21,7 +20,7 @@ from courseware.access import has_access from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university, sort_by_announcement) import courseware.tabs as tabs -from courseware.models import StudentModuleCache +from courseware.models import StudentModuleCache, TimedModule from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor from django_comment_client.utils import get_discussion_title @@ -402,16 +401,55 @@ def timed_exam(request, course_id, chapter, section): # duration from the test, then doing some math to modify the duration based on accommodations, # and then use that value as the end. Once we have calculated this, it should be sticky -- we # use the same value for future requests, unless it's a tester. - + # get value for duration from the section's metadata: + # for now, assume that the duration is set as an integer value, indicating the number of seconds: if 'duration' not in section_descriptor.metadata: raise Http404 - - # for now, assume that the duration is set as an integer value, indicating the number of seconds: duration = int(section_descriptor.metadata.get('duration')) + # get corresponding time module, if one is present: + # TODO: determine what to use for module_key... + try: + timed_module = TimedModule.objects.get(student=request.user, course_id=course_id) + + # if a module exists, check to see if it has already been started, + # and if it has already ended. + if timed_module.has_ended: + # the exam has already ended, and the student has tried to + # revisit the exam. + # TODO: determine what do we do here. + # For a Pearson exam, we want to go to the exit page. + # (Not so sure what to do in general.) + # Proposal: store URL in the section descriptor, + # along with the duration. If no such URL is set, + # just put up the error page, + raise Exception("Time expired on {}".format(timed_module)) + elif not timed_module.has_begun: + # user has not started the exam, but may have an accommodation + # that has been granted to them. + # modified_duration = timed_module.get_accommodated_duration(duration) + # timed_module.started_at = datetime.utcnow() # time() * 1000 + # timed_module.end_date = timed_module. + timed_module.begin(duration) + timed_module.save() + + except TimedModule.DoesNotExist: + # no entry found. So we're starting this test + # without any accommodations being preset. + # TODO: determine what to use for module_key... + timed_module = TimedModule(student=request.user, course_id=course_id) + timed_module.begin(duration) + timed_module.save() + + + # the exam has already been started, and the student is returning to the + # exam page. Fetch the end time (in GMT) as stored + # in the module when it was started. + end_date = timed_module.get_end_time_in_ms() + # This value should be UTC time as number of milliseconds since epoch. - context['end_date'] = (time() + duration) * 1000 + context['end_date'] = end_date result = render_to_response('courseware/testcenter_exam.html', context) except Exception as e: From f8b7d5fad6205fe4eead190538b82df8585ceb98 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 4 Feb 2013 15:13:14 -0500 Subject: [PATCH 06/58] have timer redirect when it expires --- lms/djangoapps/courseware/views.py | 16 ++++-- lms/templates/courseware/testcenter_exam.html | 53 ++++++++++--------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index b44887dbfd..2484aa5c6b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -8,7 +8,7 @@ from django.core.context_processors import csrf from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required -from django.http import Http404 +from django.http import Http404, HttpResponseRedirect from django.shortcuts import redirect from mitxmako.shortcuts import render_to_response, render_to_string #from django.views.decorators.csrf import ensure_csrf_cookie @@ -397,6 +397,12 @@ def timed_exam(request, course_id, chapter, section): context['content'] = section_module.get_html() + # determine where to go when the exam ends: + if 'time_expired_redirect_url' not in section_descriptor.metadata: + raise Http404 + time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url') + context['time_expired_redirect_url'] = time_expired_redirect_url + # figure out when the timed exam should end. Going forward, this is determined by getting a "normal" # duration from the test, then doing some math to modify the duration based on accommodations, # and then use that value as the end. Once we have calculated this, it should be sticky -- we @@ -407,7 +413,7 @@ def timed_exam(request, course_id, chapter, section): if 'duration' not in section_descriptor.metadata: raise Http404 duration = int(section_descriptor.metadata.get('duration')) - + # get corresponding time module, if one is present: # TODO: determine what to use for module_key... try: @@ -424,7 +430,11 @@ def timed_exam(request, course_id, chapter, section): # Proposal: store URL in the section descriptor, # along with the duration. If no such URL is set, # just put up the error page, - raise Exception("Time expired on {}".format(timed_module)) + if time_expired_redirect_url is None: + raise Exception("Time expired on {}".format(timed_module)) + else: + return HttpResponseRedirect(time_expired_redirect_url) + elif not timed_module.has_begun: # user has not started the exam, but may have an accommodation # that has been granted to them. diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html index d2f74ab296..8082200146 100644 --- a/lms/templates/courseware/testcenter_exam.html +++ b/lms/templates/courseware/testcenter_exam.html @@ -61,26 +61,21 @@ From 1923ae0d6b08aa094ba1e464a4ead26b1b782293 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Mon, 4 Feb 2013 15:40:01 -0500 Subject: [PATCH 07/58] pearson - added in timer styling for IE7 --- lms/static/sass/course/layout/_timer.scss | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lms/static/sass/course/layout/_timer.scss b/lms/static/sass/course/layout/_timer.scss index 01d62d87c7..eef21b8c27 100644 --- a/lms/static/sass/course/layout/_timer.scss +++ b/lms/static/sass/course/layout/_timer.scss @@ -2,10 +2,26 @@ div.timer-main { position: fixed; z-index: 99; width: 100%; + border-top: 2px solid #000; div#timer_wrapper { position: relative; + top: -3px; float: right; margin-right: 10px; + background: #000; + color: #fff; + padding: 10px 20px; + border-radius: 3px; + } + + .timer_label { + color: #ccc; + font-size: 13px; + } + + #exam_timer { + font-weight: bold; + font-size: 15px; } } From 1b465d1beb0fe744199dc8c7d222563e3199a2e3 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 5 Feb 2013 18:01:55 -0500 Subject: [PATCH 08/58] implement testcenter_login --- common/djangoapps/student/views.py | 130 ++++++++++++++++++++++++++-- lms/djangoapps/courseware/models.py | 4 + lms/djangoapps/courseware/views.py | 6 +- 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b583599e97..9312f7b76a 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1,12 +1,10 @@ import datetime import feedparser -#import itertools import json import logging import random import string import sys -#import time import urllib import uuid @@ -18,10 +16,13 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.core.context_processors import csrf from django.core.mail import send_mail +from django.core.urlresolvers import reverse from django.core.validators import validate_email, validate_slug, ValidationError from django.db import IntegrityError -from django.http import HttpResponse, HttpResponseForbidden, Http404 +from django.http import HttpResponse, HttpResponseForbidden, Http404,\ + HttpResponseRedirect from django.shortcuts import redirect + from mitxmako.shortcuts import render_to_response, render_to_string from bs4 import BeautifulSoup from django.core.cache import cache @@ -39,11 +40,11 @@ from xmodule.course_module import CourseDescriptor from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore -#from datetime import date from collections import namedtuple from courseware.courses import get_courses, sort_by_announcement from courseware.access import has_access +from courseware.models import TimedModule from statsd import statsd @@ -1058,7 +1059,7 @@ def accept_name_change(request): # TODO: This is a giant kludge to give Pearson something to test against ASAP. # Will need to get replaced by something that actually ties into TestCenterUser @csrf_exempt -def test_center_login(request): +def atest_center_login(request): if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'): raise Http404 @@ -1076,6 +1077,125 @@ def test_center_login(request): return HttpResponseForbidden() +@csrf_exempt +def test_center_login(request): + # errors are returned by navigating to the error_url, adding a query parameter named "code" + # which contains the error code describing the exceptional condition. + def makeErrorURL(error_url, error_code): + return "{}&code={}".format(error_url, error_code); + + # get provided error URL, which will be used as a known prefix for returning error messages to the + # Pearson shell. It does not have a trailing slash, so we need to add one when creating output URLs. + error_url = request.POST.get("errorURL") + + # check that the parameters have not been tampered with, by comparing the code provided by Pearson + # with the code we calculate for the same parameters. + if 'code' not in request.POST: + return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode")); + code = request.POST.get("code") + + # calculate SHA for query string + # TODO: figure out how to get the original query string, so we can hash it and compare. + + + if 'clientCandidateID' not in request.POST: + return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID")); + client_candidate_id = request.POST.get("clientCandidateID") + + # TODO: check remaining parameters, and maybe at least log if they're not matching + # expected values.... + # registration_id = request.POST.get("registrationID") + # exit_url = request.POST.get("exitURL") + + + # find testcenter_user that matches the provided ID: + try: + testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id) + except TestCenterUser.DoesNotExist: + return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")); + + + # find testcenter_registration that matches the provided exam code: + # Note that we could rely on either the registrationId or the exam code, + # or possibly both. + if 'vueExamSeriesCode' not in request.POST: + # TODO: confirm this error code (made up, not in documentation) + return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode")); + exam_series_code = request.POST.get('vueExamSeriesCode') + + registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code) + + if not registrations: + return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned")); + + # TODO: figure out what to do if there are more than one registrations.... + # for now, just take the first... + registration = registrations[0] + course_id = registration.course_id + + # if we want to look up whether the test has already been taken, or to + # communicate that a time accommodation needs to be applied, we need to + # know the module_id to use that corresponds to the particular exam_series_code. + # For now, we can hardcode that... + if exam_series_code == '6002x001': + chapter_url_name = 'Final_Exam' + section_url_name = 'Final_Exam_Fall_2012' + redirect_url = reverse('courseware_section', args=[course_id, chapter_url_name, section_url_name]) + location = 'i4x://MITx/6.002x/2012_Fall/sequence/Final_Exam_Fall_2012' + else: + # TODO: clarify if this is the right error code for this condition. + return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")); + + + time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME', + 'ET30MN' : 'ADD30MIN', + 'ETDBTM' : 'ADDDOUBLE', } + + # check if the test has already been taken + timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, module_state_key=location) + if timed_modules: + timed_module = timed_modules[0] + if timed_module.has_ended: + return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken")); + elif registration.get_accommodation_codes(): + # we don't have a timed module created yet, so if we have time accommodations + # to implement, create an entry now: + time_accommodation_code = None + for code in registration.get_accommodation_codes(): + if code in time_accommodation_mapping: + time_accommodation_code = time_accommodation_mapping[code] + if client_candidate_id == "edX003671291147": + time_accommodation_code = 'TESTING' + if time_accommodation_code: + timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=location) + timed_module.accommodation_code = time_accommodation_code + timed_module.save() + + # Now log the user in: +# user = authenticate(username=testcenteruser.user.username, +# password=testcenteruser.user.password) +# +# if user is None: +# # argh. We couldn't login! +# return HttpResponseRedirect(makeErrorURL(error_url, "ARGH! User cannot log in")); + + # UGLY HACK!!! + # Login assumes that authentication has occurred, and that there is a + # backend annotation on the user object, indicating which backend + # against which the user was authenticated. We're authenticating here + # against the registration entry, and assuming that the request given + # this information is correct, we allow the user to be logged in + # without a password. This could all be formalized in a backend object + # that does the above checking. + # TODO: create a backend class to do this. + # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) + testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass") + login(request, testcenteruser.user) + + # And start the test: + return redirect(redirect_url) + + def _get_news(top=None): "Return the n top news items on settings.RSS_URL" diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index bd2da02027..00079d30f2 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -239,6 +239,7 @@ class TimedModule(models.Model): ('ADDHALFTIME', 'Extra Time - 1 1/2 Time'), ('ADD30MIN', 'Extra Time - 30 Minutes'), ('DOUBLE', 'Extra Time - Double Time'), + ('TESTING', 'Extra Time -- Large amount for testing purposes') ) accommodation_code = models.CharField(max_length=12, choices=TIME_ACCOMMODATION_CODES, default='NONE', db_index=True) @@ -256,6 +257,9 @@ class TimedModule(models.Model): return (duration + (30 * 60)) elif self.accommodation_code == 'DOUBLE': return (duration * 2) + elif self.accommodation_code == 'TESTING': + # when testing, set timer to run for a week at a time. + return 3600 * 24 * 7 # store state: diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 2484aa5c6b..5afda7b181 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -415,9 +415,8 @@ def timed_exam(request, course_id, chapter, section): duration = int(section_descriptor.metadata.get('duration')) # get corresponding time module, if one is present: - # TODO: determine what to use for module_key... try: - timed_module = TimedModule.objects.get(student=request.user, course_id=course_id) + timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, module_state_key=section_module.id) # if a module exists, check to see if it has already been started, # and if it has already ended. @@ -447,8 +446,7 @@ def timed_exam(request, course_id, chapter, section): except TimedModule.DoesNotExist: # no entry found. So we're starting this test # without any accommodations being preset. - # TODO: determine what to use for module_key... - timed_module = TimedModule(student=request.user, course_id=course_id) + timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=section_module.id) timed_module.begin(duration) timed_module.save() From 6ed2737c36b4405169569c0eaedbf510508dbafe Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 6 Feb 2013 13:29:48 -0500 Subject: [PATCH 09/58] make LMS forum subsystem more robust in case of orphaned discussion modules. Given our draft/non-draft duality, we don't currently have a means to always do proper housekeeping at this point in time. However, we have to stop the LMS Forums from blowing up when encoutering one of these. --- lms/djangoapps/django_comment_client/utils.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index b58e3b30e6..40fd106b40 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -11,6 +11,8 @@ from django.http import HttpResponse from django.utils import simplejson from django_comment_client.models import Role from django_comment_client.permissions import check_permissions_by_view +from xmodule.modulestore.exceptions import NoPathToItem + from mitxmako import middleware import pystache_custom as pystache @@ -158,6 +160,14 @@ def initialize_discussion_info(course): log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) skip_module = True + # cdodge: pre-compute the path_to_location. Note this can throw an exception for any + # dangling discussion modules + try: + _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_location(modulestore(), course.id, module.location) + except NoPathToItem: + log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location)) + skip_module = True + if skip_module: continue @@ -360,7 +370,13 @@ def get_courseware_context(content, course): if id in id_map: location = id_map[id]["location"].url() title = id_map[id]["title"] - (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) + + # cdodge: did we pre-compute, if so, then let's use that rather than recomputing + if 'path_to_location' in _DISCUSSIONINFO[course.id]: + (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'] + else: + (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) + url = reverse('courseware_position', kwargs={"course_id":course_id, "chapter":chapter, "section":section, From 114d800c6a6a6650c49c4cddee40d7866c5e442e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 6 Feb 2013 15:17:47 -0500 Subject: [PATCH 10/58] need to make path_to_location a dictionary since it needs to be keyed by the location --- lms/djangoapps/django_comment_client/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 0940d065f4..151bde3dd5 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -166,6 +166,7 @@ def initialize_discussion_info(course): # get all discussion models within this course_id all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id) + path_to_locations = {} for module in all_modules: skip_module = False for key in ('id', 'discussion_category', 'for'): @@ -176,7 +177,7 @@ def initialize_discussion_info(course): # cdodge: pre-compute the path_to_location. Note this can throw an exception for any # dangling discussion modules try: - _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_location(modulestore(), course.id, module.location) + path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location) except NoPathToItem: log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location)) skip_module = True @@ -245,6 +246,7 @@ def initialize_discussion_info(course): _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['category_map'] = category_map _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() + _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations class JsonResponse(HttpResponse): @@ -402,8 +404,8 @@ def get_courseware_context(content, course): title = id_map[id]["title"] # cdodge: did we pre-compute, if so, then let's use that rather than recomputing - if 'path_to_location' in _DISCUSSIONINFO[course.id]: - (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'] + if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']: + (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location] else: (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) From bfc452759013a443915396c3df1f34e5e9e423f1 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 6 Feb 2013 15:23:11 -0500 Subject: [PATCH 11/58] Switch timed_module to store location, and use to navigate from timer when timer displays on non-exam course pages. --- common/djangoapps/student/views.py | 21 ++---- .../migrations/0006_add_timed_module.py | 14 ++-- lms/djangoapps/courseware/models.py | 6 +- lms/djangoapps/courseware/views.py | 50 ++++++++++++-- lms/templates/courseware/courseware.html | 66 ++++++++++++++++++- lms/templates/main.html | 5 ++ 6 files changed, 133 insertions(+), 29 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 9312f7b76a..61313376d1 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1138,10 +1138,11 @@ def test_center_login(request): # know the module_id to use that corresponds to the particular exam_series_code. # For now, we can hardcode that... if exam_series_code == '6002x001': - chapter_url_name = 'Final_Exam' - section_url_name = 'Final_Exam_Fall_2012' - redirect_url = reverse('courseware_section', args=[course_id, chapter_url_name, section_url_name]) - location = 'i4x://MITx/6.002x/2012_Fall/sequence/Final_Exam_Fall_2012' + # This should not be hardcoded here, but should be added to the exam definition. + # TODO: look the location up in the course, by finding the exam_info with the matching code, + # and get the location from that. + location = 'i4x://MITx/6.002x/sequential/Final_Exam_Fall_2012' + redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location}) else: # TODO: clarify if this is the right error code for this condition. return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")); @@ -1152,7 +1153,7 @@ def test_center_login(request): 'ETDBTM' : 'ADDDOUBLE', } # check if the test has already been taken - timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, module_state_key=location) + timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, location=location) if timed_modules: timed_module = timed_modules[0] if timed_module.has_ended: @@ -1167,17 +1168,9 @@ def test_center_login(request): if client_candidate_id == "edX003671291147": time_accommodation_code = 'TESTING' if time_accommodation_code: - timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=location) + timed_module = TimedModule(student=request.user, course_id=course_id, location=location) timed_module.accommodation_code = time_accommodation_code timed_module.save() - - # Now log the user in: -# user = authenticate(username=testcenteruser.user.username, -# password=testcenteruser.user.password) -# -# if user is None: -# # argh. We couldn't login! -# return HttpResponseRedirect(makeErrorURL(error_url, "ARGH! User cannot log in")); # UGLY HACK!!! # Login assumes that authentication has occurred, and that there is a diff --git a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py index 89b63cf659..6e8791a975 100644 --- a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py +++ b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py @@ -11,7 +11,7 @@ class Migration(SchemaMigration): # Adding model 'TimedModule' db.create_table('courseware_timedmodule', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('module_state_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='module_id', db_index=True)), + ('location', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='location', db_index=True)), ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), ('accommodation_code', self.gf('django.db.models.fields.CharField')(default='NONE', max_length=12, db_index=True)), @@ -22,13 +22,13 @@ class Migration(SchemaMigration): )) db.send_create_signal('courseware', ['TimedModule']) - # Adding unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id'] - db.create_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id']) + # Adding unique constraint on 'TimedModule', fields ['student', 'location', 'course_id'] + db.create_unique('courseware_timedmodule', ['student_id', 'location', 'course_id']) def backwards(self, orm): - # Removing unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id'] - db.delete_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id']) + # Removing unique constraint on 'TimedModule', fields ['student', 'location', 'course_id'] + db.delete_unique('courseware_timedmodule', ['student_id', 'location', 'course_id']) # Deleting model 'TimedModule' db.delete_table('courseware_timedmodule') @@ -103,15 +103,15 @@ class Migration(SchemaMigration): 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) }, 'courseware.timedmodule': { - 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'TimedModule'}, + 'Meta': {'unique_together': "(('student', 'location', 'course_id'),)", 'object_name': 'TimedModule'}, 'accommodation_code': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '12', 'db_index': 'True'}), 'beginning_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), 'ending_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'location'", 'db_index': 'True'}), 'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), - 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}), 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) } } diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 00079d30f2..d9cc560215 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -224,12 +224,14 @@ class TimedModule(models.Model): # but for abtests and the like, this can be set to a shared value # for many instances of the module. # Filename for homeworks, etc. - module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id') + # module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id') + location = models.CharField(max_length=255, db_index=True, db_column='location') student = models.ForeignKey(User, db_index=True) course_id = models.CharField(max_length=255, db_index=True) class Meta: - unique_together = (('student', 'module_state_key', 'course_id'),) +# unique_together = (('student', 'module_state_key', 'course_id'),) + unique_together = (('student', 'location', 'course_id'),) # For a timed activity, we are only interested here # in time-related accommodations, and these should be disjoint. diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 5afda7b181..0acf435f0b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -215,6 +215,43 @@ def index(request, course_id, chapter=None, section=None, 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa') } + # check here if this page is within a course that has an active timed module running. If so, then + # display the appropriate timer information: + timed_modules = TimedModule.objects.filter(student=request.user, course_id=course_id) + if timed_modules: + for timed_module in timed_modules: + if timed_module.has_begun and not timed_module.has_ended: + # a timed module has been found that is active, so display + # the relevant time: + # module_state_key = timed_module.module_state_key + location = timed_module.location + + # when we actually make the state be stored in the StudentModule, then + # we can fetch what we need from that. + # student_module = student_module_cache.lookup(course_id, 'sequential', module_state_key) + # But the module doesn't give us anything helpful to find the corresponding descriptor + + # get the corresponding section_descriptor for this timed_module entry: + section_descriptor = modulestore().get_instance(course_id, Location(location)) + + # determine where to go when the timer expires: + # Note that if we could get this from the timed_module, we wouldn't have to + # fetch the section_descriptor in the first place. + if 'time_expired_redirect_url' not in section_descriptor.metadata: + raise Http404 + time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url') + context['time_expired_redirect_url'] = time_expired_redirect_url + + # Fetch the end time (in GMT) as stored in the module when it was started. + # This value should be UTC time as number of milliseconds since epoch. + end_date = timed_module.get_end_time_in_ms() + context['timer_expiration_datetime'] = end_date + if 'suppress_toplevel_navigation' in section_descriptor.metadata: + context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation'] + return_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location}) + context['timer_navigation_return_url'] = return_url + + chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter) if chapter_descriptor is not None: instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) @@ -416,7 +453,7 @@ def timed_exam(request, course_id, chapter, section): # get corresponding time module, if one is present: try: - timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, module_state_key=section_module.id) + timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, location=section_module.location) # if a module exists, check to see if it has already been started, # and if it has already ended. @@ -446,7 +483,7 @@ def timed_exam(request, course_id, chapter, section): except TimedModule.DoesNotExist: # no entry found. So we're starting this test # without any accommodations being preset. - timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=section_module.id) + timed_module = TimedModule(student=request.user, course_id=course_id, location=section_module.location) timed_module.begin(duration) timed_module.save() @@ -457,9 +494,12 @@ def timed_exam(request, course_id, chapter, section): end_date = timed_module.get_end_time_in_ms() # This value should be UTC time as number of milliseconds since epoch. - context['end_date'] = end_date - - result = render_to_response('courseware/testcenter_exam.html', context) + # context['end_date'] = end_date + context['timer_expiration_datetime'] = end_date + if 'suppress_toplevel_navigation' in section_descriptor.metadata: + context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation'] + + result = render_to_response('courseware/courseware.html', context) except Exception as e: if isinstance(e, Http404): # let it propagate diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 1ea3df1b5a..da429c5275 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -59,12 +59,75 @@ }); }); + +% if timer_expiration_datetime: + +% endif + -<%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> +% if timer_expiration_datetime: +
+
+
Time Remaining:
 
+ % if timer_navigation_return_url: + Return... + % endif +
+
+% endif + +% if accordion: + <%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> +% endif
+ +% if accordion:
close @@ -76,6 +139,7 @@
+% endif
${content} diff --git a/lms/templates/main.html b/lms/templates/main.html index 5d3fd29104..42d5a71228 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -29,13 +29,18 @@ +% if not suppress_toplevel_navigation: <%include file="navigation.html" /> +% endif +
${self.body()} <%block name="bodyextra"/>
+% if not suppress_toplevel_navigation: <%include file="footer.html" /> +% endif <%static:js group='application'/> <%static:js group='module-js'/> From aae5c7ca23378e47584e91c65f078892e733cac3 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 6 Feb 2013 15:25:25 -0500 Subject: [PATCH 12/58] remove unneeded files --- lms/templates/courseware/testcenter_exam.html | 179 ------------------ lms/templates/main_nonav.html | 46 ----- 2 files changed, 225 deletions(-) delete mode 100644 lms/templates/courseware/testcenter_exam.html delete mode 100644 lms/templates/main_nonav.html diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html deleted file mode 100644 index 8082200146..0000000000 --- a/lms/templates/courseware/testcenter_exam.html +++ /dev/null @@ -1,179 +0,0 @@ -<%inherit file="/main_nonav.html" /> -<%namespace name='static' file='/static_content.html'/> -<%block name="bodyclass">courseware ${course.css_class} -<%block name="title">${course.number} Exam - -<%block name="headextra"> - <%static:css group='course'/> - <%include file="../discussion/_js_head_dependencies.html" /> - - -<%block name="js_extra"> - - - - ## codemirror - - - ## alternate codemirror - ## - ## - ## - - - <%static:js group='courseware'/> - <%static:js group='discussion'/> - - <%include file="../discussion/_js_body_dependencies.html" /> - % if staff_access: - <%include file="xqa_interface.html"/> - % endif - - - - - - - - - - -
-
-
Time Remaining:
 
-
-
- -% if accordion: - <%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> -% endif - - -
- -
- -% if accordion: -
-
- close -
- - -
-% endif - -
- ${content} -
-
-
- -% if course.show_calculator: -
- Calculator - -
-
-
- - -
- Hints -
-
Suffixes:
-
%kMGTcmunp
-
Operations:
-
^ * / + - ()
-
Functions:
-
sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs
-
Constants
-
e, pi
- - -
-
-
- - -
- -
-
-% endif diff --git a/lms/templates/main_nonav.html b/lms/templates/main_nonav.html deleted file mode 100644 index f2b87ef348..0000000000 --- a/lms/templates/main_nonav.html +++ /dev/null @@ -1,46 +0,0 @@ -<%namespace name='static' file='static_content.html'/> - - - - <%block name="title">edX - - - - - <%static:css group='application'/> - - <%static:js group='main_vendor'/> - <%block name="headextra"/> - - - - - - - - % if not course: - <%include file="google_analytics.html" /> - % endif - - - - - - -
- ${self.body()} - <%block name="bodyextra"/> -
- - - - <%static:js group='application'/> - <%static:js group='module-js'/> - - <%block name="js_extra"/> - - From a770e34bec6b339776f64f46ad09faf8659d48e6 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 6 Feb 2013 20:36:44 -0500 Subject: [PATCH 13/58] Adding multiple-choice loncapa integration. --- common/lib/capa/capa/responsetypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 78c986a963..1ecba36d50 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -632,8 +632,10 @@ class MultipleChoiceResponse(LoncapaResponse): # define correct choices (after calling secondary setup) xml = self.xml - cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) - self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml] + cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id')) + self.correct_choices = [contextualize_text(choice.get('name'), + self.context) for choice in cxml if + contextualize_text(choice.get('correct'), self.context) == "true"] def mc_setup_response(self): ''' From bf17341fdb7a7fd5f72a0d32adb2ed7fd20f8ef2 Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 6 Feb 2013 23:16:44 -0500 Subject: [PATCH 14/58] add select_related("profile") --- lms/djangoapps/instructor/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 5ce256b275..ae4ac05167 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -387,7 +387,7 @@ def instructor_dashboard(request, course_id): # DataDump elif 'Download CSV of all student profile data' in action: - enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username') + enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username').select_related("profile") profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', 'goals'] datatable = {'header': ['username', 'email'] + profkeys} From 33a5a5fd9fa3a84773316ced8ea8fc1a3d522f01 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Thu, 7 Feb 2013 05:17:56 -0500 Subject: [PATCH 15/58] add implementation for fixed_time_module --- common/lib/xmodule/setup.py | 1 + .../lib/xmodule/xmodule/fixed_time_module.py | 180 ++++++++++++++++++ common/lib/xmodule/xmodule/modulestore/xml.py | 2 +- 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/xmodule/fixed_time_module.py diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 29227c3188..f61e6f6f36 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -23,6 +23,7 @@ setup( "course = xmodule.course_module:CourseDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor", "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor", + "fixedtime = xmodule.fixed_time_module:FixedTimeDescriptor", "html = xmodule.html_module:HtmlDescriptor", "image = xmodule.backcompat_module:TranslateCustomTagDescriptor", "error = xmodule.error_module:ErrorDescriptor", diff --git a/common/lib/xmodule/xmodule/fixed_time_module.py b/common/lib/xmodule/xmodule/fixed_time_module.py new file mode 100644 index 0000000000..f1fec26dc3 --- /dev/null +++ b/common/lib/xmodule/xmodule/fixed_time_module.py @@ -0,0 +1,180 @@ +import json +import logging + +from lxml import etree +from time import time + +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.x_module import XModule +from xmodule.progress import Progress +from xmodule.exceptions import NotFoundError +from pkg_resources import resource_string + + +log = logging.getLogger(__name__) + +# HACK: This shouldn't be hard-coded to two types +# OBSOLETE: This obsoletes 'type' +# class_priority = ['video', 'problem'] + + +class FixedTimeModule(XModule): + ''' + Wrapper module which imposes a time constraint for the completion of its child. + ''' + + def __init__(self, system, location, definition, descriptor, instance_state=None, + shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + + # NOTE: Position is 1-indexed. This is silly, but there are now student + # positions saved on prod, so it's not easy to fix. +# self.position = 1 + self.beginning_at = None + self.ending_at = None + self.accommodation_code = None + + if instance_state is not None: + state = json.loads(instance_state) + + if 'beginning_at' in state: + self.beginning_at = state['beginning_at'] + if 'ending_at' in state: + self.ending_at = state['ending_at'] + if 'accommodation_code' in state: + self.accommodation_code = state['accommodation_code'] + + + # if position is specified in system, then use that instead +# if system.get('position'): +# self.position = int(system.get('position')) + + self.rendered = False + + # For a timed activity, we are only interested here + # in time-related accommodations, and these should be disjoint. + # (For proctored exams, it is possible to have multiple accommodations + # apply to an exam, so they require accommodating a multi-choice.) + TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'), + ('ADDHALFTIME', 'Extra Time - 1 1/2 Time'), + ('ADD30MIN', 'Extra Time - 30 Minutes'), + ('DOUBLE', 'Extra Time - Double Time'), + ('TESTING', 'Extra Time -- Large amount for testing purposes') + ) + + def _get_accommodated_duration(self, duration): + ''' + Get duration for activity, as adjusted for accommodations. + Input and output are expressed in seconds. + ''' + if self.accommodation_code is None or self.accommodation_code == 'NONE': + return duration + elif self.accommodation_code == 'ADDHALFTIME': + # TODO: determine what type to return + return int(duration * 1.5) + elif self.accommodation_code == 'ADD30MIN': + return (duration + (30 * 60)) + elif self.accommodation_code == 'DOUBLE': + return (duration * 2) + elif self.accommodation_code == 'TESTING': + # when testing, set timer to run for a week at a time. + return 3600 * 24 * 7 + + # store state: + + @property + def has_begun(self): + return self.beginning_at is not None + + @property + def has_ended(self): + if not self.ending_at: + return False + return self.ending_at < time() + + def begin(self, duration): + ''' + Sets the starting time and ending time for the activity, + based on the duration provided (in seconds). + ''' + self.beginning_at = time() + modified_duration = self._get_accommodated_duration(duration) + # datetime_duration = timedelta(seconds=modified_duration) + # self.ending_at = self.beginning_at + datetime_duration + self.ending_at = self.beginning_at + modified_duration + + def get_end_time_in_ms(self): + return int(self.ending_at * 1000) + + def get_instance_state(self): + state = {} + if self.beginning_at: + state['beginning_at'] = self.beginning_at + if self.ending_at: + state['ending_at'] = self.ending_at + if self.accommodation_code: + state['accommodation_code'] = self.accommodation_code + return json.dumps(state) + + def get_html(self): + self.render() + return self.content + + def get_progress(self): + ''' Return the total progress, adding total done and total available. + (assumes that each submodule uses the same "units" for progress.) + ''' + # TODO: Cache progress or children array? + children = self.get_children() + progresses = [child.get_progress() for child in children] + progress = reduce(Progress.add_counts, progresses) + return progress + + def handle_ajax(self, dispatch, get): # TODO: bounds checking +# ''' get = request.POST instance ''' +# if dispatch == 'goto_position': +# self.position = int(get['position']) +# return json.dumps({'success': True}) + raise NotFoundError('Unexpected dispatch type') + + def render(self): + if self.rendered: + return + # assumes there is one and only one child, so it only renders the first child + child = self.get_display_items()[0] + self.content = child.get_html() + self.rendered = True + + def get_icon_class(self): + return self.get_children()[0].get_icon_class() + + +class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor): + # TODO: fix this template?! + mako_template = 'widgets/sequence-edit.html' + module_class = FixedTimeModule + + stores_state = True # For remembering when a student started, and when they should end + + @classmethod + def definition_from_xml(cls, xml_object, system): + children = [] + for child in xml_object: + try: + children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url()) + except Exception as e: + log.exception("Unable to load child when parsing FixedTime wrapper. Continuing...") + if system.error_tracker is not None: + system.error_tracker("ERROR: " + str(e)) + continue + return {'children': children} + + def definition_to_xml(self, resource_fs): + xml_object = etree.Element('fixedtime') + for child in self.get_children(): + xml_object.append( + etree.fromstring(child.export_to_xml(resource_fs))) + return xml_object + diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 17d6f04932..332b1b1898 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -73,7 +73,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): # VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check) # tags that really need unique names--they store (or should store) state. - need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence') + need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'fixedtime') attr = xml_data.attrib tag = xml_data.tag From 2b637fe5110996febeff0982e5c10ebc7159e4ce Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Feb 2013 10:13:53 -0500 Subject: [PATCH 16/58] Fix loads of local static files when running in dev mode --- common/djangoapps/static_replace/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index bf27f5b38d..d41688530e 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -77,9 +77,13 @@ def replace_static_urls(text, data_directory, course_namespace=None): # course_namespace is not None, then use studio style urls if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): url = StaticContent.convert_legacy_static_url(rest, course_namespace) + # In debug mode, if we can find the url as is, + elif settings.DEBUG and finders.find(rest, True): + return original # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: course_path = "/".join((data_directory, rest)) + try: if staticfiles_storage.exists(rest): url = staticfiles_storage.url(rest) From faf7c64ea50d7f96fe2645d09abacbd5115fd040 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 7 Feb 2013 10:32:13 -0500 Subject: [PATCH 17/58] add try/catch and fallback to returning a path to the root of the course --- lms/djangoapps/django_comment_client/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 151bde3dd5..877c730539 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -407,7 +407,12 @@ def get_courseware_context(content, course): if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']: (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location] else: - (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) + try: + (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) + except NoPathToItem: + # Object is not in the graph any longer, let's just get path to the base of the course + # so that we can at least return something to the caller + (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location) url = reverse('courseware_position', kwargs={"course_id":course_id, "chapter":chapter, From 70560d22379af4b27be12e1bfb10ad956328ad44 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Thu, 7 Feb 2013 10:41:37 -0500 Subject: [PATCH 18/58] Put DnD back into edit_subsection page --- cms/templates/edit_subsection.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index de5e14e0a9..d81f577940 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -107,6 +107,8 @@ + + +%endif + From 61f59918858f98754fd793bf1cf171130910f5a4 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 8 Feb 2013 12:15:09 -0500 Subject: [PATCH 33/58] Posting lots of new jobs --- lms/templates/static_templates/jobs.html | 474 ++++++++++++++++++----- 1 file changed, 368 insertions(+), 106 deletions(-) diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index 621e25b0bd..134f8a7a15 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -14,17 +14,17 @@
-

Our mission is to transform learning.

+

Our mission is to transform learning.

-
-

“EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”

- —Rafael Reif, MIT President -
+
+

“EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”

+ —Rafael Reif, MIT President +
-
-

“EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”

- —Drew Faust, Harvard President -
+
+

“EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”

+ —Drew Faust, Harvard President +
@@ -34,25 +34,45 @@
-
+

EdX is looking to add new talent to our team!

Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status

Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access to free education.  We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.

-

Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

+

Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you’re results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

As part of the edX team, you’ll receive:

  • Competitive compensation
  • Generous benefits package
  • Free lunch every day
  • -
  • A great working experience where everyone cares
  • +
  • A great working experience where everyone cares and wants to change the world (no, we’re not kidding)
-

While we appreciate every applicant's interest, only those under consideration will be contacted. We regret that phone calls will not be accepted.

+

While we appreciate every applicant’s interest, only those under consideration will be contacted. We regret that phone calls will not be accepted. Equal opportunity employer.

-

Positions

How to Apply

E-mail your resume, coverletter and any other materials to jobs@edx.org

Our Location

11 Cambridge Center
- Cambridge, MA 02142

+ Cambridge, MA 02142

From 83a4f40ac61ba1baf3b6cb92a16a4c94242d3132 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 8 Feb 2013 12:24:07 -0500 Subject: [PATCH 34/58] fix typo; fix phrasing for software engineer job --- lms/templates/static_templates/jobs.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index 134f8a7a15..a50484d722 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -419,8 +419,8 @@

Requirements:

    -
  • Expert Python Developer or familiar with dynamic development languages
  • -
  • Able to code front to back, including HTML, CSS, javascript, Django, Python
  • +
  • Real-world experience with Python or other dynamic development languages.
  • +
  • Able to code front to back, including HTML, CSS, Javascript, Django, Python
  • You must be committed to an agile development practices, in Scrum or Kanban
  • Demonstrated skills in building Service based architecture
  • Test Driven Development
  • @@ -452,7 +452,7 @@ Software Engineer

    How to Apply

    -

    E-mail your resume, coverletter and any other materials to jobs@edx.org

    +

    E-mail your resume, cover letter and any other materials to jobs@edx.org

    Our Location

    11 Cambridge Center
    Cambridge, MA 02142

    From a68094515ca839980e7e893259feacdd6cb43260 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 8 Feb 2013 12:33:19 -0500 Subject: [PATCH 35/58] fix capitalization and spacing --- lms/templates/static_templates/jobs.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index a50484d722..da31e07e42 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -96,7 +96,7 @@
  • Knowledge of copyright, trademark and patent law
  • Experience with open source content and open source software preferred
  • Outstanding communications skills (written and oral)
  • -
  • Experience with drafting and legal review of internet privacy policies and terms of use.
  • +
  • Experience with drafting and legal review of Internet privacy policies and terms of use.
  • Understanding of how to balance legal risks with business objectives
  • Ability to develop an innovative approach to legal issues in support of strategic business initiatives
  • An internal business and customer focused proactive attitude with ability to prioritize effectively
  • @@ -307,7 +307,7 @@

    DIRECTOR, PRODUCT MANAGEMENT

    When the power of edX is at its fullest, individuals become the students they had always hoped to be, Professors teach the courses they had always imagined and Universities offer educational opportunities never before seen. None of that happens by accident, so edX is seeking a Product Manager who can keep their eyes on the future and their heart and hands with a team of ferociously intelligent and dedicated technologists.

    -

    The responsibility of a Product Manager is first and foremost to provide evidence to the development team that what they build will succeed in the marketplace. It is the responsibility of the Product Manager to define the product backlog and the team to build the backlog. The Product Manager is one of the most highly leveraged individuals in the Engineering organization. They work to bring a deep knowledge of the Customer – Students, Professors and Course Staff to the Product Roadmap. The Product Manager is well-versed in the data and sets the KPI’s that drives the team, the Product Scorecard and the Company Scorecard. They are expected to become experts in the business of online learning, familiar with blended models, MOOC’s and University and Industry needs and the competition. The Product Manager must be able to understand the edX stakeholders. +

    The responsibility of a Product Manager is first and foremost to provide evidence to the development team that what they build will succeed in the marketplace. It is the responsibility of the Product Manager to define the product backlog and the team to build the backlog. The Product Manager is one of the most highly leveraged individuals in the Engineering organization. They work to bring a deep knowledge of the Customer – Students, Professors and Course Staff to the product roadmap. The Product Manager is well-versed in the data and sets the KPI’s that drives the team, the Product Scorecard and the Company Scorecard. They are expected to become experts in the business of online learning, familiar with blended models, MOOC’s and University and Industry needs and the competition. The Product Manager must be able to understand the edX stakeholders.

    Responsibilities:

      @@ -413,7 +413,7 @@

      Forums: We are building our own Forums software because we believe that education requires a forums platform capable of supporting learning communities. We are analytics driven. The ideal Forums candidates are focused on metrics and key performance indicators, understand how to build on top of a service based architecture and are wedded to quick iterations and user feedback. -

      Analytics: We are looking for a platform engineer who has deep MongoDB or no SQL database experience. Our data infrastructure needs to scale to multiple tera bytes. Researchers from Harvard, MIT, Berkeley and edX Universities will use our analytics platform to research and examine the fundamentals of learning. The analytics engineer will be responsible for both building out an analytics platform and a pub-sub and real-time pipeline processing architecture. Together they will allow researchers, students and Professors access to never before seen analytics. +

      Analytics: We are looking for a platform engineer who has deep MongoDB or no SQL database experience. Our data infrastructure needs to scale to multiple terabytes. Researchers from Harvard, MIT, Berkeley and edX Universities will use our analytics platform to research and examine the fundamentals of learning. The analytics engineer will be responsible for both building out an analytics platform and a pub-sub and real-time pipeline processing architecture. Together they will allow researchers, students and Professors access to never before seen analytics.

      Course Development Authoring Tools: We are committed to making it easy for Professors to develop and publish their courses online. So we are building the tools that allow them to readily convert their vision to an online course ready for thousands of students.

      From 98026e4325b5f8554a2e1db2c40dd4a7fcc700bd Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 8 Feb 2013 12:46:32 -0500 Subject: [PATCH 36/58] A couple minor bug fixes. https://edx.lighthouseapp.com/projects/102637/tickets/173 https://edx.lighthouseapp.com/projects/102637/tickets/177 --- cms/templates/settings.html | 2 +- common/lib/xmodule/xmodule/js/src/html/edit.coffee | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 8cd4246da9..c96d5686fd 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -206,7 +206,7 @@ from contentstore import utils

      Introducing Your Course

      - Information for perspective students + Information for prospective students
      diff --git a/common/lib/xmodule/xmodule/js/src/html/edit.coffee b/common/lib/xmodule/xmodule/js/src/html/edit.coffee index 238182f3d9..eae9df0f20 100644 --- a/common/lib/xmodule/xmodule/js/src/html/edit.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/edit.coffee @@ -107,12 +107,13 @@ class @HTMLEditingDescriptor # In order for isDirty() to return true ONLY if edits have been made after setting the text, # both the startContent must be sync'ed up and the dirty flag set to false. visualEditor.startContent = visualEditor.getContent({format: "raw", no_events: 1}); - visualEditor.isNotDirty = true @focusVisualEditor(visualEditor) @showingVisualEditor = true focusVisualEditor: (visualEditor) => visualEditor.focus() + # Need to mark editor as not dirty both when it is initially created and when we switch back to it. + visualEditor.isNotDirty = true if not @$mceToolbar? @$mceToolbar = $(@element).find('table.mceToolbar') From f51876da6a7d17a6e6197587e540fff23b2bfd7c Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 8 Feb 2013 13:42:54 -0500 Subject: [PATCH 37/58] cosmetic changes --- common/djangoapps/student/views.py | 7 +++---- lms/djangoapps/courseware/models.py | 1 + lms/djangoapps/courseware/views.py | 11 ++++------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 9ef6a0d73a..d6ad9ba4eb 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1166,10 +1166,9 @@ def test_center_login(request): 'ETDBTM' : 'ADDDOUBLE', } time_accommodation_code = None - if registration.get_accommodation_codes(): - for code in registration.get_accommodation_codes(): - if code in time_accommodation_mapping: - time_accommodation_code = time_accommodation_mapping[code] + for code in registration.get_accommodation_codes(): + if code in time_accommodation_mapping: + time_accommodation_code = time_accommodation_mapping[code] # special, hard-coded client ID used by Pearson shell for testing: if client_candidate_id == "edX003671291147": time_accommodation_code = 'TESTING' diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 875c3fcf3a..ac9bde77cd 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -24,6 +24,7 @@ class StudentModule(models.Model): MODULE_TYPES = (('problem', 'problem'), ('video', 'video'), ('html', 'html'), + ('timelimit', 'timelimit'), ) ## These three are the key for the object module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index ffe3972310..e8b36ecd2a 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -174,8 +174,7 @@ def check_for_active_timelimit_module(request, course_id, course): location = timelimit_module.location # determine where to go when the timer expires: if 'time_expired_redirect_url' not in timelimit_descriptor.metadata: - # TODO: provide a better error - raise Http404 + raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location)) time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url') context['time_expired_redirect_url'] = time_expired_redirect_url # Fetch the end time (in GMT) as stored in the module when it was started. @@ -197,8 +196,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des context = {} # determine where to go when the exam ends: if 'time_expired_redirect_url' not in timelimit_descriptor.metadata: - # TODO: provide a better error - raise Http404 + raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', timelimit_module.location)) time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url') context['time_expired_redirect_url'] = time_expired_redirect_url @@ -206,8 +204,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des if not timelimit_module.has_begun: # user has not started the exam, so start it now. if 'duration' not in timelimit_descriptor.metadata: - # TODO: provide a better error - raise Http404 + raise Http404("No {0} metadata at this location: {1} ".format('duration', timelimit_module.location)) # The user may have an accommodation that has been granted to them. # This accommodation information should already be stored in the module's state. duration = int(timelimit_descriptor.metadata.get('duration')) @@ -295,7 +292,7 @@ def index(request, course_id, chapter=None, section=None, instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) save_child_position(course_module, chapter, instance_module) else: - raise Http404 + raise Http404('No chapter descriptor found with name {}'.format(chapter)) chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter) if chapter_module is None: From 2a1c89bec5c9296c96d4d3c04efd81a4a7ea7072 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Sat, 9 Feb 2013 02:24:51 -0500 Subject: [PATCH 38/58] change missingExamSeriesCode to missingPartnerID --- common/djangoapps/student/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index d6ad9ba4eb..4413ebfc0f 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1112,9 +1112,11 @@ def test_center_login(request): # or possibly both. But for now we know what to do with an ExamSeriesCode, # while we currently have no record of RegistrationID values at all. if 'vueExamSeriesCode' not in request.POST: - # TODO: confirm this error code (made up, not in documentation) + # we are not allowed to make up a new error code, according to Pearson, + # so instead of "missingExamSeriesCode", we use a valid one that is + # inaccurate but at least distinct. (Sigh.) log.error("missing exam series code for cand ID {}".format(client_candidate_id)) - return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode")); + return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID")); exam_series_code = request.POST.get('vueExamSeriesCode') # special case for supporting test user: if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001': @@ -1188,7 +1190,7 @@ def test_center_login(request): # this information is correct, we allow the user to be logged in # without a password. This could all be formalized in a backend object # that does the above checking. - # TODO: create a backend class to do this. + # TODO: (brian) create a backend class to do this. # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass") login(request, testcenteruser.user) From 4f67c6c0522959c60658bab920d782bb08672fc1 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Sun, 10 Feb 2013 16:17:08 -0500 Subject: [PATCH 39/58] quick hack to give some protection from unauthorized users from making new courses. Make it so only is_staff people see the 'Create New Course' button. --- cms/djangoapps/contentstore/views.py | 7 ++++++- cms/templates/index.html | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 137e71b24a..87a2943773 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -122,7 +122,8 @@ def index(request): course.location.course, course.location.name])) for course in courses], - 'user': request.user + 'user': request.user, + 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff }) @@ -1259,6 +1260,10 @@ def edge(request): @login_required @expect_json def create_new_course(request): + + if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff: + raise PermissionDenied() + # This logic is repeated in xmodule/modulestore/tests/factories.py # so if you change anything here, you need to also change it there. # TODO: write a test that creates two courses, one with the factory and diff --git a/cms/templates/index.html b/cms/templates/index.html index 92987babda..45c4edc176 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -37,7 +37,9 @@

      My Courses

      % if user.is_active: - New Course + % if not disable_course_creation: + New Course + %endif
        %for course, url in courses:
      • From fb1e7f3dc1f4a2d38035c18045ccf7c1ceb00917 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sun, 10 Feb 2013 19:03:25 -0500 Subject: [PATCH 40/58] Make type check for self.code a little more robust, give default val for mmlans so it's never undefined --- common/lib/capa/capa/responsetypes.py | 2 +- lms/lib/symmath/symmath_check.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 78c986a963..ad084bdaf7 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -999,7 +999,7 @@ def sympy_check2(): self.context['debug'] = self.system.DEBUG # exec the check function - if type(self.code) == str: + if isinstance(self.code, basestring): try: exec self.code in self.context['global_context'], self.context correct = self.context['correct'] diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py index a3dec4aae5..d386ea6b06 100644 --- a/lms/lib/symmath/symmath_check.py +++ b/lms/lib/symmath/symmath_check.py @@ -238,8 +238,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None ###### PMathML input ###### # convert mathml answer to formula try: - if dynamath: - mmlans = dynamath[0] + mmlans = dynamath[0] if dynamath else None except Exception, err: mmlans = None if not mmlans: From 33009eba7d6cd9915c305894b8a85bf3ec7d42a7 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 11 Feb 2013 14:30:26 -0500 Subject: [PATCH 41/58] add exporting of grading_policy.json --- cms/djangoapps/contentstore/tests/test_contentstore.py | 10 ++++++++++ common/lib/xmodule/xmodule/modulestore/xml_exporter.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 72ae3821cc..adecd392eb 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -10,6 +10,7 @@ import json from fs.osfs import OSFS import copy from mock import Mock +from json import dumps, loads from student.models import Registration from django.contrib.auth.models import User @@ -207,6 +208,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # check for custom_tags self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template') + # check for graiding_policy.json + fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') + self.assertTrue(fs.exists('grading_policy.json')) + + # compare what's on disk compared to what we have in our course + with fs.open('grading_policy.json','r') as grading_policy: + on_disk = loads(grading_policy.read()) + course = ms.get_item(location) + self.assertEqual(on_disk, course.definition['data']['grading_policy']) # remove old course delete_course(ms, cs, location) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py index bdbd5a6133..509a2c7db9 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py @@ -2,6 +2,7 @@ import logging from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from fs.osfs import OSFS +from json import dumps def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir): @@ -27,6 +28,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d # export the course updates export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html') + # export the grading policy + policies_dir = export_fs.makeopendir('policies') + course_run_policy_dir = policies_dir.makeopendir(course.location.name) + with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: + grading_policy.write(dumps(course.definition['data']['grading_policy'])) + def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''): query_loc = Location('i4x', course_location.org, course_location.course, category_type, None) From ae4a854e188165239ebd961df53a557431f0dd9c Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 11 Feb 2013 15:46:58 -0500 Subject: [PATCH 42/58] Bug 180 --- cms/static/js/views/overview.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 8cbae177a8..d064f24006 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -202,13 +202,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) { children = _.without(children, ui.draggable.data('id')); } // add to this parent (figure out where) - for (var i = 0; i < _els.length; i++) { - if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) { + for (var i = 0, bump = 0; i < _els.length; i++) { + if (ui.draggable.is(_els[i])) { + bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c + // it's not in that list + } + else if (ui.offset.top < $(_els[i]).offset().top) { // insert at i in children and _els ui.draggable.insertBefore($(_els[i])); // TODO figure out correct way to have it remove the style: top:n; setting (and similar line below) ui.draggable.attr("style", "position:relative;"); - children.splice(i, 0, ui.draggable.data('id')); + children.splice(i + bump, 0, ui.draggable.data('id')); break; } } From 565f5f0adc8505966e099b525ec9abe21757745a Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 11 Feb 2013 16:28:22 -0500 Subject: [PATCH 43/58] Stop simple clicks from paging to top w/o disrupting dnd --- cms/static/js/views/overview.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index d064f24006..24b29156cd 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -58,6 +58,9 @@ $(document).ready(function() { drop: onSectionReordered, greedy: true }); + + // stop clicks on drag bars from doing their thing w/o stopping drag (did this cancel pointer?) + $('.courseware-overview').click(function(e) {e.preventDefault(); }); }); From 777e0ef9c5165afee5a6094a1231c812eb72d085 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 11 Feb 2013 16:36:34 -0500 Subject: [PATCH 44/58] The other one was too inclusive --- cms/static/js/views/overview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 24b29156cd..db42c13208 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -60,7 +60,7 @@ $(document).ready(function() { }); // stop clicks on drag bars from doing their thing w/o stopping drag (did this cancel pointer?) - $('.courseware-overview').click(function(e) {e.preventDefault(); }); + $('.drag-handle').click(function(e) {e.preventDefault(); }); }); From 8bc9564d92304f60def3a384ce78e876bf63e203 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 11 Feb 2013 16:45:36 -0500 Subject: [PATCH 45/58] clean up note to self --- cms/static/js/views/overview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index db42c13208..7d92ab69ad 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -59,7 +59,7 @@ $(document).ready(function() { greedy: true }); - // stop clicks on drag bars from doing their thing w/o stopping drag (did this cancel pointer?) + // stop clicks on drag bars from doing their thing w/o stopping drag $('.drag-handle').click(function(e) {e.preventDefault(); }); }); From aa2d0ecaf26f79b7b4bf807a289d415e4a9b3024 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 11 Feb 2013 17:06:35 -0500 Subject: [PATCH 46/58] We no longer need override_settings, Django has a better one. --- cms/djangoapps/contentstore/tests/test_contentstore.py | 2 +- cms/djangoapps/contentstore/tests/tests.py | 1 - cms/djangoapps/contentstore/tests/utils.py | 1 - common/djangoapps/course_groups/tests/tests.py | 2 +- common/djangoapps/status/tests.py | 2 +- lms/djangoapps/course_wiki/tests/tests.py | 2 +- lms/djangoapps/courseware/tests/tests.py | 2 +- lms/djangoapps/django_comment_client/tests.py | 2 +- lms/djangoapps/instructor/tests.py | 2 +- lms/djangoapps/open_ended_grading/tests.py | 2 +- requirements.txt | 1 - 11 files changed, 8 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index adecd392eb..dcd1f408cd 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,7 +1,7 @@ import json import shutil from django.test.client import Client -from override_settings import override_settings +from django.test.utils import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 9af5b09276..d2f18f2e49 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,7 +1,6 @@ import json import shutil from django.test.client import Client -from override_settings import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 4e3510463f..be028b2836 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -2,7 +2,6 @@ import json import copy from time import time from django.test import TestCase -from override_settings import override_settings from django.conf import settings from student.models import Registration diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index 0fbf863fee..b3ad928b39 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -2,7 +2,7 @@ import django.test from django.contrib.auth.models import User from django.conf import settings -from override_settings import override_settings +from django.test.utils import override_settings from course_groups.models import CourseUserGroup from course_groups.cohorts import (get_cohort, get_course_cohorts, diff --git a/common/djangoapps/status/tests.py b/common/djangoapps/status/tests.py index 98a36f433a..1695663ac5 100644 --- a/common/djangoapps/status/tests.py +++ b/common/djangoapps/status/tests.py @@ -1,7 +1,7 @@ from django.conf import settings from django.test import TestCase import os -from override_settings import override_settings +from django.test.utils import override_settings from tempfile import NamedTemporaryFile from status import get_site_status_msg diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 99f138f0bc..cecc4f9cf9 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -1,5 +1,5 @@ from django.core.urlresolvers import reverse -from override_settings import override_settings +from django.test.utils import override_settings import xmodule.modulestore.django diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index efa5ad823e..fb6842d4a9 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -11,7 +11,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.conf import settings from django.core.urlresolvers import reverse -from override_settings import override_settings +from django.test.utils import override_settings import xmodule.modulestore.django from xmodule.modulestore.mongo import MongoModuleStore diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py index ac059a1e3f..4b5fe2ba5a 100644 --- a/lms/djangoapps/django_comment_client/tests.py +++ b/lms/djangoapps/django_comment_client/tests.py @@ -6,7 +6,7 @@ from django.conf import settings from mock import Mock -from override_settings import override_settings +from django.test.utils import override_settings import xmodule.modulestore.django diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 2610e57422..b775aa158a 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -15,7 +15,7 @@ import json from nose import SkipTest from mock import patch, Mock -from override_settings import override_settings +from django.test.utils import override_settings # Need access to internal func to put users in the right group from django.contrib.auth.models import Group diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 4d220d4baa..ec2fe5ab38 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -22,7 +22,7 @@ from mitxmako.shortcuts import render_to_string import logging log = logging.getLogger(__name__) -from override_settings import override_settings +from django.test.utils import override_settings from django.http import QueryDict diff --git a/requirements.txt b/requirements.txt index 0faf2e3ba5..7bfaa11bc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,6 @@ django_nose nosexcover==1.0.7 rednose==0.3.3 GitPython==0.3.2.RC1 -django-override-settings==1.2 mock==0.8.0 PyYAML==3.10 South==0.7.6 From 2dcdeff4b6d47b333971e74cd47eece1a86546c0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 11 Feb 2013 17:07:15 -0500 Subject: [PATCH 47/58] Make replace_static_urls leave alone urls that have '?raw' at the end - needed for GWT'ed modules that rely on the filename to find dependencies --- common/djangoapps/static_replace/__init__.py | 20 +++++++++--- .../test/test_static_replace.py | 31 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index d41688530e..088ba09181 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -13,12 +13,22 @@ log = logging.getLogger(__name__) def _url_replace_regex(prefix): + """ + Match static urls in quotes that don't end in '?raw'. + + I'm sorry. http://xkcd.com/1171/ + + (?\\?['"]) # the opening quotes - (?P{prefix}) # theeprefix - (?P.*?) # everything else in the url - (?P=quote) # the first matching closing quote + (?x) # flags=re.VERBOSE + (?P\\?['"]) # the opening quotes + (?P{prefix}) # the prefix + (?P.*? # everything else in the url... + (? Date: Mon, 11 Feb 2013 17:50:13 -0500 Subject: [PATCH 48/58] Simplify, make it actually work. - regexp was too complicated, didn't work --- common/djangoapps/static_replace/__init__.py | 14 +++++++------- .../static_replace/test/test_static_replace.py | 9 ++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index 088ba09181..fb1f48d143 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -16,18 +16,14 @@ def _url_replace_regex(prefix): """ Match static urls in quotes that don't end in '?raw'. - I'm sorry. http://xkcd.com/1171/ - - (?\\?['"]) # the opening quotes (?P{prefix}) # the prefix - (?P.*? # everything else in the url... - (?.*?) # everything else in the url (?P=quote) # the first matching closing quote """.format(prefix=prefix) @@ -84,6 +80,10 @@ def replace_static_urls(text, data_directory, course_namespace=None): quote = match.group('quote') rest = match.group('rest') + # Don't mess with things that end in '?raw' + if rest.endswith('?raw'): + return original + # course_namespace is not None, then use studio style urls if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): url = StaticContent.convert_legacy_static_url(rest, course_namespace) diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py index 80b129f459..f23610e1bd 100644 --- a/common/djangoapps/static_replace/test/test_static_replace.py +++ b/common/djangoapps/static_replace/test/test_static_replace.py @@ -81,15 +81,22 @@ def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings): def test_raw_static_check(): + """ + Make sure replace_static_urls leaves alone things that end in '.raw' + """ path = '"/static/foo.png?raw"' assert_equals(path, replace_static_urls(path, DATA_DIRECTORY)) + text = 'text
        Date: Mon, 11 Feb 2013 20:01:09 -0500 Subject: [PATCH 49/58] fix showing less than and greater than in code --- .../src/discussion/views/discussion_thread_show_view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index a8e95c2565..6320c3d1e3 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -47,7 +47,7 @@ if Backbone? convertMath: -> element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] toggleVote: (event) -> From 21f4616080bc01de312d85d9ad9207c19c788b6d Mon Sep 17 00:00:00 2001 From: Kevin Chugh Date: Tue, 12 Feb 2013 09:18:46 -0500 Subject: [PATCH 50/58] spread less than and greater than fix to responses and comments, and profile view --- .../src/discussion/views/discussion_thread_profile_view.coffee | 2 +- .../src/discussion/views/response_comment_show_view.coffee | 2 +- .../src/discussion/views/thread_response_show_view.coffee | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee index d31a402a99..8b47696c01 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee @@ -50,7 +50,7 @@ if Backbone? convertMath: -> element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] renderResponses: -> diff --git a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee index e6c8064978..84e7357e1f 100644 --- a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee +++ b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee @@ -26,7 +26,7 @@ if Backbone? convertMath: -> body = @$el.find(".response-body") - body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html() + body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]] markAsStaff: -> diff --git a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee index 32683fe6f6..1f305ddf34 100644 --- a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee @@ -30,7 +30,7 @@ if Backbone? convertMath: -> element = @$(".response-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] markAsStaff: -> From 83e4dfaacb49c880b59f123d306a1280a0a6aabc Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 12 Feb 2013 09:58:16 -0500 Subject: [PATCH 51/58] lms - pearson: trying to resolve JQUI accordion width bug in IE7 --- lms/static/sass/course/courseware/_sidebar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index 4e893d2455..1ab841e1e5 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -99,6 +99,7 @@ section.course-index { @include border-radius(0); margin: 0; padding: 9px 0 9px 9px; + overflow: auto; li { border-bottom: 0; From f33f7134fbf246de87e6b8d938ef145373196bb8 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 12 Feb 2013 10:34:21 -0500 Subject: [PATCH 52/58] lms - pearson: trying to resolve JQUI accordion width bug in IE7 - enforcing width to chapters --- lms/static/sass/course/courseware/_sidebar.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index 1ab841e1e5..81b497d4f9 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -67,7 +67,7 @@ section.course-index { } .chapter { - width: 100%; + width: 100% !important; @include box-sizing(border-box); padding: 11px 14px; @include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0)); @@ -100,6 +100,7 @@ section.course-index { margin: 0; padding: 9px 0 9px 9px; overflow: auto; + width: 100%; li { border-bottom: 0; From 78d3d2006919bf95e0fb29ad2101bb6ece707899 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 23 Jan 2013 17:11:02 -0500 Subject: [PATCH 53/58] When checking types to convert data, don't forget about longs. 32-bit Pythons make longs from values that are ints on 64-bit Pythons. Conflicts: common/djangoapps/util/converters.py --- common/djangoapps/util/converters.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py index 900371a0dd..ec2d29ecfa 100644 --- a/common/djangoapps/util/converters.py +++ b/common/djangoapps/util/converters.py @@ -18,10 +18,13 @@ def jsdate_to_time(field): """ if field is None: return field - elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z - d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable + elif isinstance(field, basestring): + # ISO format but ignores time zone assuming it's Z. + d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable return d.utctimetuple() - elif isinstance(field, int) or isinstance(field, float): + elif isinstance(field, (int, long, float)): return time.gmtime(field / 1000) elif isinstance(field, time.struct_time): return field + else: + raise ValueError("Couldn't convert %r to time" % field) From b5378b04b3402db5c99aaeaf3db456bbd8c5e684 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 12 Feb 2013 12:25:11 -0500 Subject: [PATCH 54/58] change timer to calculate relative duration in javascript --- .../lib/xmodule/xmodule/timelimit_module.py | 4 ++-- lms/djangoapps/courseware/views.py | 19 +++++++++---------- lms/templates/courseware/courseware.html | 17 +++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py index 23ed06eb59..9abb5d183f 100644 --- a/common/lib/xmodule/xmodule/timelimit_module.py +++ b/common/lib/xmodule/xmodule/timelimit_module.py @@ -86,8 +86,8 @@ class TimeLimitModule(XModule): modified_duration = self._get_accommodated_duration(duration) self.ending_at = self.beginning_at + modified_duration - def get_end_time_in_ms(self): - return int(self.ending_at * 1000) + def get_remaining_time_in_ms(self): + return int((self.ending_at - time()) * 1000) def get_instance_state(self): state = {} diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index e8b36ecd2a..fb351e1c01 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -177,10 +177,10 @@ def check_for_active_timelimit_module(request, course_id, course): raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location)) time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url') context['time_expired_redirect_url'] = time_expired_redirect_url - # Fetch the end time (in GMT) as stored in the module when it was started. - # This value should be UTC time as number of milliseconds since epoch. - end_date = timelimit_module.get_end_time_in_ms() - context['timer_expiration_datetime'] = end_date + # Fetch the remaining time relative to the end time as stored in the module when it was started. + # This value should be in milliseconds. + remaining_time = timelimit_module.get_remaining_time_in_ms() + context['timer_expiration_duration'] = remaining_time if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata: context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation'] return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location}) @@ -191,7 +191,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des ''' Updates the state of the provided timing module, starting it if it hasn't begun. Returns dict with timer-related values to enable display of time remaining. - Returns 'timer_expiration_datetime' in dict if timer is still active, and not if timer has expired. + Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired. ''' context = {} # determine where to go when the exam ends: @@ -215,10 +215,9 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des instance_module.save() # the exam has been started, either because the student is returning to the - # exam page, or because they have just visited it. Fetch the end time (in GMT) as stored - # in the module when it was started. - # This value should be UTC time as number of milliseconds since epoch. - context['timer_expiration_datetime'] = timelimit_module.get_end_time_in_ms() + # exam page, or because they have just visited it. Fetch the remaining time relative to the + # end time as stored in the module when it was started. + context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms() # also use the timed module to determine whether top-level navigation is visible: if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata: context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation'] @@ -325,7 +324,7 @@ def index(request, course_id, chapter=None, section=None, if section_module.category == 'timelimit': timer_context = update_timelimit_module(request.user, course_id, student_module_cache, section_descriptor, section_module) - if 'timer_expiration_datetime' in timer_context: + if 'timer_expiration_duration' in timer_context: context.update(timer_context) else: # if there is no expiration defined, then we know the timer has expired: diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 72a4b2cae1..fcbc83d815 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -60,14 +60,12 @@ }); -% if timer_expiration_datetime: +% if timer_expiration_duration: