Commit 08d99429 by cahrens

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.
parent 2677598a
......@@ -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(
......
"""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
......
<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"}}}}
\ 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)
......@@ -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)
......
<%!
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>
......
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