diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 08844018af..b522d51a0f 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -1213,12 +1213,15 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
"""
+ # If this descriptor has been bound to a student, return the corresponding
+ # XModule. If not, just use the descriptor itself
try:
module = getattr(self, '_xmodule', None)
if not module:
module = self
except UndefinedContext:
module = self
+
all_descriptors = []
graded_sections = {}
diff --git a/lms/djangoapps/ccx/overrides.py b/lms/djangoapps/ccx/overrides.py
index 9a2c5d4e98..aced5410f8 100644
--- a/lms/djangoapps/ccx/overrides.py
+++ b/lms/djangoapps/ccx/overrides.py
@@ -7,6 +7,8 @@ import threading
from contextlib import contextmanager
+from django.db import transaction, IntegrityError
+
from courseware.field_overrides import FieldOverrideProvider
from ccx import ACTIVE_CCX_KEY
@@ -97,20 +99,29 @@ def _get_overrides_for_ccx(ccx, block):
return overrides
+@transaction.commit_on_success
def override_field_for_ccx(ccx, block, name, value):
"""
Overrides a field for the `ccx`. `block` and `name` specify the block
and the name of the field on that block to override. `value` is the
value to set for the given field.
"""
- override, created = CcxFieldOverride.objects.get_or_create(
+ field = block.fields[name]
+ value = json.dumps(field.to_json(value))
+ try:
+ override = CcxFieldOverride.objects.create(
+ ccx=ccx,
+ location=block.location,
+ field=name,
+ value=value)
+ except IntegrityError:
+ transaction.commit()
+ override = CcxFieldOverride.objects.get(
ccx=ccx,
location=block.location,
field=name)
- field = block.fields[name]
- override.value = json.dumps(field.to_json(value))
+ override.value = value
override.save()
-
if hasattr(block, '_ccx_overrides'):
del block._ccx_overrides[ccx.id]
diff --git a/lms/djangoapps/ccx/tests/test_overrides.py b/lms/djangoapps/ccx/tests/test_overrides.py
index 875a1c0249..c3e624a769 100644
--- a/lms/djangoapps/ccx/tests/test_overrides.py
+++ b/lms/djangoapps/ccx/tests/test_overrides.py
@@ -11,6 +11,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..models import CustomCourseForEdX
from ..overrides import override_field_for_ccx
+from .test_views import flatten, iter_blocks
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'ccx.overrides.CustomCoursesForEdxOverrideProvider',))
@@ -76,6 +77,28 @@ class TestFieldOverrides(ModuleStoreTestCase):
override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
self.assertEquals(chapter.start, ccx_start)
+ def test_override_num_queries(self):
+ """
+ Test that overriding and accessing a field produce same number of queries.
+ """
+ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
+ chapter = self.course.get_children()[0]
+ with self.assertNumQueries(4):
+ override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
+ dummy = chapter.start
+
+ def test_overriden_field_access_produces_no_extra_queries(self):
+ """
+ Test no extra queries when accessing an overriden field more than once.
+ """
+ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC)
+ chapter = self.course.get_children()[0]
+ with self.assertNumQueries(4):
+ override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
+ dummy1 = chapter.start
+ dummy2 = chapter.start
+ dummy3 = chapter.start
+
def test_override_is_inherited(self):
"""
Test that sequentials inherit overridden start date from chapter.
@@ -98,22 +121,3 @@ class TestFieldOverrides(ModuleStoreTestCase):
override_field_for_ccx(self.ccx, chapter, 'due', ccx_due)
vertical = chapter.get_children()[0].get_children()[0]
self.assertEqual(vertical.due, ccx_due)
-
-
-def flatten(seq):
- """
- For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse.
- """
- return [x for sub in seq for x in sub]
-
-
-def iter_blocks(course):
- """
- Returns an iterator over all of the blocks in a course.
- """
- def visit(block):
- yield block
- for child in block.get_children():
- for descendant in visit(child): # wish they'd backport yield from
- yield descendant
- return visit(course)
diff --git a/lms/djangoapps/ccx/tests/test_utils.py b/lms/djangoapps/ccx/tests/test_utils.py
index 764dfca2ad..d9c21152f0 100644
--- a/lms/djangoapps/ccx/tests/test_utils.py
+++ b/lms/djangoapps/ccx/tests/test_utils.py
@@ -253,7 +253,7 @@ class TestEnrollEmail(ModuleStoreTestCase):
"""enroll a non-user email and send an enrollment email to them
"""
# ensure no emails are in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
test_email = "nobody@nowhere.com"
before, after = self.call_FUT(
student_email=test_email, email_students=True
@@ -273,7 +273,7 @@ class TestEnrollEmail(ModuleStoreTestCase):
"""
self.create_user()
# ensure no emails are in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
before, after = self.call_FUT(email_students=True)
# there should be a membership set for this email address now
@@ -290,7 +290,7 @@ class TestEnrollEmail(ModuleStoreTestCase):
"""
self.register_user_in_ccx()
# ensure no emails are in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
before, after = self.call_FUT(email_students=True)
# there should be a membership set for this email address now
@@ -306,7 +306,7 @@ class TestEnrollEmail(ModuleStoreTestCase):
"""register a non-user via email address but send no email
"""
# ensure no emails are in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
test_email = "nobody@nowhere.com"
before, after = self.call_FUT(
student_email=test_email, email_students=False
@@ -317,13 +317,13 @@ class TestEnrollEmail(ModuleStoreTestCase):
for state in [before, after]:
self.check_enrollment_state(state, False, None, False)
# ensure there are still no emails in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
def test_enroll_non_member_no_email(self):
"""register a non-member but send no email"""
self.create_user()
# ensure no emails are in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
before, after = self.call_FUT(email_students=False)
# there should be a membership set for this email address now
@@ -331,14 +331,14 @@ class TestEnrollEmail(ModuleStoreTestCase):
self.check_enrollment_state(before, False, self.user, True)
self.check_enrollment_state(after, True, self.user, True)
# ensure there are still no emails in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
def test_enroll_member_no_email(self):
"""enroll a member but send no email
"""
self.register_user_in_ccx()
# ensure no emails are in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
before, after = self.call_FUT(email_students=False)
# there should be a membership set for this email address now
@@ -346,7 +346,7 @@ class TestEnrollEmail(ModuleStoreTestCase):
for state in [before, after]:
self.check_enrollment_state(state, True, self.user, True)
# ensure there are still no emails in the outbox now
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
# TODO: deal with changes in behavior for auto_enroll
@@ -364,11 +364,6 @@ class TestUnenrollEmail(ModuleStoreTestCase):
self.ccx = CcxFactory(course_id=course.id, coach=coach)
self.outbox = self.get_outbox()
- def tearDown(self):
- for attr in ['user', 'email']:
- if hasattr(self, attr):
- delattr(self, attr)
-
def get_outbox(self):
"""Return the django mail outbox"""
from django.core import mail
@@ -428,7 +423,7 @@ class TestUnenrollEmail(ModuleStoreTestCase):
self.make_ccx_future_membership()
# assert that a membership exists and that no emails have been sent
self.assertTrue(self.check_membership(future=True))
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
# unenroll the student
before, after = self.call_FUT(email_students=True)
@@ -447,7 +442,7 @@ class TestUnenrollEmail(ModuleStoreTestCase):
self.make_ccx_membership()
# assert that a membership exists and that no emails have been sent
self.assertTrue(self.check_membership())
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
# unenroll the student
before, after = self.call_FUT(email_students=True)
@@ -467,7 +462,7 @@ class TestUnenrollEmail(ModuleStoreTestCase):
self.make_ccx_future_membership()
# assert that a membership exists and that no emails have been sent
self.assertTrue(self.check_membership(future=True))
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
# unenroll the student
before, after = self.call_FUT()
@@ -477,7 +472,7 @@ class TestUnenrollEmail(ModuleStoreTestCase):
for state in [before, after]:
self.check_enrollment_state(state, False, None, False)
# no email was sent to the student
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
def test_unenroll_member_no_email(self):
"""unenroll a current member but send no email
@@ -485,7 +480,7 @@ class TestUnenrollEmail(ModuleStoreTestCase):
self.make_ccx_membership()
# assert that a membership exists and that no emails have been sent
self.assertTrue(self.check_membership())
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
# unenroll the student
before, after = self.call_FUT()
@@ -495,7 +490,7 @@ class TestUnenrollEmail(ModuleStoreTestCase):
self.check_enrollment_state(after, False, self.user, True)
self.check_enrollment_state(before, True, self.user, True)
# no email was sent to the student
- self.assertEqual(len(self.outbox), 0)
+ self.assertEqual(self.outbox, [])
class TestUserCCXList(ModuleStoreTestCase):
@@ -530,16 +525,16 @@ class TestUserCCXList(ModuleStoreTestCase):
def test_anonymous_sees_no_ccx(self):
memberships = self.call_FUT(self.anonymous)
- self.assertEqual(len(memberships), 0)
+ self.assertEqual(memberships, [])
def test_unenrolled_sees_no_ccx(self):
memberships = self.call_FUT(self.user)
- self.assertEqual(len(memberships), 0)
+ self.assertEqual(memberships, [])
def test_enrolled_inactive_sees_no_ccx(self):
self.register_user_in_ccx()
memberships = self.call_FUT(self.user)
- self.assertEqual(len(memberships), 0)
+ self.assertEqual(memberships, [])
def test_enrolled_sees_a_ccx(self):
self.register_user_in_ccx(active=True)
diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py
index 1b4cb1db69..3f03288859 100644
--- a/lms/djangoapps/ccx/tests/test_views.py
+++ b/lms/djangoapps/ccx/tests/test_views.py
@@ -223,7 +223,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
enrollment = CourseEnrollmentFactory(course_id=self.course.id)
student = enrollment.user
outbox = self.get_outbox()
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
url = reverse(
'ccx_invite',
@@ -254,7 +254,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
enrollment = CourseEnrollmentFactory(course_id=self.course.id)
student = enrollment.user
outbox = self.get_outbox()
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
# student is member of CCX:
CcxMembershipFactory(ccx=ccx, student=student)
@@ -286,7 +286,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.make_coach()
ccx = self.make_ccx()
outbox = self.get_outbox()
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
url = reverse(
'ccx_invite',
@@ -318,7 +318,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
ccx = self.make_ccx()
outbox = self.get_outbox()
CcxFutureMembershipFactory(ccx=ccx, email=test_email)
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
url = reverse(
'ccx_invite',
@@ -351,7 +351,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
student = enrollment.user
# no emails have been sent so far
outbox = self.get_outbox()
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
url = reverse(
'ccx_manage_student',
@@ -366,7 +366,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
# we were redirected to our current location
self.assertEqual(len(response.redirect_chain), 1)
self.assertTrue(302 in response.redirect_chain[0])
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
# a CcxMembership exists for this student
self.assertTrue(
CcxMembership.objects.filter(ccx=ccx, student=student).exists()
@@ -382,7 +382,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
CcxMembershipFactory(ccx=ccx, student=student)
# no emails have been sent so far
outbox = self.get_outbox()
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
url = reverse(
'ccx_manage_student',
@@ -397,7 +397,7 @@ class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
# we were redirected to our current location
self.assertEqual(len(response.redirect_chain), 1)
self.assertTrue(302 in response.redirect_chain[0])
- self.assertEqual(len(outbox), 0)
+ self.assertEqual(outbox, [])
# a CcxMembership exists for this student
self.assertFalse(
CcxMembership.objects.filter(ccx=ccx, student=student).exists()
diff --git a/lms/djangoapps/ccx/utils.py b/lms/djangoapps/ccx/utils.py
index a587bce4ea..8dca532dd4 100644
--- a/lms/djangoapps/ccx/utils.py
+++ b/lms/djangoapps/ccx/utils.py
@@ -65,8 +65,8 @@ def enroll_email(ccx, student_email, auto_enroll=False, email_students=False, em
previous_state = EmailEnrollmentState(ccx, student_email)
if previous_state.user:
+ user = User.objects.get(email=student_email)
if not previous_state.in_ccx:
- user = User.objects.get(email=student_email)
membership = CcxMembership(
ccx=ccx, student=user, active=True
)
diff --git a/lms/djangoapps/ccx/views.py b/lms/djangoapps/ccx/views.py
index ddedf4fc3e..a42018639f 100644
--- a/lms/djangoapps/ccx/views.py
+++ b/lms/djangoapps/ccx/views.py
@@ -32,7 +32,7 @@ from courseware.grades import iterate_grades_for
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from edxmako.shortcuts import render_to_response
-from opaque_keys.edx.locations import SlashSeparatedCourseKey
+from opaque_keys.edx.keys import CourseKey
from student.roles import CourseCcxCoachRole
from instructor.offline_gradecalc import student_grades
@@ -69,7 +69,7 @@ def coach_dashboard(view):
Wraps the view function, performing access check, loading the course,
and modifying the view's call signature.
"""
- course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
+ course_key = CourseKey.from_string(course_id)
role = CourseCcxCoachRole(course_key)
if not role.has_user(request.user):
return HttpResponseForbidden(
@@ -283,6 +283,8 @@ def get_ccx_schedule(course, ccx):
def visit(node, depth=1):
"""
Recursive generator function which yields CCX schedule nodes.
+ We convert dates to string to get them ready for use by the js date
+ widgets, which use text inputs.
"""
for child in node.get_children():
start = get_override_for_ccx(ccx, child, 'start', None)
@@ -358,7 +360,7 @@ def ccx_invite(request, course):
if action == "Unenroll":
unenroll_email(ccx, email, email_students=email_students)
except ValidationError:
- pass # maybe log this?
+ log.info('Invalid user name or email when trying to invite students: %s', email)
url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id})
return redirect(url)
@@ -389,7 +391,7 @@ def ccx_student_management(request, course):
elif action == 'revoke':
unenroll_email(ccx, email, email_students=False)
except ValidationError:
- pass # XXX: log, report?
+ log.info('Invalid user name or email when trying to enroll student: %s', email)
url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id})
return redirect(url)
@@ -499,8 +501,7 @@ def ccx_grades_csv(request, course):
def switch_active_ccx(request, course_id, ccx_id=None):
"""set the active CCX for the logged-in user
"""
- user = request.user
- course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
+ course_key = CourseKey.from_string(course_id)
# will raise Http404 if course_id is bad
course = get_course_by_id(course_key)
course_url = reverse(
@@ -509,7 +510,7 @@ def switch_active_ccx(request, course_id, ccx_id=None):
if ccx_id is not None:
try:
requested_ccx = CustomCourseForEdX.objects.get(pk=ccx_id)
- assert requested_ccx.course_id.to_deprecated_string() == course_id
+ assert unicode(requested_ccx.course_id) == course_id
if not CcxMembership.objects.filter(
ccx=requested_ccx, student=request.user, active=True
).exists():
diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py
index a84dc001b4..e7b0c8e121 100644
--- a/lms/djangoapps/courseware/field_overrides.py
+++ b/lms/djangoapps/courseware/field_overrides.py
@@ -199,20 +199,7 @@ def _lineage(block):
Returns an iterator over all ancestors of the given block, starting with
its immediate parent and ending at the root of the block tree.
"""
- parent = _get_parent(block)
+ parent = block.get_parent()
while parent:
yield parent
- parent = _get_parent(parent)
-
-
-def _get_parent(block):
- """
- Gets parent of a block, trying to cache result.
- """
- if not hasattr(block, '_parent'):
- store = modulestore()
- # Call to get_parent_location is fairly expensive. Is there a more
- # performant way to get at the ancestors of a block?
- location = store.get_parent_location(block.location)
- block._parent = store.get_item(location) if location else None
- return block._parent
+ parent = parent.get_parent()
diff --git a/lms/djangoapps/courseware/migrations/0012_auto__del_unique_studentfieldoverride_course_id_location_student__add_.py b/lms/djangoapps/courseware/migrations/0012_auto__del_unique_studentfieldoverride_course_id_location_student__add_.py
new file mode 100644
index 0000000000..d79f119a24
--- /dev/null
+++ b/lms/djangoapps/courseware/migrations/0012_auto__del_unique_studentfieldoverride_course_id_location_student__add_.py
@@ -0,0 +1,138 @@
+# -*- 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 unique constraint on 'StudentFieldOverride', fields ['course_id', 'field', 'location', 'student']
+ db.create_unique('courseware_studentfieldoverride', ['course_id', 'field', 'location', 'student_id'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'StudentFieldOverride', fields ['course_id', 'field', 'location', 'student']
+ db.delete_unique('courseware_studentfieldoverride', ['course_id', 'field', 'location', 'student_id'])
+
+
+ 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': ('xmodule_django.models.CourseKeyField', [], {'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': ('xmodule_django.models.CourseKeyField', [], {'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.studentfieldoverride': {
+ 'Meta': {'unique_together': "(('course_id', 'field', 'location', 'student'),)", 'object_name': 'StudentFieldOverride'},
+ 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'location': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.studentmodule': {
+ 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
+ 'course_id': ('xmodule_django.models.CourseKeyField', [], {'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': ('xmodule_django.models.LocationKeyField', [], {'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.studentmodulehistory': {
+ 'Meta': {'object_name': 'StudentModuleHistory'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'student_module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courseware.StudentModule']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'})
+ },
+ 'courseware.xmodulestudentinfofield': {
+ 'Meta': {'unique_together': "(('student', 'field_name'),)", 'object_name': 'XModuleStudentInfoField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmodulestudentprefsfield': {
+ 'Meta': {'unique_together': "(('student', 'module_type', 'field_name'),)", 'object_name': 'XModuleStudentPrefsField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'module_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmoduleuserstatesummaryfield': {
+ 'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleUserStateSummaryField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'usage_id': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ }
+ }
+
+ complete_apps = ['courseware']
diff --git a/lms/djangoapps/courseware/migrations/0013_auto__add_field_studentfieldoverride_created__add_field_studentfieldov.py b/lms/djangoapps/courseware/migrations/0013_auto__add_field_studentfieldoverride_created__add_field_studentfieldov.py
new file mode 100644
index 0000000000..bd67226137
--- /dev/null
+++ b/lms/djangoapps/courseware/migrations/0013_auto__add_field_studentfieldoverride_created__add_field_studentfieldov.py
@@ -0,0 +1,155 @@
+# -*- 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 field 'StudentFieldOverride.created'
+ db.add_column('courseware_studentfieldoverride', 'created',
+ self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now),
+ keep_default=False)
+
+ # Adding field 'StudentFieldOverride.modified'
+ db.add_column('courseware_studentfieldoverride', 'modified',
+ self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now),
+ keep_default=False)
+
+ # Adding index on 'StudentFieldOverride', fields ['course_id', 'location', 'student']
+ db.create_index('courseware_studentfieldoverride', ['course_id', 'location', 'student_id'])
+
+
+ def backwards(self, orm):
+ # Deleting field 'StudentFieldOverride.created'
+ db.delete_column('courseware_studentfieldoverride', 'created')
+
+ # Deleting field 'StudentFieldOverride.modified'
+ db.delete_column('courseware_studentfieldoverride', 'modified')
+
+ # Removing index on 'StudentFieldOverride', fields ['course_id', 'location', 'student']
+ db.delete_index('courseware_studentfieldoverride', ['course_id', 'location', 'student_id'])
+
+
+ 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': ('xmodule_django.models.CourseKeyField', [], {'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': ('xmodule_django.models.CourseKeyField', [], {'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.studentfieldoverride': {
+ 'Meta': {'unique_together': "(('course_id', 'field', 'location', 'student'),)", 'object_name': 'StudentFieldOverride'},
+ 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
+ 'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'location': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.studentmodule': {
+ 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
+ 'course_id': ('xmodule_django.models.CourseKeyField', [], {'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': ('xmodule_django.models.LocationKeyField', [], {'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.studentmodulehistory': {
+ 'Meta': {'object_name': 'StudentModuleHistory'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'student_module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courseware.StudentModule']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'})
+ },
+ 'courseware.xmodulestudentinfofield': {
+ 'Meta': {'unique_together': "(('student', 'field_name'),)", 'object_name': 'XModuleStudentInfoField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmodulestudentprefsfield': {
+ 'Meta': {'unique_together': "(('student', 'module_type', 'field_name'),)", 'object_name': 'XModuleStudentPrefsField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'module_type': ('xmodule_django.models.BlockTypeKeyField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ },
+ 'courseware.xmoduleuserstatesummaryfield': {
+ 'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleUserStateSummaryField'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'usage_id': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
+ }
+ }
+
+ complete_apps = ['courseware']
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index 75a7afffa3..0b5e650677 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -18,6 +18,8 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
+from model_utils.models import TimeStampedModel
+
from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField
@@ -232,7 +234,7 @@ class OfflineComputedGradeLog(models.Model):
return "[OCGLog] %s: %s" % (self.course_id.to_deprecated_string(), self.created) # pylint: disable=no-member
-class StudentFieldOverride(models.Model):
+class StudentFieldOverride(TimeStampedModel):
"""
Holds the value of a specific field overriden for a student. This is used
by the code in the `courseware.student_field_overrides` module to provide
@@ -243,7 +245,7 @@ class StudentFieldOverride(models.Model):
student = models.ForeignKey(User, db_index=True)
class Meta: # pylint: disable=missing-docstring
- unique_together = (('course_id', 'location', 'student'),)
+ unique_together = (('course_id', 'field', 'location', 'student'),)
field = models.CharField(max_length=255)
value = models.TextField(default='null')
diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py
index 1f6e596b63..7be040a674 100644
--- a/lms/djangoapps/instructor/tests/test_tools.py
+++ b/lms/djangoapps/instructor/tests/test_tools.py
@@ -242,6 +242,12 @@ class TestSetDueDateExtension(ModuleStoreTestCase):
self.assertEqual(self.homework.due, extended)
self.assertEqual(self.assignment.due, extended)
+ def test_set_due_date_extension_num_queries(self):
+ extended = datetime.datetime(2013, 12, 25, 0, 0, tzinfo=utc)
+ with self.assertNumQueries(4):
+ tools.set_due_date_extension(self.course, self.week1, self.user, extended)
+ self._clear_field_data_cache()
+
def test_set_due_date_extension_invalid_date(self):
extended = datetime.datetime(2009, 1, 1, 0, 0, tzinfo=utc)
with self.assertRaises(tools.DashboardError):
diff --git a/lms/static/js/ccx/schedule.js b/lms/static/js/ccx/schedule.js
index 328019c02a..9773669f49 100644
--- a/lms/static/js/ccx/schedule.js
+++ b/lms/static/js/ccx/schedule.js
@@ -54,55 +54,6 @@ var edx = edx || {};
self.schedule_collection.set(self.schedule);
self.render();
});
- },
-
- render: function() {
- self.schedule = this.schedule_collection.toJSON();
- self.hidden = this.pruned(self.schedule, function(node) {
- return node.hidden || node.category !== 'vertical'});
- this.showing = this.pruned(self.schedule, function(node) {
- return !node.hidden});
- this.$el.html(schedule_template({chapters: this.showing}));
- $('table.ccx-schedule .sequential,.vertical').hide();
- $('table.ccx-schedule .toggle-collapse').on('click', this.toggle_collapse);
- //
- // Hidden hover fields for empty date fields
- $('table.ccx-schedule .date a').each(function() {
- if (! $(this).text()) {
- $(this).text('Set date').addClass('empty');
- }
- });
-
- // Handle date edit clicks
- $('table.ccx-schedule .date a').attr('href', '#enter-date-modal')
- .leanModal({closeButton: '.close-modal'});
- $('table.ccx-schedule .due-date a').on('click', this.enterNewDate('due'));
- $('table.ccx-schedule .start-date a').on('click', this.enterNewDate('start'));
- // Click handler for remove all
- $('table.ccx-schedule a#remove-all').on('click', function(event) {
- event.preventDefault();
- self.schedule_apply(self.schedule, self.hide);
- self.dirty = true;
- self.schedule_collection.set(self.schedule);
- self.render();
- });
-
- // Show or hide form
- if (this.hidden.length) {
- // Populate chapters select, depopulate others
- this.chapter_select.html('')
- .append('')
- .append(self.schedule_options(this.hidden));
- this.sequential_select.html('').prop('disabled', true);
- this.vertical_select.html('').prop('disabled', true);
- $('form#add-unit').show();
- $('#all-units-added').hide();
- $('#add-unit-button').prop('disabled', true);
- }
- else {
- $('form#add-unit').hide();
- $('#all-units-added').show();
- }
// Add unit handlers
this.chapter_select.on('change', function(event) {
@@ -174,6 +125,44 @@ var edx = edx || {};
self.render();
});
+ // Handle save button
+ $('#dirty-schedule #save-changes').on('click', function(event) {
+ event.preventDefault();
+ self.save();
+ });
+
+ },
+
+ render: function() {
+ self.schedule = this.schedule_collection.toJSON();
+ self.hidden = this.pruned(self.schedule, function(node) {
+ return node.hidden || node.category !== 'vertical'});
+ this.showing = this.pruned(self.schedule, function(node) {
+ return !node.hidden});
+ this.$el.html(schedule_template({chapters: this.showing}));
+ $('table.ccx-schedule .sequential,.vertical').hide();
+ $('table.ccx-schedule .toggle-collapse').on('click', this.toggle_collapse);
+ //
+ // Hidden hover fields for empty date fields
+ $('table.ccx-schedule .date a').each(function() {
+ if (! $(this).text()) {
+ $(this).text('Set date').addClass('empty');
+ }
+ });
+
+ // Handle date edit clicks
+ $('table.ccx-schedule .date a').attr('href', '#enter-date-modal')
+ .leanModal({closeButton: '.close-modal'});
+ $('table.ccx-schedule .due-date a').on('click', this.enterNewDate('due'));
+ $('table.ccx-schedule .start-date a').on('click', this.enterNewDate('start'));
+ // Click handler for remove all
+ $('table.ccx-schedule a#remove-all').on('click', function(event) {
+ event.preventDefault();
+ self.schedule_apply(self.schedule, self.hide);
+ self.dirty = true;
+ self.schedule_collection.set(self.schedule);
+ self.render();
+ });
// Remove unit handler
$('table.ccx-schedule a.remove-unit').on('click', function(event) {
var row = $(this).closest('tr'),
@@ -185,16 +174,28 @@ var edx = edx || {};
self.render();
});
+
+ // Show or hide form
+ if (this.hidden.length) {
+ // Populate chapters select, depopulate others
+ this.chapter_select.html('')
+ .append('')
+ .append(self.schedule_options(this.hidden));
+ this.sequential_select.html('').prop('disabled', true);
+ this.vertical_select.html('').prop('disabled', true);
+ $('form#add-unit').show();
+ $('#all-units-added').hide();
+ $('#add-unit-button').prop('disabled', true);
+ }
+ else {
+ $('form#add-unit').hide();
+ $('#all-units-added').show();
+ }
+
// Show or hide save button
if (this.dirty) $('#dirty-schedule').show()
else $('#dirty-schedule').hide();
- // Handle save button
- $('#dirty-schedule #save-changes').on('click', function(event) {
- event.preventDefault();
- self.save();
- });
-
$('#ajax-error').hide();
return this;