From 9b3d7efb3f60d3b0913c49e01847f9be38892b6b Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Wed, 30 Jan 2013 18:14:53 -0500
Subject: [PATCH 01/58] add first pass at testcenter exam
---
lms/djangoapps/courseware/views.py | 135 ++++++++++++++++
lms/templates/courseware/testcenter_exam.html | 152 ++++++++++++++++++
lms/templates/main_nonav.html | 46 ++++++
lms/urls.py | 4 +
4 files changed, 337 insertions(+)
create mode 100644 lms/templates/courseware/testcenter_exam.html
create mode 100644 lms/templates/main_nonav.html
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 5d65d7c632..c7838156cb 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -2,6 +2,7 @@ import logging
import urllib
from functools import partial
+from time import time
from django.conf import settings
from django.core.context_processors import csrf
@@ -297,6 +298,140 @@ def index(request, course_id, chapter=None, section=None,
return result
+@login_required
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+def testcenter_exam(request, course_id, chapter, section):
+ """
+ Displays only associated content. If course, chapter,
+ and section are all specified, renders the page, or returns an error if they
+ are invalid.
+
+ Returns an error if these are not all specified and correct.
+
+ Arguments:
+
+ - request : HTTP request
+ - course_id : course id (str: ORG/course/URL_NAME)
+ - chapter : chapter url_name (str)
+ - section : section url_name (str)
+
+ Returns:
+
+ - HTTPresponse
+ """
+ course = get_course_with_access(request.user, course_id, 'load', depth=2)
+ staff_access = has_access(request.user, course, 'staff')
+ registered = registered_for_course(course, request.user)
+ if not registered:
+ log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
+ raise # error
+ try:
+ student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
+ course.id, request.user, course, depth=2)
+
+ # Has this student been in this course before?
+ # first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
+
+ # Load the module for the course
+ course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id)
+ if course_module is None:
+ log.warning('If you see this, something went wrong: if we got this'
+ ' far, should have gotten a course module for this user')
+ # return redirect(reverse('about_course', args=[course.id]))
+ raise # error
+
+ if chapter is None:
+ # return redirect_to_course_position(course_module, first_time)
+ raise # error
+
+ # BW: add this test earlier, and remove later clause
+ if section is None:
+ # return redirect_to_course_position(course_module, first_time)
+ raise # error
+
+ context = {
+ 'csrf': csrf(request)['csrf_token'],
+ # 'accordion': render_accordion(request, course, chapter, section),
+ 'COURSE_TITLE': course.title,
+ 'course': course,
+ 'init': '',
+ 'content': '',
+ 'staff_access': staff_access,
+ 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
+ }
+
+ chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
+ if chapter_descriptor is not None:
+ instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
+ save_child_position(course_module, chapter, instance_module)
+ else:
+ raise Http404
+
+ chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
+ if chapter_module is None:
+ # User may be trying to access a chapter that isn't live yet
+ raise Http404
+
+ section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
+ if section_descriptor is None:
+ # Specifically asked-for section doesn't exist
+ raise Http404
+
+ # Load all descendents of the section, because we're going to display its
+ # html, which in general will need all of its children
+ section_module = get_module(request.user, request, section_descriptor.location,
+ student_module_cache, course.id, position=None, depth=None)
+ if section_module is None:
+ # User may be trying to be clever and access something
+ # they don't have access to.
+ raise Http404
+
+ # Save where we are in the chapter
+# instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
+# save_child_position(chapter_module, section, instance_module)
+
+
+ context['content'] = section_module.get_html()
+
+ # figure out when the exam should end. Going forward, this is determined by getting a "normal"
+ # duration from the test, then doing some math to modify the duration based on accommodations,
+ # and then use that value as the end. Once we have calculated this, it should be sticky -- we
+ # use the same value for future requests, unless it's a tester.
+
+ # Let's try 600s for now...
+ context['end_date'] = (time() + 600) * 1000
+
+ result = render_to_response('courseware/testcenter_exam.html', context)
+ except Exception as e:
+ if isinstance(e, Http404):
+ # let it propagate
+ raise
+
+ # In production, don't want to let a 500 out for any reason
+ if settings.DEBUG:
+ raise
+ else:
+ log.exception("Error in exam view: user={user}, course={course},"
+ " chapter={chapter} section={section}"
+ "position={position}".format(
+ user=request.user,
+ course=course,
+ chapter=chapter,
+ section=section
+ ))
+ try:
+ result = render_to_response('courseware/courseware-error.html',
+ {'staff_access': staff_access,
+ 'course' : course})
+ except:
+ # Let the exception propagate, relying on global config to at
+ # at least return a nice error message
+ log.exception("Error while rendering courseware-error page")
+ raise
+
+ return result
+
@ensure_csrf_cookie
def jump_to(request, course_id, location):
'''
diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html
new file mode 100644
index 0000000000..66adfefcff
--- /dev/null
+++ b/lms/templates/courseware/testcenter_exam.html
@@ -0,0 +1,152 @@
+<%inherit file="/main_nonav.html" />
+<%namespace name='static' file='/static_content.html'/>
+<%block name="bodyclass">courseware ${course.css_class}%block>
+<%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/58] rename testcenter_exam to timed_exam, and read duration
from metadata (policy.json)
---
lms/djangoapps/courseware/views.py | 17 ++++++++++++-----
lms/templates/courseware/testcenter_exam.html | 3 ++-
lms/urls.py | 6 +++---
3 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index c7838156cb..1ac7cebd4b 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -301,7 +301,7 @@ def index(request, course_id, chapter=None, section=None,
@login_required
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-def testcenter_exam(request, course_id, chapter, section):
+def timed_exam(request, course_id, chapter, section):
"""
Displays only associated content. If course, chapter,
and section are all specified, renders the page, or returns an error if they
@@ -387,20 +387,27 @@ def testcenter_exam(request, course_id, chapter, section):
# they don't have access to.
raise Http404
- # Save where we are in the chapter
+ # Save where we are in the chapter NOT!
# instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
# save_child_position(chapter_module, section, instance_module)
context['content'] = section_module.get_html()
- # figure out when the exam should end. Going forward, this is determined by getting a "normal"
+ # figure out when the timed exam should end. Going forward, this is determined by getting a "normal"
# duration from the test, then doing some math to modify the duration based on accommodations,
# and then use that value as the end. Once we have calculated this, it should be sticky -- we
# use the same value for future requests, unless it's a tester.
+
+ # get value for duration from the section's metadata:
+ if 'duration' not in section_descriptor.metadata:
+ raise Http404
+
+ # for now, assume that the duration is set as an integer value, indicating the number of seconds:
+ duration = int(section_descriptor.metadata.get('duration'))
- # Let's try 600s for now...
- context['end_date'] = (time() + 600) * 1000
+ # This value should be UTC time as number of milliseconds since epoch.
+ context['end_date'] = (time() + duration) * 1000
result = render_to_response('courseware/testcenter_exam.html', context)
except Exception as e:
diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html
index 66adfefcff..638778f7cd 100644
--- a/lms/templates/courseware/testcenter_exam.html
+++ b/lms/templates/courseware/testcenter_exam.html
@@ -67,7 +67,8 @@
return ( num < 10 ? "0" : "" ) + num;
}
- // set the end time when the template is rendered
+ // set the end time when the template is rendered.
+ // This value should be UTC time as number of milliseconds since epoch.
var endTime = new Date(${end_date});
var currentTime = new Date();
var remaining_secs = Math.floor((endTime - currentTime)/1000);
diff --git a/lms/urls.py b/lms/urls.py
index 2c5db07d00..021079333a 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -217,9 +217,9 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"),
- # testcenter exam:
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/testcenter_exam/(?P[^/]*)/(?P[^/]*)/$',
- 'courseware.views.testcenter_exam', name="testcenter_exam"),
+ # timed exam:
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/timed_exam/(?P[^/]*)/(?P[^/]*)/$',
+ 'courseware.views.timed_exam', name="timed_exam"),
#Inside the course
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/$',
From 9d98b7055de26a4f70f0e987c8f682786564441d Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Fri, 1 Feb 2013 17:52:14 -0500
Subject: [PATCH 03/58] add course navigation back into timed exam. Add
initial timer styling.
---
lms/djangoapps/courseware/views.py | 12 +++++---
lms/static/sass/course.scss | 2 +-
lms/static/sass/course/layout/_timer.scss | 11 +++++++
lms/templates/courseware/testcenter_exam.html | 29 +++++++++++++++++--
lms/urls.py | 6 ++++
5 files changed, 52 insertions(+), 8 deletions(-)
create mode 100644 lms/static/sass/course/layout/_timer.scss
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 1ac7cebd4b..47942f3a63 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -352,7 +352,6 @@ def timed_exam(request, course_id, chapter, section):
context = {
'csrf': csrf(request)['csrf_token'],
- # 'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
@@ -361,6 +360,11 @@ def timed_exam(request, course_id, chapter, section):
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
}
+ # in general, we may want to disable accordion display on timed exams.
+ provide_accordion = True
+ if provide_accordion:
+ context['accordion'] = render_accordion(request, course, chapter, section)
+
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None:
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
@@ -387,9 +391,9 @@ def timed_exam(request, course_id, chapter, section):
# they don't have access to.
raise Http404
- # Save where we are in the chapter NOT!
-# instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
-# save_child_position(chapter_module, section, instance_module)
+ # Save where we are in the chapter:
+ instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
+ save_child_position(chapter_module, section, instance_module)
context['content'] = section_module.get_html()
diff --git a/lms/static/sass/course.scss b/lms/static/sass/course.scss
index e900e589b2..7c2522a194 100644
--- a/lms/static/sass/course.scss
+++ b/lms/static/sass/course.scss
@@ -22,7 +22,7 @@
@import 'course/courseware/sidebar';
@import 'course/courseware/amplifier';
@import 'course/layout/calculator';
-
+@import 'course/layout/timer';
// course-specific courseware (all styles in these files should be gated by a
// course-specific class). This should be replaced with a better way of
diff --git a/lms/static/sass/course/layout/_timer.scss b/lms/static/sass/course/layout/_timer.scss
new file mode 100644
index 0000000000..01d62d87c7
--- /dev/null
+++ b/lms/static/sass/course/layout/_timer.scss
@@ -0,0 +1,11 @@
+div.timer-main {
+ position: fixed;
+ z-index: 99;
+ width: 100%;
+
+ div#timer_wrapper {
+ position: relative;
+ float: right;
+ margin-right: 10px;
+ }
+}
diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html
index 638778f7cd..d2f74ab296 100644
--- a/lms/templates/courseware/testcenter_exam.html
+++ b/lms/templates/courseware/testcenter_exam.html
@@ -78,6 +78,7 @@
// TBD...
if (remaining_secs <= 0) {
return "00:00:00";
+ // do we just set window.location = value?
}
// count down in terms of hours, minutes, and seconds:
@@ -104,19 +105,41 @@
%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 ddc8bf2b425f5b9effefc54e1092c2b9d4ba8971 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Mon, 4 Feb 2013 04:23:23 +0000
Subject: [PATCH 04/58] add instructor dashboard feature to download profile &
problem state
---
lms/djangoapps/instructor/views.py | 40 +++++++++++++++++++
.../courseware/instructor_dashboard.html | 15 +++++++
2 files changed, 55 insertions(+)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index a707506045..5ce256b275 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -383,6 +383,46 @@ def instructor_dashboard(request, course_id):
user.groups.remove(group)
track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard')
+ #----------------------------------------
+ # DataDump
+
+ elif 'Download CSV of all student profile data' in action:
+ enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
+ profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education',
+ 'mailing_address', 'goals']
+ datatable = {'header': ['username', 'email'] + profkeys}
+ def getdat(u):
+ p = u.profile
+ return [u.username, u.email] + [getattr(p,x,'') for x in profkeys]
+
+ datatable['data'] = [getdat(u) for u in enrolled_students]
+ datatable['title'] = 'Student profile data for course %s' % course_id
+ return return_csv('profiledata_%s.csv' % course_id, datatable)
+
+
+ elif 'Download CSV of all responses to problem' in action:
+ problem_to_dump = request.POST.get('problem_to_dump','')
+
+ if problem_to_dump[-4:]==".xml":
+ problem_to_dump=problem_to_dump[:-4]
+ try:
+ (org, course_name, run)=course_id.split("/")
+ module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump
+ smdat = StudentModule.objects.filter(course_id=course_id,
+ module_state_key=module_state_key)
+ smdat = smdat.order_by('student')
+ msg+="Found module to reset. "
+ except Exception as err:
+ msg+="Couldn't find module with that urlname. "
+ msg += "
%s
" % escape(err)
+ smdat = []
+
+ if smdat:
+ datatable = {'header': ['username', 'state']}
+ datatable['data'] = [ [x.student.username, x.state] for x in smdat ]
+ datatable['title'] = 'Student state for problem %s' % problem_to_dump
+ return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable)
+
#----------------------------------------
# Group management
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index 7b177c6b6c..a31ee0025e 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -64,6 +64,7 @@ function goto( mode)
Admin |
Forum Admin |
Enrollment |
+ DataDump |
Manage Groups ]
@@ -269,6 +270,20 @@ function goto( mode)
##-----------------------------------------------------------------------------
+%if modeflag.get('Data'):
+
+
+
+
+
Problem urlname:
+
+
+
+
+%endif
+
+##-----------------------------------------------------------------------------
+
%if modeflag.get('Manage Groups'):
%if instructor_access:
From 1685f302ab7721be80634ae2f68154d74d452cd2 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Mon, 4 Feb 2013 02:22:24 -0500
Subject: [PATCH 05/58] add TimerModule to courseware
---
.../migrations/0006_add_timed_module.py | 119 ++++++++++++++++++
lms/djangoapps/courseware/models.py | 88 ++++++++++++-
lms/djangoapps/courseware/views.py | 50 +++++++-
3 files changed, 245 insertions(+), 12 deletions(-)
create mode 100644 lms/djangoapps/courseware/migrations/0006_add_timed_module.py
diff --git a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
new file mode 100644
index 0000000000..89b63cf659
--- /dev/null
+++ b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'TimedModule'
+ db.create_table('courseware_timedmodule', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('module_state_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='module_id', db_index=True)),
+ ('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('accommodation_code', self.gf('django.db.models.fields.CharField')(default='NONE', max_length=12, db_index=True)),
+ ('beginning_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
+ ('ending_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
+ ('modified_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
+ ))
+ db.send_create_signal('courseware', ['TimedModule'])
+
+ # Adding unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id']
+ db.create_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id']
+ db.delete_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id'])
+
+ # Deleting model 'TimedModule'
+ db.delete_table('courseware_timedmodule')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'courseware.offlinecomputedgrade': {
+ 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'courseware.offlinecomputedgradelog': {
+ 'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'courseware.studentmodule': {
+ 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
+ 'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
+ 'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'courseware.timedmodule': {
+ 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'TimedModule'},
+ 'accommodation_code': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '12', 'db_index': 'True'}),
+ 'beginning_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'ending_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ }
+ }
+
+ complete_apps = ['courseware']
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 21ef8b3d66..bd2da02027 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -12,15 +12,12 @@ file and check it in at the same time as your model changes. To do that,
ASSUMPTIONS: modules have unique IDs, even across different module_types
"""
+from datetime import datetime, timedelta
+from calendar import timegm
+
from django.db import models
-#from django.core.cache import cache
from django.contrib.auth.models import User
-#from cache_toolbox import cache_model, cache_relation
-
-#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
-
-
class StudentModule(models.Model):
"""
Keeps student state for a particular module in a particular course.
@@ -214,3 +211,82 @@ class OfflineComputedGradeLog(models.Model):
def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created)
+
+class TimedModule(models.Model):
+ """
+ Keeps student state for a timed activity in a particular course.
+ Includes information about time accommodations granted,
+ time started, and ending time.
+ """
+ ## These three are the key for the object
+
+ # Key used to share state. By default, this is the module_id,
+ # but for abtests and the like, this can be set to a shared value
+ # for many instances of the module.
+ # Filename for homeworks, etc.
+ module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
+ student = models.ForeignKey(User, db_index=True)
+ course_id = models.CharField(max_length=255, db_index=True)
+
+ class Meta:
+ unique_together = (('student', 'module_state_key', 'course_id'),)
+
+ # For a timed activity, we are only interested here
+ # in time-related accommodations, and these should be disjoint.
+ # (For proctored exams, it is possible to have multiple accommodations
+ # apply to an exam, so they require accommodating a multi-choice.)
+ TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
+ ('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
+ ('ADD30MIN', 'Extra Time - 30 Minutes'),
+ ('DOUBLE', 'Extra Time - Double Time'),
+ )
+ accommodation_code = models.CharField(max_length=12, choices=TIME_ACCOMMODATION_CODES, default='NONE', db_index=True)
+
+ def _get_accommodated_duration(self, duration):
+ '''
+ Get duration for activity, as adjusted for accommodations.
+ Input and output are expressed in seconds.
+ '''
+ if self.accommodation_code == 'NONE':
+ return duration
+ elif self.accommodation_code == 'ADDHALFTIME':
+ # TODO: determine what type to return
+ return int(duration * 1.5)
+ elif self.accommodation_code == 'ADD30MIN':
+ return (duration + (30 * 60))
+ elif self.accommodation_code == 'DOUBLE':
+ return (duration * 2)
+
+ # store state:
+
+ beginning_at = models.DateTimeField(null=True, db_index=True)
+ ending_at = models.DateTimeField(null=True, db_index=True)
+ created_at = models.DateTimeField(auto_now_add=True, db_index=True)
+ modified_at = models.DateTimeField(auto_now=True, db_index=True)
+
+ @property
+ def has_begun(self):
+ return self.beginning_at is not None
+
+ @property
+ def has_ended(self):
+ if not self.ending_at:
+ return False
+ return self.ending_at < datetime.utcnow()
+
+ def begin(self, duration):
+ '''
+ Sets the starting time and ending time for the activity,
+ based on the duration provided (in seconds).
+ '''
+ self.beginning_at = datetime.utcnow()
+ modified_duration = self._get_accommodated_duration(duration)
+ datetime_duration = timedelta(seconds=modified_duration)
+ self.ending_at = self.beginning_at + datetime_duration
+
+ def get_end_time_in_ms(self):
+ return (timegm(self.ending_at.timetuple()) * 1000)
+
+ def __unicode__(self):
+ return '/'.join([self.course_id, self.student.username, self.module_state_key])
+
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 47942f3a63..b44887dbfd 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -2,7 +2,6 @@ import logging
import urllib
from functools import partial
-from time import time
from django.conf import settings
from django.core.context_processors import csrf
@@ -21,7 +20,7 @@ from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement)
import courseware.tabs as tabs
-from courseware.models import StudentModuleCache
+from courseware.models import StudentModuleCache, TimedModule
from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
from django_comment_client.utils import get_discussion_title
@@ -402,16 +401,55 @@ def timed_exam(request, course_id, chapter, section):
# duration from the test, then doing some math to modify the duration based on accommodations,
# and then use that value as the end. Once we have calculated this, it should be sticky -- we
# use the same value for future requests, unless it's a tester.
-
+
# get value for duration from the section's metadata:
+ # for now, assume that the duration is set as an integer value, indicating the number of seconds:
if 'duration' not in section_descriptor.metadata:
raise Http404
-
- # for now, assume that the duration is set as an integer value, indicating the number of seconds:
duration = int(section_descriptor.metadata.get('duration'))
+ # get corresponding time module, if one is present:
+ # TODO: determine what to use for module_key...
+ try:
+ timed_module = TimedModule.objects.get(student=request.user, course_id=course_id)
+
+ # if a module exists, check to see if it has already been started,
+ # and if it has already ended.
+ if timed_module.has_ended:
+ # the exam has already ended, and the student has tried to
+ # revisit the exam.
+ # TODO: determine what do we do here.
+ # For a Pearson exam, we want to go to the exit page.
+ # (Not so sure what to do in general.)
+ # Proposal: store URL in the section descriptor,
+ # along with the duration. If no such URL is set,
+ # just put up the error page,
+ raise Exception("Time expired on {}".format(timed_module))
+ elif not timed_module.has_begun:
+ # user has not started the exam, but may have an accommodation
+ # that has been granted to them.
+ # modified_duration = timed_module.get_accommodated_duration(duration)
+ # timed_module.started_at = datetime.utcnow() # time() * 1000
+ # timed_module.end_date = timed_module.
+ timed_module.begin(duration)
+ timed_module.save()
+
+ except TimedModule.DoesNotExist:
+ # no entry found. So we're starting this test
+ # without any accommodations being preset.
+ # TODO: determine what to use for module_key...
+ timed_module = TimedModule(student=request.user, course_id=course_id)
+ timed_module.begin(duration)
+ timed_module.save()
+
+
+ # the exam has already been started, and the student is returning to the
+ # exam page. Fetch the end time (in GMT) as stored
+ # in the module when it was started.
+ end_date = timed_module.get_end_time_in_ms()
+
# This value should be UTC time as number of milliseconds since epoch.
- context['end_date'] = (time() + duration) * 1000
+ context['end_date'] = end_date
result = render_to_response('courseware/testcenter_exam.html', context)
except Exception as e:
From f8b7d5fad6205fe4eead190538b82df8585ceb98 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Mon, 4 Feb 2013 15:13:14 -0500
Subject: [PATCH 06/58] have timer redirect when it expires
---
lms/djangoapps/courseware/views.py | 16 ++++--
lms/templates/courseware/testcenter_exam.html | 53 ++++++++++---------
2 files changed, 41 insertions(+), 28 deletions(-)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index b44887dbfd..2484aa5c6b 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -8,7 +8,7 @@ from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
-from django.http import Http404
+from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
@@ -397,6 +397,12 @@ def timed_exam(request, course_id, chapter, section):
context['content'] = section_module.get_html()
+ # determine where to go when the exam ends:
+ if 'time_expired_redirect_url' not in section_descriptor.metadata:
+ raise Http404
+ time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url')
+ context['time_expired_redirect_url'] = time_expired_redirect_url
+
# figure out when the timed exam should end. Going forward, this is determined by getting a "normal"
# duration from the test, then doing some math to modify the duration based on accommodations,
# and then use that value as the end. Once we have calculated this, it should be sticky -- we
@@ -407,7 +413,7 @@ def timed_exam(request, course_id, chapter, section):
if 'duration' not in section_descriptor.metadata:
raise Http404
duration = int(section_descriptor.metadata.get('duration'))
-
+
# get corresponding time module, if one is present:
# TODO: determine what to use for module_key...
try:
@@ -424,7 +430,11 @@ def timed_exam(request, course_id, chapter, section):
# Proposal: store URL in the section descriptor,
# along with the duration. If no such URL is set,
# just put up the error page,
- raise Exception("Time expired on {}".format(timed_module))
+ if time_expired_redirect_url is None:
+ raise Exception("Time expired on {}".format(timed_module))
+ else:
+ return HttpResponseRedirect(time_expired_redirect_url)
+
elif not timed_module.has_begun:
# user has not started the exam, but may have an accommodation
# that has been granted to them.
diff --git a/lms/templates/courseware/testcenter_exam.html b/lms/templates/courseware/testcenter_exam.html
index d2f74ab296..8082200146 100644
--- a/lms/templates/courseware/testcenter_exam.html
+++ b/lms/templates/courseware/testcenter_exam.html
@@ -61,26 +61,21 @@
%block>
From 1923ae0d6b08aa094ba1e464a4ead26b1b782293 Mon Sep 17 00:00:00 2001
From: Brian Talbot
Date: Mon, 4 Feb 2013 15:40:01 -0500
Subject: [PATCH 07/58] pearson - added in timer styling for IE7
---
lms/static/sass/course/layout/_timer.scss | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/lms/static/sass/course/layout/_timer.scss b/lms/static/sass/course/layout/_timer.scss
index 01d62d87c7..eef21b8c27 100644
--- a/lms/static/sass/course/layout/_timer.scss
+++ b/lms/static/sass/course/layout/_timer.scss
@@ -2,10 +2,26 @@ div.timer-main {
position: fixed;
z-index: 99;
width: 100%;
+ border-top: 2px solid #000;
div#timer_wrapper {
position: relative;
+ top: -3px;
float: right;
margin-right: 10px;
+ background: #000;
+ color: #fff;
+ padding: 10px 20px;
+ border-radius: 3px;
+ }
+
+ .timer_label {
+ color: #ccc;
+ font-size: 13px;
+ }
+
+ #exam_timer {
+ font-weight: bold;
+ font-size: 15px;
}
}
From 1b465d1beb0fe744199dc8c7d222563e3199a2e3 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Tue, 5 Feb 2013 18:01:55 -0500
Subject: [PATCH 08/58] implement testcenter_login
---
common/djangoapps/student/views.py | 130 ++++++++++++++++++++++++++--
lms/djangoapps/courseware/models.py | 4 +
lms/djangoapps/courseware/views.py | 6 +-
3 files changed, 131 insertions(+), 9 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index b583599e97..9312f7b76a 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1,12 +1,10 @@
import datetime
import feedparser
-#import itertools
import json
import logging
import random
import string
import sys
-#import time
import urllib
import uuid
@@ -18,10 +16,13 @@ from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.mail import send_mail
+from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError
-from django.http import HttpResponse, HttpResponseForbidden, Http404
+from django.http import HttpResponse, HttpResponseForbidden, Http404,\
+ HttpResponseRedirect
from django.shortcuts import redirect
+
from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup
from django.core.cache import cache
@@ -39,11 +40,11 @@ from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
-#from datetime import date
from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access
+from courseware.models import TimedModule
from statsd import statsd
@@ -1058,7 +1059,7 @@ def accept_name_change(request):
# TODO: This is a giant kludge to give Pearson something to test against ASAP.
# Will need to get replaced by something that actually ties into TestCenterUser
@csrf_exempt
-def test_center_login(request):
+def atest_center_login(request):
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
raise Http404
@@ -1076,6 +1077,125 @@ def test_center_login(request):
return HttpResponseForbidden()
+@csrf_exempt
+def test_center_login(request):
+ # errors are returned by navigating to the error_url, adding a query parameter named "code"
+ # which contains the error code describing the exceptional condition.
+ def makeErrorURL(error_url, error_code):
+ return "{}&code={}".format(error_url, error_code);
+
+ # get provided error URL, which will be used as a known prefix for returning error messages to the
+ # Pearson shell. It does not have a trailing slash, so we need to add one when creating output URLs.
+ error_url = request.POST.get("errorURL")
+
+ # check that the parameters have not been tampered with, by comparing the code provided by Pearson
+ # with the code we calculate for the same parameters.
+ if 'code' not in request.POST:
+ return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"));
+ code = request.POST.get("code")
+
+ # calculate SHA for query string
+ # TODO: figure out how to get the original query string, so we can hash it and compare.
+
+
+ if 'clientCandidateID' not in request.POST:
+ return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"));
+ client_candidate_id = request.POST.get("clientCandidateID")
+
+ # TODO: check remaining parameters, and maybe at least log if they're not matching
+ # expected values....
+ # registration_id = request.POST.get("registrationID")
+ # exit_url = request.POST.get("exitURL")
+
+
+ # find testcenter_user that matches the provided ID:
+ try:
+ testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
+ except TestCenterUser.DoesNotExist:
+ return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
+
+
+ # find testcenter_registration that matches the provided exam code:
+ # Note that we could rely on either the registrationId or the exam code,
+ # or possibly both.
+ if 'vueExamSeriesCode' not in request.POST:
+ # TODO: confirm this error code (made up, not in documentation)
+ return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode"));
+ exam_series_code = request.POST.get('vueExamSeriesCode')
+
+ registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
+
+ if not registrations:
+ return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
+
+ # TODO: figure out what to do if there are more than one registrations....
+ # for now, just take the first...
+ registration = registrations[0]
+ course_id = registration.course_id
+
+ # if we want to look up whether the test has already been taken, or to
+ # communicate that a time accommodation needs to be applied, we need to
+ # know the module_id to use that corresponds to the particular exam_series_code.
+ # For now, we can hardcode that...
+ if exam_series_code == '6002x001':
+ chapter_url_name = 'Final_Exam'
+ section_url_name = 'Final_Exam_Fall_2012'
+ redirect_url = reverse('courseware_section', args=[course_id, chapter_url_name, section_url_name])
+ location = 'i4x://MITx/6.002x/2012_Fall/sequence/Final_Exam_Fall_2012'
+ else:
+ # TODO: clarify if this is the right error code for this condition.
+ return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
+
+
+ time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
+ 'ET30MN' : 'ADD30MIN',
+ 'ETDBTM' : 'ADDDOUBLE', }
+
+ # check if the test has already been taken
+ timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, module_state_key=location)
+ if timed_modules:
+ timed_module = timed_modules[0]
+ if timed_module.has_ended:
+ return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
+ elif registration.get_accommodation_codes():
+ # we don't have a timed module created yet, so if we have time accommodations
+ # to implement, create an entry now:
+ time_accommodation_code = None
+ for code in registration.get_accommodation_codes():
+ if code in time_accommodation_mapping:
+ time_accommodation_code = time_accommodation_mapping[code]
+ if client_candidate_id == "edX003671291147":
+ time_accommodation_code = 'TESTING'
+ if time_accommodation_code:
+ timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=location)
+ timed_module.accommodation_code = time_accommodation_code
+ timed_module.save()
+
+ # Now log the user in:
+# user = authenticate(username=testcenteruser.user.username,
+# password=testcenteruser.user.password)
+#
+# if user is None:
+# # argh. We couldn't login!
+# return HttpResponseRedirect(makeErrorURL(error_url, "ARGH! User cannot log in"));
+
+ # UGLY HACK!!!
+ # Login assumes that authentication has occurred, and that there is a
+ # backend annotation on the user object, indicating which backend
+ # against which the user was authenticated. We're authenticating here
+ # against the registration entry, and assuming that the request given
+ # this information is correct, we allow the user to be logged in
+ # without a password. This could all be formalized in a backend object
+ # that does the above checking.
+ # TODO: create a backend class to do this.
+ # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
+ testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
+ login(request, testcenteruser.user)
+
+ # And start the test:
+ return redirect(redirect_url)
+
+
def _get_news(top=None):
"Return the n top news items on settings.RSS_URL"
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index bd2da02027..00079d30f2 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -239,6 +239,7 @@ class TimedModule(models.Model):
('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
('ADD30MIN', 'Extra Time - 30 Minutes'),
('DOUBLE', 'Extra Time - Double Time'),
+ ('TESTING', 'Extra Time -- Large amount for testing purposes')
)
accommodation_code = models.CharField(max_length=12, choices=TIME_ACCOMMODATION_CODES, default='NONE', db_index=True)
@@ -256,6 +257,9 @@ class TimedModule(models.Model):
return (duration + (30 * 60))
elif self.accommodation_code == 'DOUBLE':
return (duration * 2)
+ elif self.accommodation_code == 'TESTING':
+ # when testing, set timer to run for a week at a time.
+ return 3600 * 24 * 7
# store state:
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 2484aa5c6b..5afda7b181 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -415,9 +415,8 @@ def timed_exam(request, course_id, chapter, section):
duration = int(section_descriptor.metadata.get('duration'))
# get corresponding time module, if one is present:
- # TODO: determine what to use for module_key...
try:
- timed_module = TimedModule.objects.get(student=request.user, course_id=course_id)
+ timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, module_state_key=section_module.id)
# if a module exists, check to see if it has already been started,
# and if it has already ended.
@@ -447,8 +446,7 @@ def timed_exam(request, course_id, chapter, section):
except TimedModule.DoesNotExist:
# no entry found. So we're starting this test
# without any accommodations being preset.
- # TODO: determine what to use for module_key...
- timed_module = TimedModule(student=request.user, course_id=course_id)
+ timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=section_module.id)
timed_module.begin(duration)
timed_module.save()
From 6ed2737c36b4405169569c0eaedbf510508dbafe Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Wed, 6 Feb 2013 13:29:48 -0500
Subject: [PATCH 09/58] make LMS forum subsystem more robust in case of
orphaned discussion modules. Given our draft/non-draft duality, we don't
currently have a means to always do proper housekeeping at this point in
time. However, we have to stop the LMS Forums from blowing up when
encoutering one of these.
---
lms/djangoapps/django_comment_client/utils.py | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index b58e3b30e6..40fd106b40 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -11,6 +11,8 @@ from django.http import HttpResponse
from django.utils import simplejson
from django_comment_client.models import Role
from django_comment_client.permissions import check_permissions_by_view
+from xmodule.modulestore.exceptions import NoPathToItem
+
from mitxmako import middleware
import pystache_custom as pystache
@@ -158,6 +160,14 @@ def initialize_discussion_info(course):
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
skip_module = True
+ # cdodge: pre-compute the path_to_location. Note this can throw an exception for any
+ # dangling discussion modules
+ try:
+ _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_location(modulestore(), course.id, module.location)
+ except NoPathToItem:
+ log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location))
+ skip_module = True
+
if skip_module:
continue
@@ -360,7 +370,13 @@ def get_courseware_context(content, course):
if id in id_map:
location = id_map[id]["location"].url()
title = id_map[id]["title"]
- (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
+
+ # cdodge: did we pre-compute, if so, then let's use that rather than recomputing
+ if 'path_to_location' in _DISCUSSIONINFO[course.id]:
+ (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location']
+ else:
+ (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
+
url = reverse('courseware_position', kwargs={"course_id":course_id,
"chapter":chapter,
"section":section,
From 114d800c6a6a6650c49c4cddee40d7866c5e442e Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Wed, 6 Feb 2013 15:17:47 -0500
Subject: [PATCH 10/58] need to make path_to_location a dictionary since it
needs to be keyed by the location
---
lms/djangoapps/django_comment_client/utils.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 0940d065f4..151bde3dd5 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -166,6 +166,7 @@ def initialize_discussion_info(course):
# get all discussion models within this course_id
all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
+ path_to_locations = {}
for module in all_modules:
skip_module = False
for key in ('id', 'discussion_category', 'for'):
@@ -176,7 +177,7 @@ def initialize_discussion_info(course):
# cdodge: pre-compute the path_to_location. Note this can throw an exception for any
# dangling discussion modules
try:
- _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_location(modulestore(), course.id, module.location)
+ path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location)
except NoPathToItem:
log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location))
skip_module = True
@@ -245,6 +246,7 @@ def initialize_discussion_info(course):
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
+ _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations
class JsonResponse(HttpResponse):
@@ -402,8 +404,8 @@ def get_courseware_context(content, course):
title = id_map[id]["title"]
# cdodge: did we pre-compute, if so, then let's use that rather than recomputing
- if 'path_to_location' in _DISCUSSIONINFO[course.id]:
- (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location']
+ if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']:
+ (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
else:
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
From bfc452759013a443915396c3df1f34e5e9e423f1 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Wed, 6 Feb 2013 15:23:11 -0500
Subject: [PATCH 11/58] Switch timed_module to store location, and use to
navigate from timer when timer displays on non-exam course pages.
---
common/djangoapps/student/views.py | 21 ++----
.../migrations/0006_add_timed_module.py | 14 ++--
lms/djangoapps/courseware/models.py | 6 +-
lms/djangoapps/courseware/views.py | 50 ++++++++++++--
lms/templates/courseware/courseware.html | 66 ++++++++++++++++++-
lms/templates/main.html | 5 ++
6 files changed, 133 insertions(+), 29 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 9312f7b76a..61313376d1 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1138,10 +1138,11 @@ def test_center_login(request):
# know the module_id to use that corresponds to the particular exam_series_code.
# For now, we can hardcode that...
if exam_series_code == '6002x001':
- chapter_url_name = 'Final_Exam'
- section_url_name = 'Final_Exam_Fall_2012'
- redirect_url = reverse('courseware_section', args=[course_id, chapter_url_name, section_url_name])
- location = 'i4x://MITx/6.002x/2012_Fall/sequence/Final_Exam_Fall_2012'
+ # This should not be hardcoded here, but should be added to the exam definition.
+ # TODO: look the location up in the course, by finding the exam_info with the matching code,
+ # and get the location from that.
+ location = 'i4x://MITx/6.002x/sequential/Final_Exam_Fall_2012'
+ redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
else:
# TODO: clarify if this is the right error code for this condition.
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
@@ -1152,7 +1153,7 @@ def test_center_login(request):
'ETDBTM' : 'ADDDOUBLE', }
# check if the test has already been taken
- timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, module_state_key=location)
+ timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, location=location)
if timed_modules:
timed_module = timed_modules[0]
if timed_module.has_ended:
@@ -1167,17 +1168,9 @@ def test_center_login(request):
if client_candidate_id == "edX003671291147":
time_accommodation_code = 'TESTING'
if time_accommodation_code:
- timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=location)
+ timed_module = TimedModule(student=request.user, course_id=course_id, location=location)
timed_module.accommodation_code = time_accommodation_code
timed_module.save()
-
- # Now log the user in:
-# user = authenticate(username=testcenteruser.user.username,
-# password=testcenteruser.user.password)
-#
-# if user is None:
-# # argh. We couldn't login!
-# return HttpResponseRedirect(makeErrorURL(error_url, "ARGH! User cannot log in"));
# UGLY HACK!!!
# Login assumes that authentication has occurred, and that there is a
diff --git a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
index 89b63cf659..6e8791a975 100644
--- a/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
+++ b/lms/djangoapps/courseware/migrations/0006_add_timed_module.py
@@ -11,7 +11,7 @@ class Migration(SchemaMigration):
# Adding model 'TimedModule'
db.create_table('courseware_timedmodule', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('module_state_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='module_id', db_index=True)),
+ ('location', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='location', db_index=True)),
('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('accommodation_code', self.gf('django.db.models.fields.CharField')(default='NONE', max_length=12, db_index=True)),
@@ -22,13 +22,13 @@ class Migration(SchemaMigration):
))
db.send_create_signal('courseware', ['TimedModule'])
- # Adding unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id']
- db.create_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id'])
+ # Adding unique constraint on 'TimedModule', fields ['student', 'location', 'course_id']
+ db.create_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
def backwards(self, orm):
- # Removing unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id']
- db.delete_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id'])
+ # Removing unique constraint on 'TimedModule', fields ['student', 'location', 'course_id']
+ db.delete_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
# Deleting model 'TimedModule'
db.delete_table('courseware_timedmodule')
@@ -103,15 +103,15 @@ class Migration(SchemaMigration):
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'courseware.timedmodule': {
- 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'TimedModule'},
+ 'Meta': {'unique_together': "(('student', 'location', 'course_id'),)", 'object_name': 'TimedModule'},
'accommodation_code': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '12', 'db_index': 'True'}),
'beginning_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'ending_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'location'", 'db_index': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
- 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 00079d30f2..d9cc560215 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -224,12 +224,14 @@ class TimedModule(models.Model):
# but for abtests and the like, this can be set to a shared value
# for many instances of the module.
# Filename for homeworks, etc.
- module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
+ # module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
+ location = models.CharField(max_length=255, db_index=True, db_column='location')
student = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
class Meta:
- unique_together = (('student', 'module_state_key', 'course_id'),)
+# unique_together = (('student', 'module_state_key', 'course_id'),)
+ unique_together = (('student', 'location', 'course_id'),)
# For a timed activity, we are only interested here
# in time-related accommodations, and these should be disjoint.
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 5afda7b181..0acf435f0b 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -215,6 +215,43 @@ def index(request, course_id, chapter=None, section=None,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
}
+ # check here if this page is within a course that has an active timed module running. If so, then
+ # display the appropriate timer information:
+ timed_modules = TimedModule.objects.filter(student=request.user, course_id=course_id)
+ if timed_modules:
+ for timed_module in timed_modules:
+ if timed_module.has_begun and not timed_module.has_ended:
+ # a timed module has been found that is active, so display
+ # the relevant time:
+ # module_state_key = timed_module.module_state_key
+ location = timed_module.location
+
+ # when we actually make the state be stored in the StudentModule, then
+ # we can fetch what we need from that.
+ # student_module = student_module_cache.lookup(course_id, 'sequential', module_state_key)
+ # But the module doesn't give us anything helpful to find the corresponding descriptor
+
+ # get the corresponding section_descriptor for this timed_module entry:
+ section_descriptor = modulestore().get_instance(course_id, Location(location))
+
+ # determine where to go when the timer expires:
+ # Note that if we could get this from the timed_module, we wouldn't have to
+ # fetch the section_descriptor in the first place.
+ if 'time_expired_redirect_url' not in section_descriptor.metadata:
+ raise Http404
+ time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url')
+ context['time_expired_redirect_url'] = time_expired_redirect_url
+
+ # Fetch the end time (in GMT) as stored in the module when it was started.
+ # This value should be UTC time as number of milliseconds since epoch.
+ end_date = timed_module.get_end_time_in_ms()
+ context['timer_expiration_datetime'] = end_date
+ if 'suppress_toplevel_navigation' in section_descriptor.metadata:
+ context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation']
+ return_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
+ context['timer_navigation_return_url'] = return_url
+
+
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None:
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
@@ -416,7 +453,7 @@ def timed_exam(request, course_id, chapter, section):
# get corresponding time module, if one is present:
try:
- timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, module_state_key=section_module.id)
+ timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, location=section_module.location)
# if a module exists, check to see if it has already been started,
# and if it has already ended.
@@ -446,7 +483,7 @@ def timed_exam(request, course_id, chapter, section):
except TimedModule.DoesNotExist:
# no entry found. So we're starting this test
# without any accommodations being preset.
- timed_module = TimedModule(student=request.user, course_id=course_id, module_state_key=section_module.id)
+ timed_module = TimedModule(student=request.user, course_id=course_id, location=section_module.location)
timed_module.begin(duration)
timed_module.save()
@@ -457,9 +494,12 @@ def timed_exam(request, course_id, chapter, section):
end_date = timed_module.get_end_time_in_ms()
# This value should be UTC time as number of milliseconds since epoch.
- context['end_date'] = end_date
-
- result = render_to_response('courseware/testcenter_exam.html', context)
+ # context['end_date'] = end_date
+ context['timer_expiration_datetime'] = end_date
+ if 'suppress_toplevel_navigation' in section_descriptor.metadata:
+ context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation']
+
+ result = render_to_response('courseware/courseware.html', context)
except Exception as e:
if isinstance(e, Http404):
# let it propagate
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index 1ea3df1b5a..da429c5275 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -59,12 +59,75 @@
});
});
+
+% if timer_expiration_datetime:
+
+% endif
+
%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
diff --git a/lms/templates/main_nonav.html b/lms/templates/main_nonav.html
deleted file mode 100644
index f2b87ef348..0000000000
--- a/lms/templates/main_nonav.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<%namespace name='static' file='static_content.html'/>
-
-
-
- <%block name="title">edX%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"/>
-
-
From a770e34bec6b339776f64f46ad09faf8659d48e6 Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Wed, 6 Feb 2013 20:36:44 -0500
Subject: [PATCH 13/58] Adding multiple-choice loncapa integration.
---
common/lib/capa/capa/responsetypes.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 78c986a963..1ecba36d50 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -632,8 +632,10 @@ class MultipleChoiceResponse(LoncapaResponse):
# define correct choices (after calling secondary setup)
xml = self.xml
- cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
- self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml]
+ cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id'))
+ self.correct_choices = [contextualize_text(choice.get('name'),
+ self.context) for choice in cxml if
+ contextualize_text(choice.get('correct'), self.context) == "true"]
def mc_setup_response(self):
'''
From bf17341fdb7a7fd5f72a0d32adb2ed7fd20f8ef2 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Wed, 6 Feb 2013 23:16:44 -0500
Subject: [PATCH 14/58] add select_related("profile")
---
lms/djangoapps/instructor/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 5ce256b275..ae4ac05167 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -387,7 +387,7 @@ def instructor_dashboard(request, course_id):
# DataDump
elif 'Download CSV of all student profile data' in action:
- enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
+ enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username').select_related("profile")
profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education',
'mailing_address', 'goals']
datatable = {'header': ['username', 'email'] + profkeys}
From 33a5a5fd9fa3a84773316ced8ea8fc1a3d522f01 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 7 Feb 2013 05:17:56 -0500
Subject: [PATCH 15/58] add implementation for fixed_time_module
---
common/lib/xmodule/setup.py | 1 +
.../lib/xmodule/xmodule/fixed_time_module.py | 180 ++++++++++++++++++
common/lib/xmodule/xmodule/modulestore/xml.py | 2 +-
3 files changed, 182 insertions(+), 1 deletion(-)
create mode 100644 common/lib/xmodule/xmodule/fixed_time_module.py
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 29227c3188..f61e6f6f36 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -23,6 +23,7 @@ setup(
"course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
+ "fixedtime = xmodule.fixed_time_module:FixedTimeDescriptor",
"html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
diff --git a/common/lib/xmodule/xmodule/fixed_time_module.py b/common/lib/xmodule/xmodule/fixed_time_module.py
new file mode 100644
index 0000000000..f1fec26dc3
--- /dev/null
+++ b/common/lib/xmodule/xmodule/fixed_time_module.py
@@ -0,0 +1,180 @@
+import json
+import logging
+
+from lxml import etree
+from time import time
+
+from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.xml_module import XmlDescriptor
+from xmodule.x_module import XModule
+from xmodule.progress import Progress
+from xmodule.exceptions import NotFoundError
+from pkg_resources import resource_string
+
+
+log = logging.getLogger(__name__)
+
+# HACK: This shouldn't be hard-coded to two types
+# OBSOLETE: This obsoletes 'type'
+# class_priority = ['video', 'problem']
+
+
+class FixedTimeModule(XModule):
+ '''
+ Wrapper module which imposes a time constraint for the completion of its child.
+ '''
+
+ def __init__(self, system, location, definition, descriptor, instance_state=None,
+ shared_state=None, **kwargs):
+ XModule.__init__(self, system, location, definition, descriptor,
+ instance_state, shared_state, **kwargs)
+
+ # NOTE: Position is 1-indexed. This is silly, but there are now student
+ # positions saved on prod, so it's not easy to fix.
+# self.position = 1
+ self.beginning_at = None
+ self.ending_at = None
+ self.accommodation_code = None
+
+ if instance_state is not None:
+ state = json.loads(instance_state)
+
+ if 'beginning_at' in state:
+ self.beginning_at = state['beginning_at']
+ if 'ending_at' in state:
+ self.ending_at = state['ending_at']
+ if 'accommodation_code' in state:
+ self.accommodation_code = state['accommodation_code']
+
+
+ # if position is specified in system, then use that instead
+# if system.get('position'):
+# self.position = int(system.get('position'))
+
+ self.rendered = False
+
+ # For a timed activity, we are only interested here
+ # in time-related accommodations, and these should be disjoint.
+ # (For proctored exams, it is possible to have multiple accommodations
+ # apply to an exam, so they require accommodating a multi-choice.)
+ TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
+ ('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
+ ('ADD30MIN', 'Extra Time - 30 Minutes'),
+ ('DOUBLE', 'Extra Time - Double Time'),
+ ('TESTING', 'Extra Time -- Large amount for testing purposes')
+ )
+
+ def _get_accommodated_duration(self, duration):
+ '''
+ Get duration for activity, as adjusted for accommodations.
+ Input and output are expressed in seconds.
+ '''
+ if self.accommodation_code is None or self.accommodation_code == 'NONE':
+ return duration
+ elif self.accommodation_code == 'ADDHALFTIME':
+ # TODO: determine what type to return
+ return int(duration * 1.5)
+ elif self.accommodation_code == 'ADD30MIN':
+ return (duration + (30 * 60))
+ elif self.accommodation_code == 'DOUBLE':
+ return (duration * 2)
+ elif self.accommodation_code == 'TESTING':
+ # when testing, set timer to run for a week at a time.
+ return 3600 * 24 * 7
+
+ # store state:
+
+ @property
+ def has_begun(self):
+ return self.beginning_at is not None
+
+ @property
+ def has_ended(self):
+ if not self.ending_at:
+ return False
+ return self.ending_at < time()
+
+ def begin(self, duration):
+ '''
+ Sets the starting time and ending time for the activity,
+ based on the duration provided (in seconds).
+ '''
+ self.beginning_at = time()
+ modified_duration = self._get_accommodated_duration(duration)
+ # datetime_duration = timedelta(seconds=modified_duration)
+ # self.ending_at = self.beginning_at + datetime_duration
+ self.ending_at = self.beginning_at + modified_duration
+
+ def get_end_time_in_ms(self):
+ return int(self.ending_at * 1000)
+
+ def get_instance_state(self):
+ state = {}
+ if self.beginning_at:
+ state['beginning_at'] = self.beginning_at
+ if self.ending_at:
+ state['ending_at'] = self.ending_at
+ if self.accommodation_code:
+ state['accommodation_code'] = self.accommodation_code
+ return json.dumps(state)
+
+ def get_html(self):
+ self.render()
+ return self.content
+
+ def get_progress(self):
+ ''' Return the total progress, adding total done and total available.
+ (assumes that each submodule uses the same "units" for progress.)
+ '''
+ # TODO: Cache progress or children array?
+ children = self.get_children()
+ progresses = [child.get_progress() for child in children]
+ progress = reduce(Progress.add_counts, progresses)
+ return progress
+
+ def handle_ajax(self, dispatch, get): # TODO: bounds checking
+# ''' get = request.POST instance '''
+# if dispatch == 'goto_position':
+# self.position = int(get['position'])
+# return json.dumps({'success': True})
+ raise NotFoundError('Unexpected dispatch type')
+
+ def render(self):
+ if self.rendered:
+ return
+ # assumes there is one and only one child, so it only renders the first child
+ child = self.get_display_items()[0]
+ self.content = child.get_html()
+ self.rendered = True
+
+ def get_icon_class(self):
+ return self.get_children()[0].get_icon_class()
+
+
+class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
+ # TODO: fix this template?!
+ mako_template = 'widgets/sequence-edit.html'
+ module_class = FixedTimeModule
+
+ stores_state = True # For remembering when a student started, and when they should end
+
+ @classmethod
+ def definition_from_xml(cls, xml_object, system):
+ children = []
+ for child in xml_object:
+ try:
+ children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
+ except Exception as e:
+ log.exception("Unable to load child when parsing FixedTime wrapper. Continuing...")
+ if system.error_tracker is not None:
+ system.error_tracker("ERROR: " + str(e))
+ continue
+ return {'children': children}
+
+ def definition_to_xml(self, resource_fs):
+ xml_object = etree.Element('fixedtime')
+ for child in self.get_children():
+ xml_object.append(
+ etree.fromstring(child.export_to_xml(resource_fs)))
+ return xml_object
+
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 17d6f04932..332b1b1898 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -73,7 +73,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check)
# tags that really need unique names--they store (or should store) state.
- need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence')
+ need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'fixedtime')
attr = xml_data.attrib
tag = xml_data.tag
From 2b637fe5110996febeff0982e5c10ebc7159e4ce Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 7 Feb 2013 10:13:53 -0500
Subject: [PATCH 16/58] Fix loads of local static files when running in dev
mode
---
common/djangoapps/static_replace/__init__.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py
index bf27f5b38d..d41688530e 100644
--- a/common/djangoapps/static_replace/__init__.py
+++ b/common/djangoapps/static_replace/__init__.py
@@ -77,9 +77,13 @@ def replace_static_urls(text, data_directory, course_namespace=None):
# course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
+ # In debug mode, if we can find the url as is,
+ elif settings.DEBUG and finders.find(rest, True):
+ return original
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else:
course_path = "/".join((data_directory, rest))
+
try:
if staticfiles_storage.exists(rest):
url = staticfiles_storage.url(rest)
From faf7c64ea50d7f96fe2645d09abacbd5115fd040 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Thu, 7 Feb 2013 10:32:13 -0500
Subject: [PATCH 17/58] add try/catch and fallback to returning a path to the
root of the course
---
lms/djangoapps/django_comment_client/utils.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 151bde3dd5..877c730539 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -407,7 +407,12 @@ def get_courseware_context(content, course):
if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']:
(course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
else:
- (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
+ try:
+ (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
+ except NoPathToItem:
+ # Object is not in the graph any longer, let's just get path to the base of the course
+ # so that we can at least return something to the caller
+ (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location)
url = reverse('courseware_position', kwargs={"course_id":course_id,
"chapter":chapter,
From 70560d22379af4b27be12e1bfb10ad956328ad44 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Thu, 7 Feb 2013 10:41:37 -0500
Subject: [PATCH 18/58] Put DnD back into edit_subsection page
---
cms/templates/edit_subsection.html | 2 ++
1 file changed, 2 insertions(+)
diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html
index de5e14e0a9..d81f577940 100644
--- a/cms/templates/edit_subsection.html
+++ b/cms/templates/edit_subsection.html
@@ -107,6 +107,8 @@
+
+
+%endif
+
From 61f59918858f98754fd793bf1cf171130910f5a4 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 8 Feb 2013 12:15:09 -0500
Subject: [PATCH 33/58] Posting lots of new jobs
---
lms/templates/static_templates/jobs.html | 474 ++++++++++++++++++-----
1 file changed, 368 insertions(+), 106 deletions(-)
diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html
index 621e25b0bd..134f8a7a15 100644
--- a/lms/templates/static_templates/jobs.html
+++ b/lms/templates/static_templates/jobs.html
@@ -14,17 +14,17 @@
-
Our mission is to transform learning.
+
Our mission is to transform learning.
-
-
“EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”
- —Rafael Reif, MIT President
-
+
+
“EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”
+ —Rafael Reif, MIT President
+
-
-
“EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”
- —Drew Faust, Harvard President
-
+
+
“EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”
+ —Drew Faust, Harvard President
+
@@ -34,25 +34,45 @@
-
+
EdX is looking to add new talent to our team!
Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status
Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access to free education. We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.
-
Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.
+
Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you’re results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.
As part of the edX team, you’ll receive:
Competitive compensation
Generous benefits package
Free lunch every day
-
A great working experience where everyone cares
+
A great working experience where everyone cares and wants to change the world (no, we’re not kidding)
-
While we appreciate every applicant's interest, only those under consideration will be contacted. We regret that phone calls will not be accepted.
+
While we appreciate every applicant’s interest, only those under consideration will be contacted. We regret that phone calls will not be accepted. Equal opportunity employer.
-
+
+
+
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 37/58] cosmetic changes
---
common/djangoapps/student/views.py | 7 +++----
lms/djangoapps/courseware/models.py | 1 +
lms/djangoapps/courseware/views.py | 11 ++++-------
3 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 9ef6a0d73a..d6ad9ba4eb 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1166,10 +1166,9 @@ def test_center_login(request):
'ETDBTM' : 'ADDDOUBLE', }
time_accommodation_code = None
- if registration.get_accommodation_codes():
- for code in registration.get_accommodation_codes():
- if code in time_accommodation_mapping:
- time_accommodation_code = time_accommodation_mapping[code]
+ for code in registration.get_accommodation_codes():
+ if code in time_accommodation_mapping:
+ time_accommodation_code = time_accommodation_mapping[code]
# special, hard-coded client ID used by Pearson shell for testing:
if client_candidate_id == "edX003671291147":
time_accommodation_code = 'TESTING'
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 875c3fcf3a..ac9bde77cd 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -24,6 +24,7 @@ class StudentModule(models.Model):
MODULE_TYPES = (('problem', 'problem'),
('video', 'video'),
('html', 'html'),
+ ('timelimit', 'timelimit'),
)
## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index ffe3972310..e8b36ecd2a 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -174,8 +174,7 @@ def check_for_active_timelimit_module(request, course_id, course):
location = timelimit_module.location
# determine where to go when the timer expires:
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
- # TODO: provide a better error
- raise Http404
+ raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
# Fetch the end time (in GMT) as stored in the module when it was started.
@@ -197,8 +196,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des
context = {}
# determine where to go when the exam ends:
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
- # TODO: provide a better error
- raise Http404
+ raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', timelimit_module.location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
@@ -206,8 +204,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des
if not timelimit_module.has_begun:
# user has not started the exam, so start it now.
if 'duration' not in timelimit_descriptor.metadata:
- # TODO: provide a better error
- raise Http404
+ raise Http404("No {0} metadata at this location: {1} ".format('duration', timelimit_module.location))
# The user may have an accommodation that has been granted to them.
# This accommodation information should already be stored in the module's state.
duration = int(timelimit_descriptor.metadata.get('duration'))
@@ -295,7 +292,7 @@ def index(request, course_id, chapter=None, section=None,
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
save_child_position(course_module, chapter, instance_module)
else:
- raise Http404
+ raise Http404('No chapter descriptor found with name {}'.format(chapter))
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
if chapter_module is None:
From 2a1c89bec5c9296c96d4d3c04efd81a4a7ea7072 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Sat, 9 Feb 2013 02:24:51 -0500
Subject: [PATCH 38/58] change missingExamSeriesCode to missingPartnerID
---
common/djangoapps/student/views.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index d6ad9ba4eb..4413ebfc0f 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1112,9 +1112,11 @@ def test_center_login(request):
# or possibly both. But for now we know what to do with an ExamSeriesCode,
# while we currently have no record of RegistrationID values at all.
if 'vueExamSeriesCode' not in request.POST:
- # TODO: confirm this error code (made up, not in documentation)
+ # we are not allowed to make up a new error code, according to Pearson,
+ # so instead of "missingExamSeriesCode", we use a valid one that is
+ # inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
- return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode"));
+ return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"));
exam_series_code = request.POST.get('vueExamSeriesCode')
# special case for supporting test user:
if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001':
@@ -1188,7 +1190,7 @@ def test_center_login(request):
# this information is correct, we allow the user to be logged in
# without a password. This could all be formalized in a backend object
# that does the above checking.
- # TODO: create a backend class to do this.
+ # TODO: (brian) create a backend class to do this.
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
login(request, testcenteruser.user)
From 4f67c6c0522959c60658bab920d782bb08672fc1 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Sun, 10 Feb 2013 16:17:08 -0500
Subject: [PATCH 39/58] quick hack to give some protection from unauthorized
users from making new courses. Make it so only is_staff people see the
'Create New Course' button.
---
cms/djangoapps/contentstore/views.py | 7 ++++++-
cms/templates/index.html | 4 +++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 137e71b24a..87a2943773 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -122,7 +122,8 @@ def index(request):
course.location.course,
course.location.name]))
for course in courses],
- 'user': request.user
+ 'user': request.user,
+ 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
@@ -1259,6 +1260,10 @@ def edge(request):
@login_required
@expect_json
def create_new_course(request):
+
+ if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
+ raise PermissionDenied()
+
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
diff --git a/cms/templates/index.html b/cms/templates/index.html
index 92987babda..45c4edc176 100644
--- a/cms/templates/index.html
+++ b/cms/templates/index.html
@@ -37,7 +37,9 @@
My Courses
% if user.is_active:
- New Course
+ % if not disable_course_creation:
+ New Course
+ %endif
%for course, url in courses:
From fb1e7f3dc1f4a2d38035c18045ccf7c1ceb00917 Mon Sep 17 00:00:00 2001
From: David Ormsbee
Date: Sun, 10 Feb 2013 19:03:25 -0500
Subject: [PATCH 40/58] Make type check for self.code a little more robust,
give default val for mmlans so it's never undefined
---
common/lib/capa/capa/responsetypes.py | 2 +-
lms/lib/symmath/symmath_check.py | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 78c986a963..ad084bdaf7 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -999,7 +999,7 @@ def sympy_check2():
self.context['debug'] = self.system.DEBUG
# exec the check function
- if type(self.code) == str:
+ if isinstance(self.code, basestring):
try:
exec self.code in self.context['global_context'], self.context
correct = self.context['correct']
diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py
index a3dec4aae5..d386ea6b06 100644
--- a/lms/lib/symmath/symmath_check.py
+++ b/lms/lib/symmath/symmath_check.py
@@ -238,8 +238,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
###### PMathML input ######
# convert mathml answer to formula
try:
- if dynamath:
- mmlans = dynamath[0]
+ mmlans = dynamath[0] if dynamath else None
except Exception, err:
mmlans = None
if not mmlans:
From 33009eba7d6cd9915c305894b8a85bf3ec7d42a7 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Mon, 11 Feb 2013 14:30:26 -0500
Subject: [PATCH 41/58] add exporting of grading_policy.json
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 10 ++++++++++
common/lib/xmodule/xmodule/modulestore/xml_exporter.py | 7 +++++++
2 files changed, 17 insertions(+)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 72ae3821cc..adecd392eb 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -10,6 +10,7 @@ import json
from fs.osfs import OSFS
import copy
from mock import Mock
+from json import dumps, loads
from student.models import Registration
from django.contrib.auth.models import User
@@ -207,6 +208,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
+ # check for graiding_policy.json
+ fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
+ self.assertTrue(fs.exists('grading_policy.json'))
+
+ # compare what's on disk compared to what we have in our course
+ with fs.open('grading_policy.json','r') as grading_policy:
+ on_disk = loads(grading_policy.read())
+ course = ms.get_item(location)
+ self.assertEqual(on_disk, course.definition['data']['grading_policy'])
# remove old course
delete_course(ms, cs, location)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py
index bdbd5a6133..509a2c7db9 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py
@@ -2,6 +2,7 @@ import logging
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from fs.osfs import OSFS
+from json import dumps
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
@@ -27,6 +28,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
# export the course updates
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
+ # export the grading policy
+ policies_dir = export_fs.makeopendir('policies')
+ course_run_policy_dir = policies_dir.makeopendir(course.location.name)
+ with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
+ grading_policy.write(dumps(course.definition['data']['grading_policy']))
+
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
From ae4a854e188165239ebd961df53a557431f0dd9c Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 11 Feb 2013 15:46:58 -0500
Subject: [PATCH 42/58] Bug 180
---
cms/static/js/views/overview.js | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index 8cbae177a8..d064f24006 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -202,13 +202,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) {
children = _.without(children, ui.draggable.data('id'));
}
// add to this parent (figure out where)
- for (var i = 0; i < _els.length; i++) {
- if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) {
+ for (var i = 0, bump = 0; i < _els.length; i++) {
+ if (ui.draggable.is(_els[i])) {
+ bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c
+ // it's not in that list
+ }
+ else if (ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els
ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;");
- children.splice(i, 0, ui.draggable.data('id'));
+ children.splice(i + bump, 0, ui.draggable.data('id'));
break;
}
}
From 565f5f0adc8505966e099b525ec9abe21757745a Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 11 Feb 2013 16:28:22 -0500
Subject: [PATCH 43/58] Stop simple clicks from paging to top w/o disrupting
dnd
---
cms/static/js/views/overview.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index d064f24006..24b29156cd 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -58,6 +58,9 @@ $(document).ready(function() {
drop: onSectionReordered,
greedy: true
});
+
+ // stop clicks on drag bars from doing their thing w/o stopping drag (did this cancel pointer?)
+ $('.courseware-overview').click(function(e) {e.preventDefault(); });
});
From 777e0ef9c5165afee5a6094a1231c812eb72d085 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 11 Feb 2013 16:36:34 -0500
Subject: [PATCH 44/58] The other one was too inclusive
---
cms/static/js/views/overview.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index 24b29156cd..db42c13208 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -60,7 +60,7 @@ $(document).ready(function() {
});
// stop clicks on drag bars from doing their thing w/o stopping drag (did this cancel pointer?)
- $('.courseware-overview').click(function(e) {e.preventDefault(); });
+ $('.drag-handle').click(function(e) {e.preventDefault(); });
});
From 8bc9564d92304f60def3a384ce78e876bf63e203 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 11 Feb 2013 16:45:36 -0500
Subject: [PATCH 45/58] clean up note to self
---
cms/static/js/views/overview.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index db42c13208..7d92ab69ad 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -59,7 +59,7 @@ $(document).ready(function() {
greedy: true
});
- // stop clicks on drag bars from doing their thing w/o stopping drag (did this cancel pointer?)
+ // stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
});
From aa2d0ecaf26f79b7b4bf807a289d415e4a9b3024 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Mon, 11 Feb 2013 17:06:35 -0500
Subject: [PATCH 46/58] We no longer need override_settings, Django has a
better one.
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 2 +-
cms/djangoapps/contentstore/tests/tests.py | 1 -
cms/djangoapps/contentstore/tests/utils.py | 1 -
common/djangoapps/course_groups/tests/tests.py | 2 +-
common/djangoapps/status/tests.py | 2 +-
lms/djangoapps/course_wiki/tests/tests.py | 2 +-
lms/djangoapps/courseware/tests/tests.py | 2 +-
lms/djangoapps/django_comment_client/tests.py | 2 +-
lms/djangoapps/instructor/tests.py | 2 +-
lms/djangoapps/open_ended_grading/tests.py | 2 +-
requirements.txt | 1 -
11 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index adecd392eb..dcd1f408cd 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -1,7 +1,7 @@
import json
import shutil
from django.test.client import Client
-from override_settings import override_settings
+from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index 9af5b09276..d2f18f2e49 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -1,7 +1,6 @@
import json
import shutil
from django.test.client import Client
-from override_settings import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index 4e3510463f..be028b2836 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -2,7 +2,6 @@ import json
import copy
from time import time
from django.test import TestCase
-from override_settings import override_settings
from django.conf import settings
from student.models import Registration
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 0fbf863fee..b3ad928b39 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -2,7 +2,7 @@ import django.test
from django.contrib.auth.models import User
from django.conf import settings
-from override_settings import override_settings
+from django.test.utils import override_settings
from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts,
diff --git a/common/djangoapps/status/tests.py b/common/djangoapps/status/tests.py
index 98a36f433a..1695663ac5 100644
--- a/common/djangoapps/status/tests.py
+++ b/common/djangoapps/status/tests.py
@@ -1,7 +1,7 @@
from django.conf import settings
from django.test import TestCase
import os
-from override_settings import override_settings
+from django.test.utils import override_settings
from tempfile import NamedTemporaryFile
from status import get_site_status_msg
diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py
index 99f138f0bc..cecc4f9cf9 100644
--- a/lms/djangoapps/course_wiki/tests/tests.py
+++ b/lms/djangoapps/course_wiki/tests/tests.py
@@ -1,5 +1,5 @@
from django.core.urlresolvers import reverse
-from override_settings import override_settings
+from django.test.utils import override_settings
import xmodule.modulestore.django
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
index efa5ad823e..fb6842d4a9 100644
--- a/lms/djangoapps/courseware/tests/tests.py
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -11,7 +11,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from django.core.urlresolvers import reverse
-from override_settings import override_settings
+from django.test.utils import override_settings
import xmodule.modulestore.django
from xmodule.modulestore.mongo import MongoModuleStore
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index ac059a1e3f..4b5fe2ba5a 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -6,7 +6,7 @@ from django.conf import settings
from mock import Mock
-from override_settings import override_settings
+from django.test.utils import override_settings
import xmodule.modulestore.django
diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py
index 2610e57422..b775aa158a 100644
--- a/lms/djangoapps/instructor/tests.py
+++ b/lms/djangoapps/instructor/tests.py
@@ -15,7 +15,7 @@ import json
from nose import SkipTest
from mock import patch, Mock
-from override_settings import override_settings
+from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group
diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py
index 4d220d4baa..ec2fe5ab38 100644
--- a/lms/djangoapps/open_ended_grading/tests.py
+++ b/lms/djangoapps/open_ended_grading/tests.py
@@ -22,7 +22,7 @@ from mitxmako.shortcuts import render_to_string
import logging
log = logging.getLogger(__name__)
-from override_settings import override_settings
+from django.test.utils import override_settings
from django.http import QueryDict
diff --git a/requirements.txt b/requirements.txt
index 0faf2e3ba5..7bfaa11bc6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,7 +24,6 @@ django_nose
nosexcover==1.0.7
rednose==0.3.3
GitPython==0.3.2.RC1
-django-override-settings==1.2
mock==0.8.0
PyYAML==3.10
South==0.7.6
From 2dcdeff4b6d47b333971e74cd47eece1a86546c0 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Mon, 11 Feb 2013 17:07:15 -0500
Subject: [PATCH 47/58] Make replace_static_urls leave alone urls that have
'?raw' at the end
- needed for GWT'ed modules that rely on the filename to find dependencies
---
common/djangoapps/static_replace/__init__.py | 20 +++++++++---
.../test/test_static_replace.py | 31 +++++++++++++++++--
2 files changed, 44 insertions(+), 7 deletions(-)
diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py
index d41688530e..088ba09181 100644
--- a/common/djangoapps/static_replace/__init__.py
+++ b/common/djangoapps/static_replace/__init__.py
@@ -13,12 +13,22 @@ log = logging.getLogger(__name__)
def _url_replace_regex(prefix):
+ """
+ Match static urls in quotes that don't end in '?raw'.
+
+ I'm sorry. http://xkcd.com/1171/
+
+ (?\\?['"]) # the opening quotes
- (?P{prefix}) # theeprefix
- (?P.*?) # everything else in the url
- (?P=quote) # the first matching closing quote
+ (?x) # flags=re.VERBOSE
+ (?P\\?['"]) # the opening quotes
+ (?P{prefix}) # the prefix
+ (?P.*? # everything else in the url...
+ (?
Date: Mon, 11 Feb 2013 17:50:13 -0500
Subject: [PATCH 48/58] Simplify, make it actually work.
- regexp was too complicated, didn't work
---
common/djangoapps/static_replace/__init__.py | 14 +++++++-------
.../static_replace/test/test_static_replace.py | 9 ++++++++-
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py
index 088ba09181..fb1f48d143 100644
--- a/common/djangoapps/static_replace/__init__.py
+++ b/common/djangoapps/static_replace/__init__.py
@@ -16,18 +16,14 @@ def _url_replace_regex(prefix):
"""
Match static urls in quotes that don't end in '?raw'.
- I'm sorry. http://xkcd.com/1171/
-
- (?\\?['"]) # the opening quotes
(?P{prefix}) # the prefix
- (?P.*? # everything else in the url...
- (?.*?) # everything else in the url
(?P=quote) # the first matching closing quote
""".format(prefix=prefix)
@@ -84,6 +80,10 @@ def replace_static_urls(text, data_directory, course_namespace=None):
quote = match.group('quote')
rest = match.group('rest')
+ # Don't mess with things that end in '?raw'
+ if rest.endswith('?raw'):
+ return original
+
# course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py
index 80b129f459..f23610e1bd 100644
--- a/common/djangoapps/static_replace/test/test_static_replace.py
+++ b/common/djangoapps/static_replace/test/test_static_replace.py
@@ -81,15 +81,22 @@ def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
def test_raw_static_check():
+ """
+ Make sure replace_static_urls leaves alone things that end in '.raw'
+ """
path = '"/static/foo.png?raw"'
assert_equals(path, replace_static_urls(path, DATA_DIRECTORY))
+ text = 'text
Date: Mon, 11 Feb 2013 20:01:09 -0500
Subject: [PATCH 49/58] fix showing less than and greater than in code
---
.../src/discussion/views/discussion_thread_show_view.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
index a8e95c2565..6320c3d1e3 100644
--- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
+++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
@@ -47,7 +47,7 @@ if Backbone?
convertMath: ->
element = @$(".post-body")
- element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
+ element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
toggleVote: (event) ->
From 21f4616080bc01de312d85d9ad9207c19c788b6d Mon Sep 17 00:00:00 2001
From: Kevin Chugh
Date: Tue, 12 Feb 2013 09:18:46 -0500
Subject: [PATCH 50/58] spread less than and greater than fix to responses and
comments, and profile view
---
.../src/discussion/views/discussion_thread_profile_view.coffee | 2 +-
.../src/discussion/views/response_comment_show_view.coffee | 2 +-
.../src/discussion/views/thread_response_show_view.coffee | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee
index d31a402a99..8b47696c01 100644
--- a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee
+++ b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee
@@ -50,7 +50,7 @@ if Backbone?
convertMath: ->
element = @$(".post-body")
- element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
+ element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
renderResponses: ->
diff --git a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee
index e6c8064978..84e7357e1f 100644
--- a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee
+++ b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee
@@ -26,7 +26,7 @@ if Backbone?
convertMath: ->
body = @$el.find(".response-body")
- body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html()
+ body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
markAsStaff: ->
diff --git a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee
index 32683fe6f6..1f305ddf34 100644
--- a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee
+++ b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee
@@ -30,7 +30,7 @@ if Backbone?
convertMath: ->
element = @$(".response-body")
- element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
+ element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
markAsStaff: ->
From 83e4dfaacb49c880b59f123d306a1280a0a6aabc Mon Sep 17 00:00:00 2001
From: Brian Talbot
Date: Tue, 12 Feb 2013 09:58:16 -0500
Subject: [PATCH 51/58] lms - pearson: trying to resolve JQUI accordion width
bug in IE7
---
lms/static/sass/course/courseware/_sidebar.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss
index 4e893d2455..1ab841e1e5 100644
--- a/lms/static/sass/course/courseware/_sidebar.scss
+++ b/lms/static/sass/course/courseware/_sidebar.scss
@@ -99,6 +99,7 @@ section.course-index {
@include border-radius(0);
margin: 0;
padding: 9px 0 9px 9px;
+ overflow: auto;
li {
border-bottom: 0;
From f33f7134fbf246de87e6b8d938ef145373196bb8 Mon Sep 17 00:00:00 2001
From: Brian Talbot
Date: Tue, 12 Feb 2013 10:34:21 -0500
Subject: [PATCH 52/58] lms - pearson: trying to resolve JQUI accordion width
bug in IE7 - enforcing width to chapters
---
lms/static/sass/course/courseware/_sidebar.scss | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss
index 1ab841e1e5..81b497d4f9 100644
--- a/lms/static/sass/course/courseware/_sidebar.scss
+++ b/lms/static/sass/course/courseware/_sidebar.scss
@@ -67,7 +67,7 @@ section.course-index {
}
.chapter {
- width: 100%;
+ width: 100% !important;
@include box-sizing(border-box);
padding: 11px 14px;
@include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0));
@@ -100,6 +100,7 @@ section.course-index {
margin: 0;
padding: 9px 0 9px 9px;
overflow: auto;
+ width: 100%;
li {
border-bottom: 0;
From 78d3d2006919bf95e0fb29ad2101bb6ece707899 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Wed, 23 Jan 2013 17:11:02 -0500
Subject: [PATCH 53/58] When checking types to convert data, don't forget about
longs. 32-bit Pythons make longs from values that are ints on 64-bit
Pythons.
Conflicts:
common/djangoapps/util/converters.py
---
common/djangoapps/util/converters.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py
index 900371a0dd..ec2d29ecfa 100644
--- a/common/djangoapps/util/converters.py
+++ b/common/djangoapps/util/converters.py
@@ -18,10 +18,13 @@ def jsdate_to_time(field):
"""
if field is None:
return field
- elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
- d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
+ elif isinstance(field, basestring):
+ # ISO format but ignores time zone assuming it's Z.
+ d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple()
- elif isinstance(field, int) or isinstance(field, float):
+ elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time):
return field
+ else:
+ raise ValueError("Couldn't convert %r to time" % field)
From b5378b04b3402db5c99aaeaf3db456bbd8c5e684 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Tue, 12 Feb 2013 12:25:11 -0500
Subject: [PATCH 54/58] change timer to calculate relative duration in
javascript
---
.../lib/xmodule/xmodule/timelimit_module.py | 4 ++--
lms/djangoapps/courseware/views.py | 19 +++++++++----------
lms/templates/courseware/courseware.html | 17 +++++++++--------
3 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py
index 23ed06eb59..9abb5d183f 100644
--- a/common/lib/xmodule/xmodule/timelimit_module.py
+++ b/common/lib/xmodule/xmodule/timelimit_module.py
@@ -86,8 +86,8 @@ class TimeLimitModule(XModule):
modified_duration = self._get_accommodated_duration(duration)
self.ending_at = self.beginning_at + modified_duration
- def get_end_time_in_ms(self):
- return int(self.ending_at * 1000)
+ def get_remaining_time_in_ms(self):
+ return int((self.ending_at - time()) * 1000)
def get_instance_state(self):
state = {}
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index e8b36ecd2a..fb351e1c01 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -177,10 +177,10 @@ def check_for_active_timelimit_module(request, course_id, course):
raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
- # Fetch the end time (in GMT) as stored in the module when it was started.
- # This value should be UTC time as number of milliseconds since epoch.
- end_date = timelimit_module.get_end_time_in_ms()
- context['timer_expiration_datetime'] = end_date
+ # Fetch the remaining time relative to the end time as stored in the module when it was started.
+ # This value should be in milliseconds.
+ remaining_time = timelimit_module.get_remaining_time_in_ms()
+ context['timer_expiration_duration'] = remaining_time
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location})
@@ -191,7 +191,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des
'''
Updates the state of the provided timing module, starting it if it hasn't begun.
Returns dict with timer-related values to enable display of time remaining.
- Returns 'timer_expiration_datetime' in dict if timer is still active, and not if timer has expired.
+ Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired.
'''
context = {}
# determine where to go when the exam ends:
@@ -215,10 +215,9 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des
instance_module.save()
# the exam has been started, either because the student is returning to the
- # exam page, or because they have just visited it. Fetch the end time (in GMT) as stored
- # in the module when it was started.
- # This value should be UTC time as number of milliseconds since epoch.
- context['timer_expiration_datetime'] = timelimit_module.get_end_time_in_ms()
+ # exam page, or because they have just visited it. Fetch the remaining time relative to the
+ # end time as stored in the module when it was started.
+ context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms()
# also use the timed module to determine whether top-level navigation is visible:
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
@@ -325,7 +324,7 @@ def index(request, course_id, chapter=None, section=None,
if section_module.category == 'timelimit':
timer_context = update_timelimit_module(request.user, course_id, student_module_cache,
section_descriptor, section_module)
- if 'timer_expiration_datetime' in timer_context:
+ if 'timer_expiration_duration' in timer_context:
context.update(timer_context)
else:
# if there is no expiration defined, then we know the timer has expired:
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index 72a4b2cae1..fcbc83d815 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -60,14 +60,12 @@
});
-% if timer_expiration_datetime:
+% if timer_expiration_duration: