Commit 70cc5008 by Christina Roberts

Merge pull request #916 from edx/christina/due-date-format

Allow custom formatting of due date strings.
parents 681e40db 2c9fc59d
......@@ -2,7 +2,6 @@ from xmodule.modulestore import Location
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):
......
......@@ -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 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
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)
......
"""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():
......
......@@ -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)):
......
<section class="about">
<h2>About This Course</h2>
<p>Include your long course description here. The long course description should contain 150-400 words.</p>
<p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>
</section>
<section class="prerequisites">
<h2>Prerequisites</h2>
<p>Add information about course prerequisites here.</p>
</section>
<section class="course-staff">
<h2>Course Staff</h2>
<article class="teacher">
<div class="teacher-image">
<img src="/static/images/pl-faculty.png" align="left" style="margin:0 20 px 0">
</div>
<h3>Staff Member #1</h3>
<p>Biography of instructor/staff member #1</p>
</article>
<article class="teacher">
<div class="teacher-image">
<img src="/static/images/pl-faculty.png" align="left" style="margin:0 20 px 0">
</div>
<h3>Staff Member #2</h3>
<p>Biography of instructor/staff member #2</p>
</article>
</section>
<section class="faq">
<section class="responses">
<h2>Frequently Asked Questions</h2>
<article class="response">
<h3>Do I need to buy a textbook?</h3>
<p>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.</p>
</article>
<article class="response">
<h3>Question #2</h3>
<p>Your answer would be displayed here.</p>
</article>
</section>
</section>
<chapter display_name="Section">
<sequential url_name="c804fa32227142a1bd9d5bc183d4a20d"/>
</chapter>
<course url_name="2013_fall" org="edX" course="due_date"/>
<course display_name="due_date">
<chapter url_name="c8ee0db7e5a84c85bac80b7013cf6512"/>
</course>
{"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
{"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
<problem display_name="Multiple Choice" markdown="A multiple choice problem presents radio buttons for student input. Students can only select a single &#10;option presented. Multiple Choice questions have been the subject of many areas of research due to the early &#10;invention and adoption of bubble sheets.&#10;&#10;One of the main elements that goes into a good multiple choice question is the existence of good distractors. &#10;That is, each of the alternate responses presented to the student should be the result of a plausible mistake &#10;that a student might make.&#10;&#10;What Apple device competed with the portable CD player?&#10; ( ) The iPad&#10; ( ) Napster&#10; (x) The iPod&#10; ( ) The vegetable peeler&#10; &#10;[explanation]&#10;The release of the iPod allowed consumers to carry their entire music library with them in a &#10;format that did not rely on fragile and energy-intensive spinning disks.&#10;[explanation]&#10;">
<p>
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.</p>
<p> 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.
</p>
<p>What Apple device competed with the portable CD player?</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false" name="ipad">The iPad</choice>
<choice correct="false" name="beatles">Napster</choice>
<choice correct="true" name="ipod">The iPod</choice>
<choice correct="false" name="peeler">The vegetable peeler</choice>
</choicegroup>
</multiplechoiceresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>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. </p>
</div>
</solution>
</problem>
<sequential display_name="Subsection" due="2013-09-18T11:30:00Z" start="1970-01-01T00:00:00Z">
<vertical url_name="45640305a210424ebcc6f8e045fad0be"/>
</sequential>
<vertical display_name="New Unit">
<problem url_name="d392c80f5c044e45a4a5f2d62f94efc5"/>
</vertical>
......@@ -22,5 +22,6 @@ MAPPINGS = {
'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)
......@@ -14,7 +14,7 @@ from student.models import CourseEnrollment
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 @@ class TestJumpTo(TestCase):
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 @@ class ViewsTestCase(TestCase):
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 @@ class ViewsTestCase(TestCase):
})
response = self.client.get(url)
self.assertFalse('<script>' in response.content)
def test_accordion_due_date(self):
"""
Tests the formatting of due dates in the accordion view.
"""
def get_accordion():
""" Returns the HTML for the accordion """
return views.render_accordion(
request, modulestore().get_course("edX/due_date/2013_fall"),
"c804fa32227142a1bd9d5bc183d4a20d", None, None
)
request = self.request_factory.get("foo")
self.verify_due_date(request, get_accordion)
def test_progress_due_date(self):
"""
Tests the formatting of due dates in the progress page.
"""
def get_progress():
""" Returns the HTML for the progress page """
return views.progress(request, "edX/due_date/2013_fall", self.user.id).content
request = self.request_factory.get("foo")
self.verify_due_date(request, get_progress)
def verify_due_date(self, request, get_text):
"""
Verifies that due dates are formatted properly in text returned by get_text function.
"""
def set_show_timezone(show_timezone):
"""
Sets the show_timezone property and returns value from get_text function.
Note that show_timezone is deprecated and cannot be set by the user.
"""
course.show_timezone = show_timezone
course.save()
return get_text()
def set_due_date_format(due_date_format):
"""
Sets the due_date_display_format property and returns value from get_text function.
"""
course.due_date_display_format = due_date_format
course.save()
return get_text()
request.user = self.user
# Clear out the modulestores, so we start with the test course in its default state.
clear_existing_modulestores()
course = modulestore().get_course("edX/due_date/2013_fall")
time_with_utc = "due Sep 18, 2013 at 11:30 UTC"
time_without_utc = "due Sep 18, 2013 at 11:30"
# The test course being used has show_timezone = False in the policy file
# (and no due_date_display_format set). This is to test our backwards compatibility--
# in course_module's init method, the date_display_format will be set accordingly to
# remove the timezone.
text = get_text()
self.assertIn(time_without_utc, text)
self.assertNotIn(time_with_utc, text)
# Test that show_timezone has been cleared (which means you get the default value of True).
self.assertTrue(course.show_timezone)
# Clear out the due date format and verify you get the default (with timezone).
delattr(course, 'due_date_display_format')
course.save()
text = get_text()
self.assertIn(time_with_utc, text)
# Same for setting the due date to None
text = set_due_date_format(None)
self.assertIn(time_with_utc, text)
# plain text due date
text = set_due_date_format("foobar")
self.assertNotIn(time_with_utc, text)
self.assertIn("due foobar", text)
# due date with no time
text = set_due_date_format(u"%b %d %y")
self.assertNotIn(time_with_utc, text)
self.assertIn("due Sep 18 13", text)
# hide due date completely
text = set_due_date_format(u"")
self.assertNotIn("due ", text)
# improperly formatted due_date_display_format falls through to default
# (value of show_timezone does not matter-- setting to False to make that clear).
set_show_timezone(False)
text = set_due_date_format(u"%%%")
self.assertNotIn("%%%", text)
self.assertIn(time_with_utc, text)
......@@ -98,7 +98,7 @@ def render_accordion(request, course, chapter, section, field_data_cache):
context = dict([('toc', toc),
('course_id', course.id),
('csrf', csrf(request)['csrf_token']),
('show_timezone', course.show_timezone)] + template_imports.items())
('due_date_display_format', course.due_date_display_format)] + template_imports.items())
return render_to_string('courseware/accordion.html', context)
......
<%!
from django.core.urlresolvers import reverse
from xmodule.util.date_utils import get_default_time_display
from xmodule.util.date_utils import get_time_display
from django.utils.translation import ugettext as _
%>
......@@ -25,7 +25,14 @@
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
<p>${section['display_name']} ${'<span class="sr">, current section</span>' if 'active' in section and section['active'] else ''}</p>
<p class="subtitle">${section['format']} ${"due " + get_default_time_display(section['due'], show_timezone) if section.get('due') is not None else ''}</p>
<%
if section.get('due') is None:
due_date = ''
else:
formatted_string = get_time_display(section['due'], due_date_display_format)
due_date = '' if len(formatted_string)==0 else _('due {date}'.format(date=formatted_string))
%>
<p class="subtitle">${section['format']} ${due_date}</p>
</a>
</li>
% endfor
......
......@@ -14,7 +14,7 @@
from django.core.urlresolvers import reverse
%>
<%! from xmodule.util.date_utils import get_default_time_display %>
<%! from xmodule.util.date_utils import get_time_display %>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
......@@ -69,8 +69,12 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
${section['format']}
%if section.get('due') is not None:
<%
formatted_string = get_time_display(section['due'], course.due_date_display_format)
due_date = '' if len(formatted_string)==0 else _('due {date}'.format(date=formatted_string))
%>
<em>
due ${get_default_time_display(section['due'], course.show_timezone)}
${due_date}
</em>
%endif
</p>
......
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