Allow custom formatting of due date strings.
STUD-724 Test course for allowing custom formatting of due date strings. STUD-724 Try making the name of the course match the folder name. More cleanup. More cleanup. updates.
This commit is contained in:
@@ -338,6 +338,10 @@ class CourseFields(object):
|
||||
])
|
||||
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)
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,28 @@ def test_get_default_time_display_notz():
|
||||
get_default_time_display(test_time, False))
|
||||
|
||||
|
||||
def test_get_time_display_return_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', True))
|
||||
assert_equals("Mar 12 1992 UTC", get_time_display(test_time, '%b %d %Y %Z', False))
|
||||
assert_equals("Mar 12 15:03", get_time_display(test_time, '%b %d %H:%M', False))
|
||||
|
||||
|
||||
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", get_time_display(test_time, None, False))
|
||||
assert_equals("Mar 12, 1992 at 15:03", get_time_display(test_time, "%", False))
|
||||
assert_equals("Mar 12, 1992 at 15:03 UTC", get_time_display(test_time, "%", True))
|
||||
|
||||
|
||||
# pylint: disable=W0232
|
||||
class NamelessTZ(tzinfo):
|
||||
"""Static timezone for testing"""
|
||||
|
||||
@@ -30,6 +30,25 @@ def get_default_time_display(dt, show_timezone=True):
|
||||
at=_(u"at"), tz=timezone).strip()
|
||||
|
||||
|
||||
def get_time_display(dt, format_string=None, show_timezone=True):
|
||||
"""
|
||||
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`
|
||||
(passing in the show_timezone argument).
|
||||
If the format_string is specified, show_timezone is ignored.
|
||||
format_string should be a unicode string that is a valid argument for datetime's strftime method.
|
||||
"""
|
||||
if dt is None or format_string is None:
|
||||
return get_default_time_display(dt, show_timezone)
|
||||
try:
|
||||
return unicode(dt.strftime(format_string))
|
||||
except ValueError:
|
||||
return get_default_time_display(dt, show_timezone)
|
||||
|
||||
|
||||
def almost_same_datetime(dt1, dt2, allowed_delta=timedelta(minutes=1)):
|
||||
"""
|
||||
Returns true if these are w/in a minute of each other. (in case secs saved to db
|
||||
|
||||
47
common/test/data/due_date/about/overview.html
Normal file
47
common/test/data/due_date/about/overview.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<chapter display_name="Section">
|
||||
<sequential url_name="c804fa32227142a1bd9d5bc183d4a20d"/>
|
||||
</chapter>
|
||||
1
common/test/data/due_date/course.xml
Normal file
1
common/test/data/due_date/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course url_name="2013_fall" org="edX" course="due_date"/>
|
||||
3
common/test/data/due_date/course/2013_fall.xml
Normal file
3
common/test/data/due_date/course/2013_fall.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<course display_name="due_date">
|
||||
<chapter url_name="c8ee0db7e5a84c85bac80b7013cf6512"/>
|
||||
</course>
|
||||
@@ -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}}
|
||||
1
common/test/data/due_date/policies/2013_fall/policy.json
Normal file
1
common/test/data/due_date/policies/2013_fall/policy.json
Normal file
@@ -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"}}}}
|
||||
@@ -0,0 +1,23 @@
|
||||
<problem display_name="Multiple Choice" markdown="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 (x) 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. [explanation] ">
|
||||
<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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<sequential display_name="Subsection" due="2013-09-18T11:30:00Z" start="1970-01-01T00:00:00Z">
|
||||
<vertical url_name="45640305a210424ebcc6f8e045fad0be"/>
|
||||
</sequential>
|
||||
@@ -0,0 +1,3 @@
|
||||
<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)
|
||||
|
||||
@@ -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,85 @@ 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.
|
||||
"""
|
||||
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
|
||||
course = modulestore().get_course("edX/due_date/2013_fall")
|
||||
|
||||
# Default case show_timezone = True
|
||||
text = set_show_timezone(True)
|
||||
self.assertIn("due Sep 18, 2013 at 11:30 UTC", text)
|
||||
|
||||
# show_timezone = False
|
||||
text = set_show_timezone(False)
|
||||
self.assertNotIn("due Sep 18, 2013 at 11:30 UTC", text)
|
||||
self.assertIn("due Sep 18, 2013 at 11:30", text)
|
||||
|
||||
# plain text due date
|
||||
text = set_due_date_format("foobar")
|
||||
self.assertNotIn("due Sep 18, 2013 at 11:30", text)
|
||||
self.assertIn("due foobar", text)
|
||||
|
||||
# due date with no time
|
||||
text = set_due_date_format(u"%b %d %Y")
|
||||
self.assertNotIn("due Sep 18, 2013 at 11:30", text)
|
||||
self.assertIn("due Sep 18 2013", 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 with show_timezone arg
|
||||
text = set_due_date_format(u"%%%")
|
||||
self.assertNotIn("%%%", text)
|
||||
self.assertIn("due Sep 18, 2013 at 11:30", text)
|
||||
self.assertNotIn("due Sep 18, 2013 at 11:30 UTC", text)
|
||||
|
||||
set_show_timezone(True)
|
||||
text = set_due_date_format(u"%%%")
|
||||
self.assertNotIn("%%%", text)
|
||||
self.assertIn("due Sep 18, 2013 at 11:30 UTC", text)
|
||||
|
||||
@@ -98,7 +98,8 @@ 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())
|
||||
('show_timezone', course.show_timezone),
|
||||
('due_date_display_format', course.due_date_display_format)] + template_imports.items())
|
||||
return render_to_string('courseware/accordion.html', context)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%!
|
||||
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, show_timezone)
|
||||
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, course.show_timezone)
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user