diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index f186fbf11648..603865b8846b 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -2,7 +2,6 @@ from contentstore.utils import get_modulestore from xmodule.modulestore.inheritance import own_metadata from xblock.fields import Scope -from xmodule.course_module import CourseDescriptor from cms.xmodule_namespace import CmsBlockMixin @@ -20,7 +19,9 @@ class CourseMetadata(object): 'enrollment_end', 'tabs', 'graceperiod', - 'checklists'] + 'checklists', + 'show_timezone' + ] @classmethod def fetch(cls, course_location): diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 658a095d1480..24e6995ae105 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -337,7 +337,14 @@ class CourseFields(object): "action_external": False}]} ]) info_sidebar_name = String(scope=Scope.settings, default='Course Handouts') - show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True) + show_timezone = Boolean( + help="True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.", + scope=Scope.settings, default=True + ) + due_date_display_format = String( + help="Format supported by strftime for displaying due dates. Takes precedence over show_timezone.", + scope=Scope.settings, default=None + ) enrollment_domain = String(help="External login method associated with user accounts allowed to register in course", scope=Scope.settings) course_image = String( @@ -391,7 +398,13 @@ def __init__(self, *args, **kwargs): elif isinstance(self.location, CourseLocator): self.wiki_slug = self.location.course_id or self.display_name - msg = None + if self.due_date_display_format is None and self.show_timezone is False: + # For existing courses with show_timezone set to False (and no due_date_display_format specified), + # set the due_date_display_format to what would have been shown previously (with no timezone). + # Then remove show_timezone so that if the user clears out the due_date_display_format, + # they get the default date display. + self.due_date_display_format = u"%b %d, %Y at %H:%M" + delattr(self, 'show_timezone') # NOTE: relies on the modulestore to call set_grading_policy() right after # init. (Modulestore is in charge of figuring out where to load the policy from) diff --git a/common/lib/xmodule/xmodule/tests/test_date_utils.py b/common/lib/xmodule/xmodule/tests/test_date_utils.py index 37f30e1b566d..c5f71dfb56e8 100644 --- a/common/lib/xmodule/xmodule/tests/test_date_utils.py +++ b/common/lib/xmodule/xmodule/tests/test_date_utils.py @@ -1,7 +1,7 @@ """Tests for xmodule.util.date_utils""" from nose.tools import assert_equals, assert_false # pylint: disable=E0611 -from xmodule.util.date_utils import get_default_time_display, almost_same_datetime +from xmodule.util.date_utils import get_default_time_display, get_time_display, almost_same_datetime from datetime import datetime, timedelta, tzinfo from pytz import UTC @@ -12,25 +12,34 @@ def test_get_default_time_display(): assert_equals( "Mar 12, 1992 at 15:03 UTC", get_default_time_display(test_time)) - assert_equals( - "Mar 12, 1992 at 15:03 UTC", - get_default_time_display(test_time, True)) - assert_equals( - "Mar 12, 1992 at 15:03", - get_default_time_display(test_time, False)) -def test_get_default_time_display_notz(): +def test_get_dflt_time_disp_notz(): test_time = datetime(1992, 3, 12, 15, 3, 30) assert_equals( "Mar 12, 1992 at 15:03 UTC", get_default_time_display(test_time)) - assert_equals( - "Mar 12, 1992 at 15:03 UTC", - get_default_time_display(test_time, True)) - assert_equals( - "Mar 12, 1992 at 15:03", - get_default_time_display(test_time, False)) + + +def test_get_time_disp_ret_empty(): + assert_equals("", get_time_display(None)) + test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC) + assert_equals("", get_time_display(test_time, "")) + + +def test_get_time_display(): + test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC) + assert_equals("dummy text", get_time_display(test_time, 'dummy text')) + assert_equals("Mar 12 1992", get_time_display(test_time, '%b %d %Y')) + assert_equals("Mar 12 1992 UTC", get_time_display(test_time, '%b %d %Y %Z')) + assert_equals("Mar 12 15:03", get_time_display(test_time, '%b %d %H:%M')) + + +def test_get_time_pass_through(): + test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC) + assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time)) + assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time, None)) + assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time, "%")) # pylint: disable=W0232 @@ -50,12 +59,6 @@ def test_get_default_time_display_no_tzname(): assert_equals( "Mar 12, 1992 at 15:03-0300", get_default_time_display(test_time)) - assert_equals( - "Mar 12, 1992 at 15:03-0300", - get_default_time_display(test_time, True)) - assert_equals( - "Mar 12, 1992 at 15:03", - get_default_time_display(test_time, False)) def test_almost_same_datetime(): diff --git a/common/lib/xmodule/xmodule/util/date_utils.py b/common/lib/xmodule/xmodule/util/date_utils.py index 8baa59558b3e..aec4f20788ad 100644 --- a/common/lib/xmodule/xmodule/util/date_utils.py +++ b/common/lib/xmodule/xmodule/util/date_utils.py @@ -2,32 +2,46 @@ Convenience methods for working with datetime objects """ from datetime import timedelta -from django.utils.translation import ugettext as _ -def get_default_time_display(dt, show_timezone=True): +def get_default_time_display(dtime): """ Converts a datetime to a string representation. This is the default representation used in Studio and LMS. - It is of the form "Apr 09, 2013 at 16:00" or "Apr 09, 2013 at 16:00 UTC", - depending on the value of show_timezone. + It is of the form "Apr 09, 2013 at 16:00 UTC". If None is passed in for dt, an empty string will be returned. - The default value of show_timezone is True. """ - if dt is None: + if dtime is None: return u"" - timezone = u"" - if show_timezone: - if dt.tzinfo is not None: - try: - timezone = u" " + dt.tzinfo.tzname(dt) - except NotImplementedError: - timezone = dt.strftime('%z') - else: - timezone = u" UTC" - return unicode(dt.strftime(u"%b %d, %Y {at} %H:%M{tz}")).format( - at=_(u"at"), tz=timezone).strip() + if dtime.tzinfo is not None: + try: + timezone = u" " + dtime.tzinfo.tzname(dtime) + except NotImplementedError: + timezone = dtime.strftime('%z') + else: + timezone = u" UTC" + return unicode(dtime.strftime(u"%b %d, %Y at %H:%M{tz}")).format( + tz=timezone).strip() + + +def get_time_display(dtime, format_string=None): + """ + Converts a datetime to a string representation. + + If None is passed in for dt, an empty string will be returned. + + If the format_string is None, or if format_string is improperly + formatted, this method will return the value from `get_default_time_display`. + + format_string should be a unicode string that is a valid argument for datetime's strftime method. + """ + if dtime is None or format_string is None: + return get_default_time_display(dtime) + try: + return unicode(dtime.strftime(format_string)) + except ValueError: + return get_default_time_display(dtime) def almost_same_datetime(dt1, dt2, allowed_delta=timedelta(minutes=1)): diff --git a/common/test/data/due_date/about/overview.html b/common/test/data/due_date/about/overview.html new file mode 100644 index 000000000000..961786b8f407 --- /dev/null +++ b/common/test/data/due_date/about/overview.html @@ -0,0 +1,47 @@ +
+

About This Course

+

Include your long course description here. The long course description should contain 150-400 words.

+ +

This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.

+
+ +
+

Prerequisites

+

Add information about course prerequisites here.

+
+ +
+

Course Staff

+
+
+ +
+ +

Staff Member #1

+

Biography of instructor/staff member #1

+
+ +
+
+ +
+ +

Staff Member #2

+

Biography of instructor/staff member #2

+
+
+ +
+
+

Frequently Asked Questions

+
+

Do I need to buy a textbook?

+

No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.

+
+ +
+

Question #2

+

Your answer would be displayed here.

+
+
+
diff --git a/common/test/data/due_date/chapter/c8ee0db7e5a84c85bac80b7013cf6512.xml b/common/test/data/due_date/chapter/c8ee0db7e5a84c85bac80b7013cf6512.xml new file mode 100644 index 000000000000..cdc5d294f8cf --- /dev/null +++ b/common/test/data/due_date/chapter/c8ee0db7e5a84c85bac80b7013cf6512.xml @@ -0,0 +1,3 @@ + + + diff --git a/common/test/data/due_date/course.xml b/common/test/data/due_date/course.xml new file mode 100644 index 000000000000..e355f924b440 --- /dev/null +++ b/common/test/data/due_date/course.xml @@ -0,0 +1 @@ + diff --git a/common/test/data/due_date/course/2013_fall.xml b/common/test/data/due_date/course/2013_fall.xml new file mode 100644 index 000000000000..97ad3760ae03 --- /dev/null +++ b/common/test/data/due_date/course/2013_fall.xml @@ -0,0 +1,3 @@ + + + diff --git a/common/test/data/due_date/policies/2013_fall/grading_policy.json b/common/test/data/due_date/policies/2013_fall/grading_policy.json new file mode 100644 index 000000000000..272cb4fec6f4 --- /dev/null +++ b/common/test/data/due_date/policies/2013_fall/grading_policy.json @@ -0,0 +1 @@ +{"GRADER": [{"short_label": "HW", "min_count": 12, "type": "Homework", "drop_count": 2, "weight": 0.15}, {"min_count": 12, "type": "Lab", "drop_count": 2, "weight": 0.15}, {"short_label": "Midterm", "min_count": 1, "type": "Midterm Exam", "drop_count": 0, "weight": 0.3}, {"short_label": "Final", "min_count": 1, "type": "Final Exam", "drop_count": 0, "weight": 0.4}], "GRADE_CUTOFFS": {"Pass": 0.5}} \ No newline at end of file diff --git a/common/test/data/due_date/policies/2013_fall/policy.json b/common/test/data/due_date/policies/2013_fall/policy.json new file mode 100644 index 000000000000..42ba75ae29f0 --- /dev/null +++ b/common/test/data/due_date/policies/2013_fall/policy.json @@ -0,0 +1 @@ +{"course/2013_fall": {"tabs": [{"type": "courseware"}, {"type": "course_info", "name": "Course Info"}, {"type": "textbooks"}, {"type": "discussion", "name": "Discussion"}, {"type": "wiki", "name": "Wiki"}, {"type": "progress", "name": "Progress"}], "display_name": "due_date", "discussion_topics": {"General": {"id": "i4x-edX-due_date-course-2013_fall"}}, "show_timezone": "false"}} \ No newline at end of file diff --git a/common/test/data/due_date/problem/d392c80f5c044e45a4a5f2d62f94efc5.xml b/common/test/data/due_date/problem/d392c80f5c044e45a4a5f2d62f94efc5.xml new file mode 100644 index 000000000000..4e50ac396b77 --- /dev/null +++ b/common/test/data/due_date/problem/d392c80f5c044e45a4a5f2d62f94efc5.xml @@ -0,0 +1,23 @@ + +

+A multiple choice problem presents radio buttons for student +input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.

+

One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make. +

+ +

What Apple device competed with the portable CD player?

+ + + The iPad + Napster + The iPod + The vegetable peeler + + + +
+

Explanation

+

The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.

+
+
+
diff --git a/common/test/data/due_date/sequential/c804fa32227142a1bd9d5bc183d4a20d.xml b/common/test/data/due_date/sequential/c804fa32227142a1bd9d5bc183d4a20d.xml new file mode 100644 index 000000000000..a26ed3789fc6 --- /dev/null +++ b/common/test/data/due_date/sequential/c804fa32227142a1bd9d5bc183d4a20d.xml @@ -0,0 +1,3 @@ + + + diff --git a/common/test/data/due_date/vertical/45640305a210424ebcc6f8e045fad0be.xml b/common/test/data/due_date/vertical/45640305a210424ebcc6f8e045fad0be.xml new file mode 100644 index 000000000000..66b6fc546b04 --- /dev/null +++ b/common/test/data/due_date/vertical/45640305a210424ebcc6f8e045fad0be.xml @@ -0,0 +1,3 @@ + + + diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py index 74fd3da57fe8..9e887855c38c 100644 --- a/lms/djangoapps/courseware/tests/modulestore_config.py +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -22,5 +22,6 @@ 'edX/test_about_blob_end_date/2012_Fall': 'xml', 'edX/graded/2012_Fall': 'xml', 'edX/open_ended/2012_Fall': 'xml', + 'edX/due_date/2013_fall': 'xml' } TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, MAPPINGS) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index b4e835d2d978..1c87abd3bc6e 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -14,7 +14,7 @@ from student.tests.factories import AdminFactory from mitxmako.middleware import MakoMiddleware -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import modulestore, clear_existing_modulestores import courseware.views as views from xmodule.modulestore import Location @@ -31,9 +31,8 @@ class TestJumpTo(TestCase): def setUp(self): - # Load toy course from XML + # Use toy course from XML self.course_name = 'edX/toy/2012_Fall' - self.toy_course = modulestore().get_course(self.course_name) def test_jumpto_invalid_location(self): location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None) @@ -62,7 +61,9 @@ def test_jumpto_id_invalid_location(self): self.assertEqual(response.status_code, 404) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class ViewsTestCase(TestCase): + """ Tests for views.py methods. """ def setUp(self): self.user = User.objects.create(username='dummy', password='123456', email='test@mit.edu') @@ -73,8 +74,6 @@ def setUp(self): self.enrollment.save() self.location = ['tag', 'org', 'course', 'category', 'name'] - # This is a CourseDescriptor object - self.toy_course = modulestore().get_course('edX/toy/2012_Fall') self.request_factory = RequestFactory() chapter = 'Overview' self.chapter_url = '%s/%s/%s' % ('/courses', self.course_id, chapter) @@ -222,3 +221,99 @@ def test_submission_history_xss(self): }) response = self.client.get(url) self.assertFalse(' @@ -69,8 +69,12 @@