MIT: CCX. Fix issues identified in code review
Original Commit Messages: use edx's own get_parent method, rather than our own. add field to unique constraint to avoid MultipleObjectsReturned in case of multiple browser clicks on submit fix 0011 migration, inherit from TimeStampedField and add composite index (migration only) fix bug where adding an already registered user to a ccx would cause a crash due to an undefined variable add assertNumQueries tests to test modules where override field providers are used remove unnecessary teardown implement recommended style for checking empty list import utility methods rather than use duplicate code added comment explaining date conversion to string for json add logging for invalid users or emails when enrolling students add comment about xmodule user state avoid using get_or_create, which seems to be causing a race condition on schedule change save relocate badly placed edvent handlers to fix multiple submit problem
This commit is contained in:
committed by
cewing
parent
3425424696
commit
b3da2a54bb
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']
|
||||
@@ -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']
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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('<option value="none">'+gettext("Select a chapter")+'...</option>')
|
||||
.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('<option value="none">'+gettext("Select a chapter")+'...</option>')
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user