From 9b3d7efb3f60d3b0913c49e01847f9be38892b6b Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Wed, 30 Jan 2013 18:14:53 -0500
Subject: [PATCH 01/29] 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>
+<%block name="title">${course.number} Exam%block>
+
+<%block name="headextra">
+ <%static:css group='course'/>
+ <%include file="../discussion/_js_head_dependencies.html" />
+%block>
+
+<%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
+
+
+
+
+
+
+
+
+%block>
+
+
+
+
+
+
+
+% 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%block>
+
+
+
+
+ <%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/29] 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/29] 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 @@
%block>
-
+
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)/(?PFinal_Exam_Fall_2012)/$',
+ 'courseware.views.timed_exam'),
#Inside the course
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/$',
From 1685f302ab7721be80634ae2f68154d74d452cd2 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Mon, 4 Feb 2013 02:22:24 -0500
Subject: [PATCH 04/29] 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 05/29] 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 @@
%block>
From 1923ae0d6b08aa094ba1e464a4ead26b1b782293 Mon Sep 17 00:00:00 2001
From: Brian Talbot
Date: Mon, 4 Feb 2013 15:40:01 -0500
Subject: [PATCH 06/29] 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 07/29] 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 bfc452759013a443915396c3df1f34e5e9e423f1 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Wed, 6 Feb 2013 15:23:11 -0500
Subject: [PATCH 08/29] 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
+
%block>
-<%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
+% if timer_expiration_datetime:
+
+
+
Time Remaining:
+ % if timer_navigation_return_url:
+ Return...
+ % endif
+
% endif
From cc11dc2aa33fefb056c42632882705e300199d1e Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 7 Feb 2013 15:57:43 -0500
Subject: [PATCH 12/29] switch to using timelimit module for Pearson test
---
common/djangoapps/student/views.py | 96 ++++--
common/lib/xmodule/setup.py | 2 +-
common/lib/xmodule/xmodule/course_module.py | 6 +-
...xed_time_module.py => timelimit_module.py} | 56 +--
.../migrations/0006_add_timed_module.py | 119 -------
lms/djangoapps/courseware/models.py | 84 -----
lms/djangoapps/courseware/views.py | 324 +++++-------------
lms/urls.py | 10 -
8 files changed, 173 insertions(+), 524 deletions(-)
rename common/lib/xmodule/xmodule/{fixed_time_module.py => timelimit_module.py} (77%)
delete mode 100644 lms/djangoapps/courseware/migrations/0006_add_timed_module.py
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 61313376d1..235f4b414f 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -39,12 +39,15 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
+from xmodule.modulestore import Location
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 courseware.models import StudentModuleCache
+from courseware.views import get_module_for_descriptor
+from courseware.module_render import get_instance_module
from statsd import statsd
@@ -1082,13 +1085,14 @@ 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);
+ log.error("generating error URL with error code {}".format(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.
+ # Pearson shell.
error_url = request.POST.get("errorURL")
- # check that the parameters have not been tampered with, by comparing the code provided by Pearson
+ # TODO: 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"));
@@ -1112,65 +1116,81 @@ def test_center_login(request):
try:
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
except TestCenterUser.DoesNotExist:
+ log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
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.
+ # Note that we could rely in future on either the registrationId or the exam code,
+ # 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)
+ log.error("missing exam series code for cand ID {}".format(client_candidate_id))
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:
+ log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
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':
- # 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.
+ course = course_from_id(course_id) # assume it will be found....
+ if not course:
+ log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
+ exam = course.get_test_center_exam(exam_series_code)
+ if not exam:
+ log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
+ return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
+ location = exam.exam_url
+ redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
+
+ log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
+
+ # check if the test has already been taken
+ timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
+ if not timelimit_descriptor:
+ log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
+ return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
+
+ timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
+ timelimit_descriptor, depth=None)
+ timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
+ timelimit_module_cache, course_id, position=None)
+ if not timelimit_module.category == 'timelimit':
+ log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
+ return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
+
+ if timelimit_module and timelimit_module.has_ended:
+ log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
+ return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
-
+ # check if we need to provide an accommodation:
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, location=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
+
+ 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]
- if client_candidate_id == "edX003671291147":
- time_accommodation_code = 'TESTING'
- if time_accommodation_code:
- timed_module = TimedModule(student=request.user, course_id=course_id, location=location)
- timed_module.accommodation_code = time_accommodation_code
- timed_module.save()
+ # special, hard-coded client ID used by Pearson shell for testing:
+ if client_candidate_id == "edX003671291147":
+ time_accommodation_code = 'TESTING'
+
+ if time_accommodation_code:
+ timelimit_module.accommodation_code = time_accommodation_code
+ instance_module = get_instance_module(course_id, testcenteruser.user, timelimit_module, timelimit_module_cache)
+ instance_module.state = timelimit_module.get_instance_state()
+ instance_module.save()
+ log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
# UGLY HACK!!!
# Login assumes that authentication has occurred, and that there is a
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index f61e6f6f36..d3a0562b41 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -23,7 +23,6 @@ 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",
@@ -32,6 +31,7 @@ setup(
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
+ "timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 6e3e2cfa39..4c5c3a0a90 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("First appointment date must be before last appointment date")
if self.registration_end_date > self.last_eligible_appointment_date:
raise ValueError("Registration end date must be before last appointment date")
-
+ self.exam_url = exam_info.get('Exam_URL')
def _try_parse_time(self, key):
"""
@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
else:
return None
+ def get_test_center_exam(self, exam_series_code):
+ exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
+ return exams[0] if len(exams) == 1 else None
+
@property
def title(self):
return self.display_name
diff --git a/common/lib/xmodule/xmodule/fixed_time_module.py b/common/lib/xmodule/xmodule/timelimit_module.py
similarity index 77%
rename from common/lib/xmodule/xmodule/fixed_time_module.py
rename to common/lib/xmodule/xmodule/timelimit_module.py
index f1fec26dc3..23ed06eb59 100644
--- a/common/lib/xmodule/xmodule/fixed_time_module.py
+++ b/common/lib/xmodule/xmodule/timelimit_module.py
@@ -4,22 +4,16 @@ import logging
from lxml import etree
from time import time
-from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.editing_module import XMLEditingDescriptor
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):
+class TimeLimitModule(XModule):
'''
Wrapper module which imposes a time constraint for the completion of its child.
'''
@@ -29,9 +23,7 @@ class FixedTimeModule(XModule):
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.rendered = False
self.beginning_at = None
self.ending_at = None
self.accommodation_code = None
@@ -46,13 +38,6 @@ class FixedTimeModule(XModule):
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
@@ -81,8 +66,6 @@ class FixedTimeModule(XModule):
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):
@@ -101,8 +84,6 @@ class FixedTimeModule(XModule):
'''
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):
@@ -132,31 +113,32 @@ class FixedTimeModule(XModule):
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})
+ def handle_ajax(self, dispatch, get):
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()
+ children = self.get_display_items()
+ if children:
+ child = children[0]
+ self.content = child.get_html()
self.rendered = True
def get_icon_class(self):
- return self.get_children()[0].get_icon_class()
+ children = self.get_children()
+ if children:
+ return children[0].get_icon_class()
+ else:
+ return "other"
+class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
-class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
- # TODO: fix this template?!
- mako_template = 'widgets/sequence-edit.html'
- module_class = FixedTimeModule
+ module_class = TimeLimitModule
- stores_state = True # For remembering when a student started, and when they should end
+ # For remembering when a student started, and when they should end
+ stores_state = True
@classmethod
def definition_from_xml(cls, xml_object, system):
@@ -165,14 +147,14 @@ class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
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...")
+ log.exception("Unable to load child when parsing TimeLimit 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')
+ xml_object = etree.Element('timelimit')
for child in self.get_children():
xml_object.append(
etree.fromstring(child.export_to_xml(resource_fs)))
diff --git a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
deleted file mode 100644
index 6e8791a975..0000000000
--- a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# -*- 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)),
- ('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)),
- ('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', 'location', 'course_id']
- db.create_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
-
-
- def backwards(self, orm):
- # 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')
-
-
- 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', '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'}),
- '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 d9cc560215..87b9edaac2 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -212,87 +212,3 @@ 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')
- 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', 'location', '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'),
- ('TESTING', 'Extra Time -- Large amount for testing purposes')
- )
- 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)
- elif self.accommodation_code == 'TESTING':
- # when testing, set timer to run for a week at a time.
- return 3600 * 24 * 7
-
- # 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 0acf435f0b..07b177979a 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -20,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, TimedModule
+from courseware.models import StudentModule, StudentModuleCache
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
@@ -31,6 +31,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location
+#from xmodule.fixed_time_module import FixedTimeModule
import comment_client
@@ -152,6 +153,80 @@ def save_child_position(seq_module, child_name, instance_module):
instance_module.state = seq_module.get_instance_state()
instance_module.save()
+def check_for_active_timelimit_module(request, course_id, course):
+ '''
+ Looks for a timing module for the given user and course that is currently active.
+ If found, returns a context dict with timer-related values to enable display of time remaining.
+ '''
+ context = {}
+ timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit')
+ if timelimit_student_modules:
+ for timelimit_student_module in timelimit_student_modules:
+ # get the corresponding section_descriptor for the given StudentModel entry:
+ module_state_key = timelimit_student_module.module_state_key
+ timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key))
+ timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course.id, request.user,
+ timelimit_descriptor, depth=None)
+ timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
+ timelimit_module_cache, course.id, position=None)
+ if timelimit_module is not None and timelimit_module.category == 'timelimit' and \
+ timelimit_module.has_begun and not timelimit_module.has_ended:
+ 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
+ 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
+ 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})
+ context['timer_navigation_return_url'] = return_url
+ return context
+
+def update_timelimit_module(user, course_id, student_module_cache, timelimit_descriptor, timelimit_module):
+ '''
+ 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.
+ '''
+ 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
+ time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
+ context['time_expired_redirect_url'] = time_expired_redirect_url
+
+ if not timelimit_module.has_ended:
+ 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
+ # 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'))
+ timelimit_module.begin(duration)
+ # we have changed state, so we need to persist the change:
+ instance_module = get_instance_module(course_id, user, timelimit_module, student_module_cache)
+ instance_module.state = timelimit_module.get_instance_state()
+ 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()
+ # 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']
+ return context
+
@login_required
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@@ -215,43 +290,6 @@ 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)
@@ -286,7 +324,20 @@ def index(request, course_id, chapter=None, section=None,
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
save_child_position(chapter_module, section, instance_module)
-
+ # check here if this section *is* a timed module.
+ 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:
+ context.update(timer_context)
+ else:
+ # if there is no expiration defined, then we know the timer has expired:
+ return HttpResponseRedirect(timer_context['time_expired_redirect_url'])
+ else:
+ # check here if this page is within a course that has an active timed module running. If so, then
+ # add in the appropriate timer information to the rendering context:
+ context.update(check_for_active_timelimit_module(request, course_id, course))
+
context['content'] = section_module.get_html()
else:
# section is none, so display a message
@@ -334,201 +385,6 @@ 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 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
- 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'],
- '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')
- }
-
- # 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)
- 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()
-
- # 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
- # 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
- duration = int(section_descriptor.metadata.get('duration'))
-
- # get corresponding time module, if one is present:
- try:
- 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.
- 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,
- 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.
- # 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.
- timed_module = TimedModule(student=request.user, course_id=course_id, location=section_module.location)
- 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'] = 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
- 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/urls.py b/lms/urls.py
index f6819d05a2..f92b63aac2 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -217,16 +217,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"),
- # 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)/(?PFinal_Exam_Fall_2012)/$',
- 'courseware.views.timed_exam'),
-
#Inside the course
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/$',
'courseware.views.course_info', name="course_root"),
From 80e4944314ae8de27adf0d4ffd2e3fe1131f72e8 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 7 Feb 2013 16:12:02 -0500
Subject: [PATCH 13/29] minor cleanup
---
common/djangoapps/student/views.py | 28 ++-----------------
common/lib/xmodule/xmodule/modulestore/xml.py | 2 +-
lms/djangoapps/courseware/models.py | 4 ---
3 files changed, 4 insertions(+), 30 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 235f4b414f..61cb1299b4 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -14,20 +14,19 @@ from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
+from django.core.cache import cache
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,\
- HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect
+from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup
-from django.core.cache import cache
-from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange,
@@ -1059,27 +1058,6 @@ def accept_name_change(request):
return accept_name_change_by_id(int(request.POST['id']))
-# 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 atest_center_login(request):
- if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
- raise Http404
-
- client_candidate_id = request.POST.get("clientCandidateID")
- # registration_id = request.POST.get("registrationID")
- exit_url = request.POST.get("exitURL")
- error_url = request.POST.get("errorURL")
-
- if client_candidate_id == "edX003671291147":
- user = authenticate(username=settings.PEARSON_TEST_USER,
- password=settings.PEARSON_TEST_PASSWORD)
- login(request, user)
- return redirect('/courses/MITx/6.002x/2012_Fall/courseware/Final_Exam/Final_Exam_Fall_2012/')
- else:
- return HttpResponseForbidden()
-
-
@csrf_exempt
def test_center_login(request):
# errors are returned by navigating to the error_url, adding a query parameter named "code"
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 332b1b1898..d225eef980 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', 'fixedtime')
+ need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'timelimit')
attr = xml_data.attrib
tag = xml_data.tag
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 87b9edaac2..78c6e738b0 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -12,9 +12,6 @@ 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.contrib.auth.models import User
@@ -211,4 +208,3 @@ class OfflineComputedGradeLog(models.Model):
def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created)
-
From 48e582647c723221b81b153d3742b1fb87c578b5 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Thu, 7 Feb 2013 16:24:43 -0500
Subject: [PATCH 14/29] bug 171
---
cms/templates/manage_users.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html
index 36930f5386..99ac279bfb 100644
--- a/cms/templates/manage_users.html
+++ b/cms/templates/manage_users.html
@@ -97,7 +97,7 @@
$cancelButton.bind('click', hideNewUserForm);
$('.new-user-button').bind('click', showNewUserForm);
- $body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
+ $('body').bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('.remove-user').click(function() {
$.ajax({
From fa5537ab719f542a6d4da49bc1efad05885ceb28 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Thu, 7 Feb 2013 17:09:57 -0500
Subject: [PATCH 15/29] On first request for handouts, create the db record.
(bug 160)
---
.../contentstore/module_info_model.py | 148 +++++++++---------
1 file changed, 72 insertions(+), 76 deletions(-)
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
index 796184baa0..7ed4505c94 100644
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -1,37 +1,35 @@
-import logging
from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
-from xmodule.modulestore.django import modulestore
-from lxml import etree
-import re
-from django.http import HttpResponseBadRequest, Http404
+from django.http import Http404
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
- try:
- if location.revision is None:
- module = store.get_item(location)
- else:
- module = store.get_item(location)
- except ItemNotFoundError:
- raise Http404
+ try:
+ if location.revision is None:
+ module = store.get_item(location)
+ else:
+ module = store.get_item(location)
+ except ItemNotFoundError:
+ # create a new one
+ template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
+ module = store.clone_item(template_location, location)
- data = module.definition['data']
- if rewrite_static_links:
- data = replace_static_urls(
- module.definition['data'],
- None,
- course_namespace=Location([
- module.location.tag,
- module.location.org,
- module.location.course,
+ data = module.definition['data']
+ if rewrite_static_links:
+ data = replace_static_urls(
+ module.definition['data'],
None,
- None
- ])
- )
+ course_namespace=Location([
+ module.location.tag,
+ module.location.org,
+ module.location.course,
+ None,
+ None
+ ])
+ )
- return {
+ return {
'id': module.location.url(),
'data': data,
'metadata': module.metadata
@@ -39,58 +37,56 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
def set_module_info(store, location, post_data):
- module = None
- isNew = False
- try:
- if location.revision is None:
- module = store.get_item(location)
- else:
- module = store.get_item(location)
- except:
- pass
+ module = None
+ try:
+ if location.revision is None:
+ module = store.get_item(location)
+ else:
+ module = store.get_item(location)
+ except:
+ pass
- if module is None:
- # new module at this location
- # presume that we have an 'Empty' template
- template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
- module = store.clone_item(template_location, location)
- isNew = True
+ if module is None:
+ # new module at this location
+ # presume that we have an 'Empty' template
+ template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
+ module = store.clone_item(template_location, location)
- if post_data.get('data') is not None:
- data = post_data['data']
- store.update_item(location, data)
+ if post_data.get('data') is not None:
+ data = post_data['data']
+ store.update_item(location, data)
- # cdodge: note calling request.POST.get('children') will return None if children is an empty array
- # so it lead to a bug whereby the last component to be deleted in the UI was not actually
- # deleting the children object from the children collection
- if 'children' in post_data and post_data['children'] is not None:
- children = post_data['children']
- store.update_children(location, children)
+ # cdodge: note calling request.POST.get('children') will return None if children is an empty array
+ # so it lead to a bug whereby the last component to be deleted in the UI was not actually
+ # deleting the children object from the children collection
+ if 'children' in post_data and post_data['children'] is not None:
+ children = post_data['children']
+ store.update_children(location, children)
- # cdodge: also commit any metadata which might have been passed along in the
- # POST from the client, if it is there
- # NOTE, that the postback is not the complete metadata, as there's system metadata which is
- # not presented to the end-user for editing. So let's fetch the original and
- # 'apply' the submitted metadata, so we don't end up deleting system metadata
- if post_data.get('metadata') is not None:
- posted_metadata = post_data['metadata']
-
- # update existing metadata with submitted metadata (which can be partial)
- # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key in posted_metadata.keys():
-
- # let's strip out any metadata fields from the postback which have been identified as system metadata
- # and therefore should not be user-editable, so we should accept them back from the client
- if metadata_key in module.system_metadata_fields:
- del posted_metadata[metadata_key]
- elif posted_metadata[metadata_key] is None:
- # remove both from passed in collection as well as the collection read in from the modulestore
- if metadata_key in module.metadata:
- del module.metadata[metadata_key]
- del posted_metadata[metadata_key]
-
- # overlay the new metadata over the modulestore sourced collection to support partial updates
- module.metadata.update(posted_metadata)
-
- # commit to datastore
- store.update_metadata(location, module.metadata)
+ # cdodge: also commit any metadata which might have been passed along in the
+ # POST from the client, if it is there
+ # NOTE, that the postback is not the complete metadata, as there's system metadata which is
+ # not presented to the end-user for editing. So let's fetch the original and
+ # 'apply' the submitted metadata, so we don't end up deleting system metadata
+ if post_data.get('metadata') is not None:
+ posted_metadata = post_data['metadata']
+
+ # update existing metadata with submitted metadata (which can be partial)
+ # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
+ for metadata_key in posted_metadata.keys():
+
+ # let's strip out any metadata fields from the postback which have been identified as system metadata
+ # and therefore should not be user-editable, so we should accept them back from the client
+ if metadata_key in module.system_metadata_fields:
+ del posted_metadata[metadata_key]
+ elif posted_metadata[metadata_key] is None:
+ # remove both from passed in collection as well as the collection read in from the modulestore
+ if metadata_key in module.metadata:
+ del module.metadata[metadata_key]
+ del posted_metadata[metadata_key]
+
+ # overlay the new metadata over the modulestore sourced collection to support partial updates
+ module.metadata.update(posted_metadata)
+
+ # commit to datastore
+ store.update_metadata(location, module.metadata)
From 7b7b0690b68d9c1aaa4350793fcb5133b4c0b814 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 7 Feb 2013 18:18:45 -0500
Subject: [PATCH 16/29] fix test_conditional
---
common/lib/xmodule/xmodule/tests/test_conditional.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py
index 1b463eccaf..361a6ea785 100644
--- a/common/lib/xmodule/xmodule/tests/test_conditional.py
+++ b/common/lib/xmodule/xmodule/tests/test_conditional.py
@@ -84,18 +84,21 @@ class ConditionalModuleTest(unittest.TestCase):
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
location = descriptor.location
instance_state = instance_states.get(location.category, None)
- print "inner_get_module, location.category=%s, inst_state=%s" % (location.category, instance_state)
+ print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
- module = inner_get_module(location)
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text
test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module
+ module = inner_get_module(location)
print "module: ", module
+ print "module definition: ", module.definition
+ print "module children: ", module.get_children()
+ print "module display items (children): ", module.get_display_items()
html = module.get_html()
print "html type: ", type(html)
From f1198b5f5f050024141eaf15231664b371c04b7e Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Fri, 8 Feb 2013 02:15:45 -0500
Subject: [PATCH 17/29] for starting pearson exam, just call jump_to() instead
of reverse(jump_to...)
---
common/djangoapps/student/views.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 60aa03857b..c9ca330479 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -45,7 +45,7 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access
from courseware.models import StudentModuleCache
-from courseware.views import get_module_for_descriptor
+from courseware.views import get_module_for_descriptor, jump_to
from courseware.module_render import get_instance_module
from statsd import statsd
@@ -1138,8 +1138,6 @@ def test_center_login(request):
log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
location = exam.exam_url
- redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
-
log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
# check if the test has already been taken
@@ -1195,7 +1193,7 @@ def test_center_login(request):
login(request, testcenteruser.user)
# And start the test:
- return redirect(redirect_url)
+ return jump_to(request, course_id, location)
def _get_news(top=None):
From ab6f383be1f8f796d61ed32b928380867b5ee8b6 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Fri, 8 Feb 2013 03:05:28 -0500
Subject: [PATCH 18/29] support for Pearson test user
---
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 c9ca330479..9ef6a0d73a 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1099,7 +1099,6 @@ def test_center_login(request):
# expected values....
# registration_id = request.POST.get("registrationID")
# exit_url = request.POST.get("exitURL")
-
# find testcenter_user that matches the provided ID:
try:
@@ -1108,7 +1107,6 @@ def test_center_login(request):
log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
-
# find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the exam code,
# or possibly both. But for now we know what to do with an ExamSeriesCode,
@@ -1118,7 +1116,11 @@ def test_center_login(request):
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode"));
exam_series_code = request.POST.get('vueExamSeriesCode')
-
+ # special case for supporting test user:
+ if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001':
+ log.warning("test user {} using unexpected exam code {}, coercing to 6002x001".format(client_candidate_id, exam_series_code))
+ exam_series_code = '6002x001'
+
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
From dd4db0601ff3cb67b7128a0818dbb863fecdf527 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 8 Feb 2013 10:45:59 -0500
Subject: [PATCH 19/29] Remove generated files
---
.../capa/capa/tests/test_files/js/.gitignore | 4 +
.../test_files/js/test_problem_display.js | 49 ------------
.../test_files/js/test_problem_generator.js | 29 -------
.../test_files/js/test_problem_grader.js | 50 ------------
.../capa/capa/tests/test_files/js/xproblem.js | 78 -------------------
5 files changed, 4 insertions(+), 206 deletions(-)
create mode 100644 common/lib/capa/capa/tests/test_files/js/.gitignore
delete mode 100644 common/lib/capa/capa/tests/test_files/js/test_problem_display.js
delete mode 100644 common/lib/capa/capa/tests/test_files/js/test_problem_generator.js
delete mode 100644 common/lib/capa/capa/tests/test_files/js/test_problem_grader.js
delete mode 100644 common/lib/capa/capa/tests/test_files/js/xproblem.js
diff --git a/common/lib/capa/capa/tests/test_files/js/.gitignore b/common/lib/capa/capa/tests/test_files/js/.gitignore
new file mode 100644
index 0000000000..d2910668f2
--- /dev/null
+++ b/common/lib/capa/capa/tests/test_files/js/.gitignore
@@ -0,0 +1,4 @@
+test_problem_display.js
+test_problem_generator.js
+test_problem_grader.js
+xproblem.js
\ No newline at end of file
diff --git a/common/lib/capa/capa/tests/test_files/js/test_problem_display.js b/common/lib/capa/capa/tests/test_files/js/test_problem_display.js
deleted file mode 100644
index b61569acea..0000000000
--- a/common/lib/capa/capa/tests/test_files/js/test_problem_display.js
+++ /dev/null
@@ -1,49 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var MinimaxProblemDisplay, root,
- __hasProp = {}.hasOwnProperty,
- __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
-
- MinimaxProblemDisplay = (function(_super) {
-
- __extends(MinimaxProblemDisplay, _super);
-
- function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
- this.state = state;
- this.submission = submission;
- this.evaluation = evaluation;
- this.container = container;
- this.submissionField = submissionField;
- this.parameters = parameters != null ? parameters : {};
- MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
- }
-
- MinimaxProblemDisplay.prototype.render = function() {};
-
- MinimaxProblemDisplay.prototype.createSubmission = function() {
- var id, value, _ref, _results;
- this.newSubmission = {};
- if (this.submission != null) {
- _ref = this.submission;
- _results = [];
- for (id in _ref) {
- value = _ref[id];
- _results.push(this.newSubmission[id] = value);
- }
- return _results;
- }
- };
-
- MinimaxProblemDisplay.prototype.getCurrentSubmission = function() {
- return this.newSubmission;
- };
-
- return MinimaxProblemDisplay;
-
- })(XProblemDisplay);
-
- root = typeof exports !== "undefined" && exports !== null ? exports : this;
-
- root.TestProblemDisplay = TestProblemDisplay;
-
-}).call(this);
diff --git a/common/lib/capa/capa/tests/test_files/js/test_problem_generator.js b/common/lib/capa/capa/tests/test_files/js/test_problem_generator.js
deleted file mode 100644
index 4b1d133723..0000000000
--- a/common/lib/capa/capa/tests/test_files/js/test_problem_generator.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var TestProblemGenerator, root,
- __hasProp = {}.hasOwnProperty,
- __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
-
- TestProblemGenerator = (function(_super) {
-
- __extends(TestProblemGenerator, _super);
-
- function TestProblemGenerator(seed, parameters) {
- this.parameters = parameters != null ? parameters : {};
- TestProblemGenerator.__super__.constructor.call(this, seed, this.parameters);
- }
-
- TestProblemGenerator.prototype.generate = function() {
- this.problemState.value = this.parameters.value;
- return this.problemState;
- };
-
- return TestProblemGenerator;
-
- })(XProblemGenerator);
-
- root = typeof exports !== "undefined" && exports !== null ? exports : this;
-
- root.generatorClass = TestProblemGenerator;
-
-}).call(this);
diff --git a/common/lib/capa/capa/tests/test_files/js/test_problem_grader.js b/common/lib/capa/capa/tests/test_files/js/test_problem_grader.js
deleted file mode 100644
index 80d7ad1690..0000000000
--- a/common/lib/capa/capa/tests/test_files/js/test_problem_grader.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var TestProblemGrader, root,
- __hasProp = {}.hasOwnProperty,
- __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
-
- TestProblemGrader = (function(_super) {
-
- __extends(TestProblemGrader, _super);
-
- function TestProblemGrader(submission, problemState, parameters) {
- this.submission = submission;
- this.problemState = problemState;
- this.parameters = parameters != null ? parameters : {};
- TestProblemGrader.__super__.constructor.call(this, this.submission, this.problemState, this.parameters);
- }
-
- TestProblemGrader.prototype.solve = function() {
- return this.solution = {
- 0: this.problemState.value
- };
- };
-
- TestProblemGrader.prototype.grade = function() {
- var allCorrect, id, value, valueCorrect, _ref;
- if (!(this.solution != null)) {
- this.solve();
- }
- allCorrect = true;
- _ref = this.solution;
- for (id in _ref) {
- value = _ref[id];
- valueCorrect = this.submission != null ? value === this.submission[id] : false;
- this.evaluation[id] = valueCorrect;
- if (!valueCorrect) {
- allCorrect = false;
- }
- }
- return allCorrect;
- };
-
- return TestProblemGrader;
-
- })(XProblemGrader);
-
- root = typeof exports !== "undefined" && exports !== null ? exports : this;
-
- root.graderClass = TestProblemGrader;
-
-}).call(this);
diff --git a/common/lib/capa/capa/tests/test_files/js/xproblem.js b/common/lib/capa/capa/tests/test_files/js/xproblem.js
deleted file mode 100644
index 55a469f7c1..0000000000
--- a/common/lib/capa/capa/tests/test_files/js/xproblem.js
+++ /dev/null
@@ -1,78 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
-
- XProblemGenerator = (function() {
-
- function XProblemGenerator(seed, parameters) {
- this.parameters = parameters != null ? parameters : {};
- this.random = new MersenneTwister(seed);
- this.problemState = {};
- }
-
- XProblemGenerator.prototype.generate = function() {
- return console.error("Abstract method called: XProblemGenerator.generate");
- };
-
- return XProblemGenerator;
-
- })();
-
- XProblemDisplay = (function() {
-
- function XProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
- this.state = state;
- this.submission = submission;
- this.evaluation = evaluation;
- this.container = container;
- this.submissionField = submissionField;
- this.parameters = parameters != null ? parameters : {};
- }
-
- XProblemDisplay.prototype.render = function() {
- return console.error("Abstract method called: XProblemDisplay.render");
- };
-
- XProblemDisplay.prototype.updateSubmission = function() {
- return this.submissionField.val(JSON.stringify(this.getCurrentSubmission()));
- };
-
- XProblemDisplay.prototype.getCurrentSubmission = function() {
- return console.error("Abstract method called: XProblemDisplay.getCurrentSubmission");
- };
-
- return XProblemDisplay;
-
- })();
-
- XProblemGrader = (function() {
-
- function XProblemGrader(submission, problemState, parameters) {
- this.submission = submission;
- this.problemState = problemState;
- this.parameters = parameters != null ? parameters : {};
- this.solution = null;
- this.evaluation = {};
- }
-
- XProblemGrader.prototype.solve = function() {
- return console.error("Abstract method called: XProblemGrader.solve");
- };
-
- XProblemGrader.prototype.grade = function() {
- return console.error("Abstract method called: XProblemGrader.grade");
- };
-
- return XProblemGrader;
-
- })();
-
- root = typeof exports !== "undefined" && exports !== null ? exports : this;
-
- root.XProblemGenerator = XProblemGenerator;
-
- root.XProblemDisplay = XProblemDisplay;
-
- root.XProblemGrader = XProblemGrader;
-
-}).call(this);
From 33a3d5dbb7c30fc44c305a3b4370bfcb5da706cb Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Fri, 8 Feb 2013 10:49:43 -0500
Subject: [PATCH 20/29] Make it so that cms doesn't try to run collectstatic on
course content
---
cms/envs/common.py | 7 -------
1 file changed, 7 deletions(-)
diff --git a/cms/envs/common.py b/cms/envs/common.py
index ef7a4f43fa..30aac6ea01 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -165,13 +165,6 @@ STATICFILES_DIRS = [
# This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images")
]
-if os.path.isdir(GITHUB_REPO_ROOT):
- STATICFILES_DIRS += [
- # TODO (cpennington): When courses aren't loaded from github, remove this
- (course_dir, GITHUB_REPO_ROOT / course_dir)
- for course_dir in os.listdir(GITHUB_REPO_ROOT)
- if os.path.isdir(GITHUB_REPO_ROOT / course_dir)
- ]
# Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
From 96d119965c1d6219fe1e1407b2ffdf0cc070b477 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 7 Feb 2013 23:18:58 -0500
Subject: [PATCH 21/29] limit staff debug verbosity to locations of specific
categories
---
lms/templates/staff_problem_info.html | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html
index 61cda0c52b..9324445dd1 100644
--- a/lms/templates/staff_problem_info.html
+++ b/lms/templates/staff_problem_info.html
@@ -1,5 +1,6 @@
${module_content}
-%if edit_link:
+%if location.category in ['problem','video','html']:
+% if edit_link:
@@ -74,3 +75,5 @@ category = ${category | h}
'user': '${user}'
});
+%endif
+
From 61f59918858f98754fd793bf1cf171130910f5a4 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 8 Feb 2013 12:15:09 -0500
Subject: [PATCH 22/29] 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.
-
+
+
+
ASSOCIATE LEGAL COUNSEL
@@ -67,133 +87,375 @@
Requirements:
-
JD from an accredited law school
-
Massachusetts bar admission required
-
2-3 years of transactional experience at a major law firm and/or as an in-house counselor
-
Substantial IP licensing experience
-
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.
-
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
-
Experience with higher education preferred but not required
-
-
If you are interested in this position, please send an email to jobs@edx.org.
-
+
+
JD from an accredited law school
+
Massachusetts bar admission required
+
2-3 years of transactional experience at a major law firm and/or as an in-house counselor
+
Substantial IP licensing experience
+
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.
+
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
+
Experience with higher education preferred but not required
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
+
+
DIRECTOR OF EDUCATIONAL SERVICES
+
The edX Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:
+
+
Delivering 20 new courses in 2013 in collaboration with the partner Universities
+
+
Reporting to the Director of Educational Services are the Video production team, responsible for post-production of Course Video. The Director must understand how to balance artistic quality and learning objectives, and reduce production time so that video capabilities are readily accessible and at reasonable costs.
+
Reporting to the Director are a small team of Program Managers, who are responsible for managing the day to day of course production and operations. The Director must be experienced in capacity planning and operations, understand how to deploy lean collaboration and able to build alliances inside edX and the University. In conjunction with the Program Managers, the Director of Educational Services will supervise the collection of research, the retrospectives with Professors and the assembly of best practices in course production and operations. The three key deliverables are the use of a well-defined lean process for onboarding Professors, the development of tracking tools, and assessment of effectiveness of Best Practices.
+
Also reporting to the Director of Education Services are content engineers and Course Fellows, skilled in the development of edX assessments. The Director of Educational Services will also be responsible for communicating to the VP of Engineering requirements for new types of course assessments. Course Fellows are extremely talented Ph.D.’s who work directly with the Professors to define and develop assessments and course curriculum.
+
+
+
Training and Onboarding of 30 Partner Universities and Affiliates
+
+
The edX Director of Educational Services is responsible for building out the Training capabilities and delivery mechanisms for onboarding Professors at partner Universities. The edX Director must build out both the Training Team and the curriculum. Training will be delivered in both online courses, self-paced formats, and workshops. The training must cover a curriculum that enables partner institutions to be completely independent. Additionally, partner institutions should be engaged to contribute to the curriculum and partner with edX in the delivery of the material. The curriculum must exemplify the best in online learning, so the Universities are inspired to offer the kind of learning they have experienced in their edX Training.
+
Expand and extend the education goals of the partner Universities by operationalizing best practices.
+
Engage with University Boards to design and define the success that the technology makes possible.
+
+
+
Growing the Team, Growing the Business
+
+
The edX Director will be responsible for working with Business Development to identify revenue opportunities and build profitable plans to grow the business and grow the team.
+
Maintain for-profit nimbleness in an organization committed to non-profit ideals.
+
Design scalable solutions to opportunities revealed by technical innovations
+
+
+
Integrating a Strong Team within Strong Organization
+
+
Connect organization’s management and University leadership with consistent and high quality expectations and deployment
+
Integrate with a highly collaborative leadership team to maximize talents of the organization
+
Successfully escalate issues within and beyond the organization to ensure the best possible educational outcome for students and Universities
+
+
+
+
Skills:
+
+
Ability to lead simultaneous initiatives in an entrepreneurial culture
Experience with deploying educational technologies on a large scale
+
Develop team skills in a ferociously intelligent group
+
Fan the enthusiasm of the partner Universities when the enormity of the transition they are facing becomes intimidating
+
Encourage creativity to allow the technology to provoke pedagogical possibilities that brick and mortar classes have precluded.
+
Lean and Agile thinking and training. Experienced in scrum or kanban.
+
Design and deliver hiring/development plans which meet rapidly changing skill needs.
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
+
MANAGER OF TRAINING SERVICES
+
The Manager of Training Services is an integral member of the edX team, a leader who is also a doer, working hands-on in the development and delivery of edX’s training portfolio. Reporting to the Director of Educational Services, the manager will be a strategic thinker, providing leadership and vision in the development of world-class training solutions tailored to meet the diverse needs of edX Universities, partners and stakeholders
+
Responsibilities:
+
+
Working with the Director of Educational Services, create and manage a world-class training program that includes in-person workshops and online formats such as self-paced courses, and webinars.
+
Work across a talented team of product developers, video producers and content experts to identify training needs and proactively develop training curricula for new products and services as they are deployed.
+
Develop the means for sharing and showcasing edX best practices for both internal and external audiences.
+
Apply sound instructional design theory and practice in the development of all edX training resources.
+
Work with program managers to develop training benchmarks and Key Performance Indicators. Monitor progress and proactively make adjustments as necessary.
+
Collaborate with product development on creating documentation and user guides.
+
Provide on-going evaluation of the effectiveness of edX training programs.
+
Assist in the revision/refinement of training curricula and resources.
+
Grow a train-the-trainer organization with edX partners, identifying expert edX users to provide on-site peer assistance.
+
Deliver internal and external trainings.
+
Coordinate with internal teams to ensure appropriate preparation for trainings, and follow-up after delivery.
+
Maintain training reporting database and training records.
+
Produce training evaluation reports, training support plans, and training improvement plans.
+
Quickly become an expert on edX’s standards, procedures and tools.
+
Stay current on emerging trends in eLearning, platform support and implementation strategy.
+
+
Requirements:
+
+
Minimum of 5-7 years experience developing and delivering educational training, preferably in an educational technology organization.
+
Lean and Agile thinking and training. Experienced in Scrum or kanban.
+
Excellent interpersonal skills including proven presentation and facilitation skills.
+
Strong oral and written communication skills.
+
Proven experience with production and delivery of online training programs that utilize asychronous and synchronous delivery mechanisms.
+
Flexibility to work on a variety of initiatives; prior startup experience preferred.
+
Outstanding work ethic, results-oriented, and creative/innovative style.
+
Proactive, optimistic approach to problem solving.
+
Commitment to constant personal and organizational improvement.
+
Willingness to travel to partner sites as needed.
+
Bachelors required, Master’s in Education, organizational learning, or other related field preferred.
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
-
INSTRUCTIONAL DESIGNER — CONTRACT OPPORTUNITY
-
The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.
-
Responsibilities:
-
-
Work with the video production team, product managers and course staff on the implementation of instructional design approaches in the development of media and other course materials.
-
Based on course staff and faculty input, articulate learning objectives and align them to design strategies and assessments.
-
Develop flipped classroom instructional strategies in coordination with community college faculty.
-
Produce clear and instructionally effective copy, instructional text, and audio and video scripts
-
Identify and deploy instructional design best practices for edX course staff and faculty as needed.
-
Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management.
-
Serve as a liaison to instructional design teams based at X universities.
-
Consult on peer review processes to be used by learners in selected courses.
-
Ability to apply game-based learning theory and design into selected courses as appropriate.
-
Use learning analytics and metrics to inform course design and revision process.
-
Collaborate with key research and learning sciences stakeholders at edX and partner institutions for the development of best practices for MOOC teaching and learning and course design.
-
Support the development of pilot courses and modules used for sponsored research initiatives.
-
-
Qualifications:
-
-
Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.
-
Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential. Ability to meet deadlines and manage expectations of constituents.
-
Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.
-
Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.
-
-
Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.
-
If you are interested in this position, please send an email to jobs@edx.org.
-
-
-
-
-
-
MEMBER SERVICES MANAGER
-
The edX Member Services Manager is responsible for both defining support best practices and directly supporting edX members by handling or routing issues that come in from our websites, email and social media tools. We are looking for a passionate person to help us define and own this experience. While this is a Manager level position, we see this candidate quickly moving through the ranks, leading a larger team of employees over time. This staff member will be running our fast growth support organization.
+
INSTRUCTIONAL DESIGNER
+
The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.
Responsibilities:
-
Define and rollout leading technology, best practices and policies to support a growing team of member care representatives.
-
Provide reports and visibility into member care metrics.
-
Identify a staffing plan that mirrors growth and work to grow the team with passionate, member-first focused staff.
-
Manage member services staff to predefined service levels.
-
Resolve issues according to edX policies; escalates non-routine issues.
-
Educate members on edX policies and getting started
-
May assist new members with edX procedures and processing registration issues.
-
Provides timely follow-up and resolution to issues.
-
A passion for doing the right thing - at edX the member is always our top priority
-
+
Work with the video production team, product managers and course staff on the implementation of instructional design approaches in the development of media and other course materials.
+
Based on course staff and faculty input, articulate learning objectives and align them to design strategies and assessments.
+
Develop flipped classroom instructional strategies in coordination with community college faculty.
+
Produce clear and instructionally effective copy, instructional text, and audio and video scripts
+
Identify and deploy instructional design best practices for edX course staff and faculty as needed.
+
Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management.
+
Serve as a liaison to instructional design teams based at our partner Universities.
+
Consult on peer review processes to be used by learners in selected courses.
+
Ability to apply game-based learning theory and design into selected courses as appropriate.
+
Use learning analytics and metrics to inform course design and revision process.
+
Collaborate with key research and learning sciences stakeholders at edX and partner institutions for the development of best practices for MOOC teaching and learning and course design.
+
Support the development of pilot courses and modules used for sponsored research initiatives.
Qualifications:
-
5-8 years in a call center or support team management
-
Exemplary customer service skills
-
Experience in creating and rolling out support/service best practices
-
Solid computer skills – must be fluent with desktop applications and have a basic understanding of web technologies (i.e. basic HTML)
-
Problem solving - the individual identifies and resolves problems in a timely manner, gathers and analyzes information skillfully and maintains confidentiality.
-
Interpersonal skills - the individual maintains confidentiality, remains open to others' ideas and exhibits willingness to try new things.
-
Oral communication - the individual speaks clearly and persuasively in positive or negative situations and demonstrates group presentation skills.
-
Written communication – the individual edits work for spelling and grammar, presents numerical data effectively and is able to read and interpret written information.
-
Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
-
Dependability - the individual is consistently at work and on time, follows instructions, responds to management direction and solicits feedback to improve performance.
-
College degree
+
Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.
+
Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.
Ability to meet deadlines and manage expectations of constituents.
+
Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.
+
Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.
+
+
+
Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
+
+
PROGRAM MANAGER
+
edX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.
+
Responsibilities:
+
+
Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple
teams engaged in the production of the courses assigned to them.
+
Train partners and drive best practices adoption. PMs train course staff from partner institutions and help them adopt best practices for workflow and tools.
+
Build capacity. Mentor staff at partner institutions, train the trainers that help them scale their course production ability.
+
Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and
Kanban, and are Lean thinkers and educators.
+
Improve workflows. PMs are responsible for carefully assessing the methods and outputs of each course and adjusting them to take best advantage of available resources.
+
Encourage innovation. Spark creativity in course teams to build new courses that could never be produced in brick and mortar settings.
+
+
Qualifications:
+
+
Bachelor's Degree. Master's Degree preferred.
+
At least 2 years of experience working with University faculty and administrators.
+
Proven record of successful Scrum or Kanban project management, including use of project management tools.
+
Ability to create processes that systematically provide solutions to open ended challenges.
+
Excellent interpersonal and communication (written and verbal) skills, the ability to define and solve technical, process and organizational problems, and time management skills.
+
Proactive, optimistic approach to problem solving.
+
Commitment to constant personal and organizational improvement.
+
+
+
Preferred qualifications
+
+
Some teaching experience,
+
Online course design and development experience.
+
Experience with Lean and Agile thinking and processes.
+
Experience with online collaboration tools
+
Familiarity with video production.
+
Basic HTML, XML, programming skills.
+
If you are interested in this position, please send an email to jobs@edx.org.
-
+
-
DIRECTOR OF PR AND COMMUNICATIONS
-
The edX Director of PR & Communications is responsible for creating and executing all PR strategy and providing company-wide leadership to help create and refine the edX core messages and identity as the revolutionary global leader in both on-campus and worldwide education. The Director will design and direct a communications program that conveys cohesive and compelling information about edX's mission, activities, personnel and products while establishing a distinct identity for edX as the leader in online education for both students and learning institutions.
+
PROJECT MANAGER (PMO)
+
As a fast paced, rapidly growing organization serving the evolving online higher education market, edX maximizes its talents and resources. To help make the most of this unapologetically intelligent and dedicated team, we seek a project manager to increase the accuracy of our resource and schedule estimates and our stakeholder satisfaction.
Responsibilities:
-
Develop and execute goals and strategy for a comprehensive external and internal communications program focused on driving student engagement around courses and institutional adoption of the edX learning platform.
-
Work with media, either directly or through our agency of record, to establish edX as the industry leader in global learning.
-
Work with key influencers including government officials on a global scale to ensure the edX mission, content and tools are embraced and supported worldwide.
-
Work with marketing colleagues to co-develop and/or monitor and evaluate the content and delivery of all communications messages and collateral.
-
Initiate and/or plan thought leadership events developed to heighten target-audience awareness; participate in meetings and trade shows
-
Conduct periodic research to determine communications benchmarks
-
Inform employees about edX's vision, values, policies, and strategies to enable them to perform their jobs efficiently and drive morale.
-
Work with and manage existing communications team to effectively meet strategic goals.
+
Coordinate multiple projects to bring Courses, Software Product and Marketing initiatives to market, all of which are related, which have both dedicated and shared resources.
+
Provide, at a moment’s notice, the state of development, so that priorities can be enforced or reset, so that future expectations can be set accurately.
+
Develop lean processes that supports a wide variety of efforts which draw on a shared resource pool.
+
Develop metrics on resource use that support the leadership team in optimizing how they respond to unexpected challenges and new opportunities.
+
Accurately and clearly escalate only those issues which need escalation for productive resolution. Assist in establishing consensus for all other issues.
+
Advise the team on best practices, whether developed internally or as industry standards.
+
Recommend to the leadership team how to re-deploy key resources to better match stated priorities.
+
Help the organization deliver on its commitments with more consistency and efficiency. Allow the organization to respond to new opportunities with more certainty in its ability to forecast resource needs.
+
Select and maintain project management tools for Scrum and Kanban that can serve as the standard for those we use with our partners.
+
Forecast future resource needs given the strategic direction of the organization.
-
Qualifications:
+
Skills:
-
Ten years of experience in PR and communications
-
Ability to work creatively and provide company-wide leadership in a fast-paced, dynamic start-up environment required
-
Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
-
Experience in working in successful consumer-focused startups preferred
-
PR agency experience in setting strategy for complex multichannel, multinational organizations a plus.
-
Extensive writing experience and simply amazing oral, written, and interpersonal communications skills
-
B.A./B.S. in communications or related field
+
Bachelor’s degree or higher
+
Exquisite communication skills, especially listening
+
Inexhaustible attention to detail with the ability to let go of perfection
+
Deep commitment to Lean project management, including a dedication to its best intentions not just its rituals
+
Sense of humor and humility
+
Ability to hold on to the important in the face of the urgent
If you are interested in this position, please send an email to jobs@edx.org.
-
+
+
+
+
+
+
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.
+
+
Responsibilities:
+
+
Assess users’ needs, whether students, Professors or Universities.
+
Research markets and competitors to provide data driven decisions.
+
Work with multiple engineering teams, through consensus and with data-backed arguments, in order to provide technology which defines the state of the art for online courses.
+
Repeatedly build and launch new products and services, complete with the training, documentation and metrics needed to enhance the already impressive brands of the edX partner institutions.
+
Establish the vision and future direction of the product with input from edX leadership and guidance from partner organizations.
+
Work in a lean organization, committed to Scrum and Kanban.
+
+
Qualifications:
+
+
Bachelor’s degree or higher in a Technical Area
+
MBA or Masters in Design preferred
+
Proven ability to develop and implement strategy
+
Exquisite organizational skills
+
Deep analytical skills
+
Social finesse and business sense
+
Scrum, Kanban
+
Infatuation with technology, in all its frustrating and fragile complexity
+
Top flight communication skills, oral and written, with teams which are centrally located and spread all over the world.
+
Personal commitment and experience of the transformational possibilities of higher education
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
+
+
CONTENT ENGINEER
+
Content engineers help create the technology for specific courses. The tasks include:
+
+
Developing of course-specific user-facing elements, such as the circuit editor and simulator.
+
Integrating course materials into courses
+
Creating programs to grade questions designed with complex technical features
+
Knowledge of Python, XML, and/or JavaScript is desired. Strong interest and background in pedagogy and education is desired as well.
+
Building course components in straight XML or through our course authoring tool, edX Studio.
+
Assisting University teams and in house staff take advantage of new course software, including designing and developing technical refinements for implementation.
+
Pushing content to production servers predictably and cleanly.
+
Sending high volumes of course email adhering to email engine protocols.
+
+
Qualifications:
+
+
Bachelor’s degree or higher
+
Thorough knowledge of Python, DJango, XML,HTML, CSS , Javascript and backbone.js
+
Ability to work on multiple projects simultaneously without splintering
+
Tactfully escalate conflicting deadlines or priorities only when needed. Otherwise help the team members negotiate a solution.
+
Unfailing attention to detail, especially the details the course teams have seen so often they don’t notice them anymore.
+
Readily zoom from the big picture to the smallest course component to notice when typos, inconsistencies or repetitions have unknowingly crept in
+
Curiosity to step into the shoes of an online student working to master the course content.
+
Solid interpersonal skills, especially good listening
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
+
+
DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER
+
In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.
+
Responsibilities:
+
+
Define and implement software design standards that make the open source community most welcome and productive.
+
Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to
make the edX platform the world’s best learning platform.
+
Help the organization recognize the benefits and limitations inherent in open source solutions.
+
Establish best practices and key tool usage, especially those based on industry standards.
+
Provide visibility for the leadership team into the concerns and challenges faced by the open source community.
+
Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.
+
Maximize the good code design coming from the open source community.
+
Provide the wit and firmness that the community needs to channel their energy productively.
+
Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.
+
Shorten lines of communication and build trust across entire team
+
+
Qualifications:
+
+
+
Bachelors, preferably Masters in Computer Science
+
Solid communication skills, especially written
+
Committed to Agile practice, Scrum and Kanban
+
Charm and humor
+
Deep familiarity with Open Source, participant and contributor
+
Python, Django, Javascript
+
Commitment to support your technical recommendations, both within and beyond the organization.
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
+
+
+
+
SOFTWARE ENGINEER
+
edX is looking for engineers who can contribute to its Open Source learning platform. We are a small team with a startup, lean culture, committed to building open-source software that scales and dramatically changes the face of education. Our ideal candidates are hands on developers who understand how to build scalable, service based systems, preferably in Python and have a proven track record of bringing their ideas to market. We are looking for engineers with all levels of experience, but you must be a proven leader and outstanding developer to work at edX.
+
+
There are a number of projects for which we are recruiting engineers:
+
+
Learning Management System: We are developing an Open Source Standard that allows for the creation of instructional plug-ins and assessments in our platform. You must have a deep interest in semantics of learning, and able to build services at scale.
+
+
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.
+
+
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.
+
+
Requirements:
+
+
Expert Python Developer or familiar with 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
+
Committed to Documentation best practices so your code can be consumed in an open source environment
+
Contributor to or consumer of Open Source Frameworks
+
BS in Computer Science from top-tier institution
+
Acknowledged by peers as a technology leader
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
Positions
How to Apply
E-mail your resume, coverletter and any other materials to jobs@edx.org
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.
- 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 26/29] 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 27/29] 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 28/29] 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 29/29] 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: