From 50d7e6160e5edbb8b76aaad374e92ea9a92dc3ca Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 3 Dec 2012 12:20:15 -0500 Subject: [PATCH] Move models from common to cms. Add unit tests. --- cms/djangoapps/__init__.py | 0 .../tests/test_course_settings.py | 194 ++++++++++++++++++ cms/djangoapps/contentstore/views.py | 3 +- cms/djangoapps/models/__init__.py | 0 .../models/settings/course_details.py | 2 - .../models/settings/course_faculty.py | 22 ++ common/djangoapps/util/converters.py | 8 +- 7 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 cms/djangoapps/__init__.py create mode 100644 cms/djangoapps/contentstore/tests/test_course_settings.py create mode 100644 cms/djangoapps/models/__init__.py create mode 100644 cms/djangoapps/models/settings/course_faculty.py diff --git a/cms/djangoapps/__init__.py b/cms/djangoapps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py new file mode 100644 index 0000000000..4495683a01 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -0,0 +1,194 @@ +from django.test.testcases import TestCase +import datetime +import time +from django.contrib.auth.models import User +import xmodule +from django.test.client import Client +from django.core.urlresolvers import reverse +from xmodule.modulestore import Location +from cms.djangoapps.models.settings.course_details import CourseDetails,\ + CourseDetailsEncoder +import json +from common.djangoapps.util import converters + +# YYYY-MM-DDThh:mm:ss.s+/-HH:MM +class ConvertersTestCase(TestCase): + def struct_to_datetime(self, 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) + self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta)) + + def test_iso_to_struct(self): + self.compare_dates(converters.jsdate_to_time("2013-01-01"), converters.jsdate_to_time("2012-12-31"), datetime.timedelta(days=1)) + self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), converters.jsdate_to_time("2012-12-31T23"), datetime.timedelta(hours=1)) + 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): + 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) + + def test_virgin_fetch(self): + details = CourseDetails.fetch(self.course_location) + self.assertEqual(details.course_location, self.course_location, "Location not copied into") + self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date)) + self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)) + self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end)) + self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus)) + self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview) + self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) + self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) + + def test_encoder(self): + details = CourseDetails.fetch(self.course_location) + jsondetails = json.dumps(details, cls=CourseDetailsEncoder) + jsondetails = json.loads(jsondetails) + self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=") + # Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense. + self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ") + self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") + self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ") + self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized") + self.assertEqual(jsondetails['overview'], "", "overview somehow initialized") + self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized") + self.assertIsNone(jsondetails['effort'], "effort somehow initialized") + + def test_update_and_fetch(self): + ## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions + jsondetails = CourseDetails.fetch(self.course_location) + jsondetails.syllabus = "bar" + self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus, + jsondetails.syllabus, "After set syllabus") + jsondetails.overview = "Overview" + self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview, + jsondetails.overview, "After set overview") + jsondetails.intro_video = "intro_video" + self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video, + jsondetails.intro_video, "After set intro_video") + jsondetails.effort = "effort" + 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) + + def alter_field(self, url, details, field, val): + details[field] = val + jsondetails = json.dumps(details, cls=CourseDetailsEncoder) + resp = self.client.post(url, jsondetails) + self.assertDictEqual(json.loads(resp), details, field + val) + + def test_update_and_fetch(self): + details = CourseDetails.fetch(self.course_location) + details_loc = self.course_location.dict().copy() + details_loc['section'] = 'details' + + resp = self.client.get(reverse('contentstore.views.get_course_settings', kwargs=self.course_location.dict())) + self.assertContains(resp, '
  • Course Details
  • ', status_code=200, html=True) + + # resp s/b json from here on + url = reverse('contentstore.views.course_settings_updates', kwargs=details_loc) + resp = self.client.get(url) + jsondetails = json.dumps(details, cls=CourseDetailsEncoder) + self.assertDictEqual(resp, jsondetails, "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) + + self.alter_field(url, details, 'enrollment_end', time.time() * 1000 + 60 * 60 * 24 * 8) + self.alter_field(url, details, 'syllabus', "bar") + self.alter_field(url, details, 'overview', "Overview") + self.alter_field(url, details, 'intro_video', "intro_video") + self.alter_field(url, details, 'effort', "effort") diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index e239da2a19..5b91b77554 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -955,8 +955,7 @@ def get_course_settings(request, org, course, name): return render_to_response('settings.html', { 'active_tab': 'settings-tab', 'context_course': course_module, - 'course_details' : json.dumps(course_details, cls=CourseDetailsEncoder), - 'video_editor_html' : preview_component(request, course_details.intro_video_loc) + 'course_details' : json.dumps(course_details, cls=CourseDetailsEncoder) }) @expect_json diff --git a/cms/djangoapps/models/__init__.py b/cms/djangoapps/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index ea800a3173..7f89589c60 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -17,7 +17,6 @@ class CourseDetails: self.syllabus = None # a pdf file asset self.overview = "" # html to render as the overview self.intro_video = None # a video pointer - self.intro_video_loc = None # a video pointer self.effort = None # int hours/week @classmethod @@ -58,7 +57,6 @@ class CourseDetails: temploc = temploc._replace(name='video') try: course.intro_video = get_modulestore(temploc).get_item(temploc).definition['data'] - course.intro_video_loc = temploc except ItemNotFoundError: pass diff --git a/cms/djangoapps/models/settings/course_faculty.py b/cms/djangoapps/models/settings/course_faculty.py new file mode 100644 index 0000000000..c1812614ec --- /dev/null +++ b/cms/djangoapps/models/settings/course_faculty.py @@ -0,0 +1,22 @@ +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 diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py index 6070338b55..4ff5b287f3 100644 --- a/common/djangoapps/util/converters.py +++ b/common/djangoapps/util/converters.py @@ -10,12 +10,12 @@ def time_to_date(time_obj): def jsdate_to_time(field): """ - Convert a true universal time (msec since epoch) from a string to a time obj + Convert a universal time (iso format) or msec since epoch to a time obj """ if field is None: return field - elif isinstance(field, unicode): # iso format but ignores time zone assuming it's Z - d=datetime.datetime(*map(int, re.split('[^\d]', field)[:-1])) + elif isinstance(field, unicode) or isinstance(field, str): # iso format but ignores time zone assuming it's Z + d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable return d.utctimetuple() - elif isinstance(field, int): + elif isinstance(field, int) or isinstance(field, float): return time.gmtime(field / 1000) \ No newline at end of file