Commit 9f04f19a by chrisndodge

Merge pull request #1205 from MITx/bug/dhm/dec12

Bug/dhm/dec12
parents b65775c3 83bc9d7b
......@@ -11,15 +11,21 @@ from cms.djangoapps.models.settings.course_details import CourseDetails,\
import json
from util import converters
import calendar
from util.converters import jsdate_to_time
from django.utils.timezone import UTC
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
import copy
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase):
def struct_to_datetime(self, struct_time):
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour, struct_time.tm_min, struct_time.tm_sec)
def compare_dates(self, date1, date2, expected_delta):
dt1 = self.struct_to_datetime(date1)
dt2 = self.struct_to_datetime(date2)
dt1 = ConvertersTestCase.struct_to_datetime(date1)
dt2 = ConvertersTestCase.struct_to_datetime(date2)
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta))
def test_iso_to_struct(self):
......@@ -28,7 +34,8 @@ class ConvertersTestCase(TestCase):
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1))
class CourseDetailsTestCase(TestCase):
class CourseTestCase(TestCase):
def setUp(self):
uname = 'testuser'
email = 'test+courses@edx.org'
......@@ -75,6 +82,7 @@ class CourseDetailsTestCase(TestCase):
"""Create new course"""
self.client.post(reverse('create_new_course'), self.course_data)
class CourseDetailsTestCase(CourseTestCase):
def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course_location)
self.assertEqual(details.course_location, self.course_location, "Location not copied into")
......@@ -104,6 +112,7 @@ class CourseDetailsTestCase(TestCase):
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
jsondetails = CourseDetails.fetch(self.course_location)
jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
jsondetails.syllabus, "After set syllabus")
jsondetails.overview = "Overview"
......@@ -116,58 +125,12 @@ class CourseDetailsTestCase(TestCase):
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
jsondetails.effort, "After set effort")
class CourseDetailsViewTest(TestCase):
def setUp(self):
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
# Create the use so we can log them in.
self.user = User.objects.create_user(uname, email, password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
self.user.save()
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
xmodule.templates.update_templates()
self.client = Client()
self.client.login(username=uname, password=password)
self.course_data = {
'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
}
self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course')
self.create_course()
def tearDown(self):
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
def create_course(self):
"""Create new course"""
self.client.post(reverse('create_new_course'), self.course_data)
class CourseDetailsViewTest(CourseTestCase):
def alter_field(self, url, details, field, val):
setattr(details, field, val)
# jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
resp = self.client.post(url, details)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + val)
# FIXME post is not invoking views.course_settings_updates
resp = self.client.post(url, details.__dict__, "application/json")
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location)
......@@ -182,13 +145,13 @@ class CourseDetailsViewTest(TestCase):
resp = self.client.get(url)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
# self.alter_field(url, details, 'start_date', time.time() * 1000)
# self.alter_field(url, details, 'start_date', time.time() * 1000 + 60 * 60 * 24)
# self.alter_field(url, details, 'end_date', time.time() * 1000 + 60 * 60 * 24 * 100)
# self.alter_field(url, details, 'enrollment_start', time.time() * 1000)
utc = UTC()
# self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc))
# self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc))
# self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc))
# self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc))
#
# self.alter_field(url, details, 'enrollment_end', time.time() * 1000 + 60 * 60 * 24 * 8)
# self.alter_field(url, details, 'syllabus', "<a href='foo'>bar</a>")
# self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc))
# self.alter_field(url, details, 'overview', "Overview")
# self.alter_field(url, details, 'intro_video', "intro_video")
# self.alter_field(url, details, 'effort', "effort")
......@@ -205,9 +168,75 @@ class CourseDetailsViewTest(TestCase):
def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None:
if field in encoded and encoded[field] is not None:
self.assertEqual(encoded[field] / 1000, calendar.timegm(details[field]), "dates not == at " + context)
encoded_encoded = jsdate_to_time(encoded[field])
details_encoded = jsdate_to_time(details[field])
dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded)
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(encoded_encoded) + "!=" + str(details_encoded) + " at " + context)
else:
self.fail(field + " missing from encoded but in details at " + context)
elif field in encoded and encoded[field] is not None:
self.fail(field + " included in encoding but missing from details at " + context)
class CourseGradingTest(CourseTestCase):
def test_initial_grader(self):
descriptor = get_modulestore(self.course_location).get_item(self.course_location)
test_grader = CourseGradingModel(descriptor)
# ??? How much should this test bake in expectations about defaults and thus fail if defaults change?
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
self.assertIsNotNone(test_grader.graders, "No graders")
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
def test_fetch_grader(self):
test_grader = CourseGradingModel.fetch(self.course_location.url())
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
self.assertIsNotNone(test_grader.graders, "No graders")
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
test_grader = CourseGradingModel.fetch(self.course_location)
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
self.assertIsNotNone(test_grader.graders, "No graders")
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
for i, grader in enumerate(test_grader.graders):
subgrader = CourseGradingModel.fetch_grader(self.course_location, i)
self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0)
self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list")
def test_fetch_cutoffs(self):
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location)
# ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think)
self.assertIsNotNone(test_grader, "No cutoffs via fetch")
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url())
self.assertIsNotNone(test_grader, "No cutoffs via fetch with url")
def test_fetch_grace(self):
test_grader = CourseGradingModel.fetch_grace_period(self.course_location)
# almost a worthless test
self.assertIn('grace_period', test_grader, "No grace via fetch")
test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url())
self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url")
def test_update_from_json(self):
test_grader = CourseGradingModel.fetch(self.course_location)
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
test_grader.grade_cutoffs['D'] = 0.3
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
test_grader.grace_period = {'hours' : '4'}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
\ No newline at end of file
......@@ -322,7 +322,7 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else 'Unset',
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
......
from xmodule.modulestore import Location
class CourseFaculty:
def __init__(self, location):
if not isinstance(location, Location):
location = Location(location)
# course_location is used so that updates know where to get the relevant data
self.course_location = location
self.first_name = ""
self.last_name = ""
self.photo = None
self.bio = ""
@classmethod
def fetch(cls, course_location):
"""
Fetch a list of faculty for the course
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
# Must always have at least one faculty member (possibly empty)
\ No newline at end of file
......@@ -237,7 +237,7 @@ class CourseGradingModel:
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.metadata.get('graceperiod', None)
if rawgrace:
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d*)\s*(\w*)', rawgrace)}
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
return parsedgrace
else: return None
......
......@@ -111,7 +111,7 @@
<input class="start-date date" type="text" name="start_date" value="" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input class="start-time time" type="text" name="start_time" value="" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<div class="description">
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students along with the 5 subsections within it. Any units marked private will only be visible to admins.</p>
<p>On the date set above, this section – <strong class="section-name"></strong> – will be released to students. Any units marked private will only be visible to admins.</p>
</div>
</div>
<a href="#" class="save-button">Save</a><a href="#" class="cancel-button">Cancel</a>
......
......@@ -81,7 +81,11 @@
<p class="publish-draft-message">This is a draft of the published unit. To update the live version, you must <a href="#" class="publish-draft">replace it with this draft</a>.</p>
</div>
<div class="row status">
<p>This unit is scheduled to be released to <strong>students</strong> on <strong>${release_date}</strong> with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
<p>This unit is scheduled to be released to <strong>students</strong>
% if release_date is not None:
on <strong>${release_date}</strong>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
</div>
<div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a>
......
......@@ -19,4 +19,6 @@ def jsdate_to_time(field):
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple()
elif isinstance(field, int) or isinstance(field, float):
return time.gmtime(field / 1000)
\ No newline at end of file
return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time):
return field
\ No newline at end of file
......@@ -116,7 +116,6 @@ class CourseDescriptor(SequenceDescriptor):
"type" : "Lab",
"min_count" : 12,
"drop_count" : 2,
"category" : "Labs",
"weight" : 0.15
},
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment