From f0cd29f02ae19aae32bcebc3203747fa43acfd2e Mon Sep 17 00:00:00 2001 From: Gregory Martin Date: Fri, 4 Nov 2016 14:49:25 -0400 Subject: [PATCH] Revert "Merge pull request #13915 from edx/revert-13794-yro_implement-dateutil" This reverts commit d59ab18b27b26244f0cb99391a718c6c89191138, reversing changes made to 0ebab35e89d83bcfd5de953a86f561dd23c67999. --- .../tests/lms/test_lms_dashboard.py | 14 +-- .../tests/test_field_override_performance.py | 54 +++++------ .../courseware/context_processor.py | 46 +++++++++ lms/djangoapps/courseware/date_summary.py | 12 ++- .../tests/test_context_processor.py | 43 +++++++++ .../courseware/tests/test_course_info.py | 4 +- .../tests/test_credit_requirements.py | 12 +-- lms/djangoapps/courseware/tests/test_views.py | 93 ++++++++----------- lms/djangoapps/courseware/views/index.py | 7 +- lms/envs/common.py | 3 + lms/static/js/dateutil_factory.js | 9 +- lms/static/js/spec/dateutil_factory_spec.js | 2 +- .../views/account_settings_factory.js | 7 +- lms/templates/course.html | 25 ++++- lms/templates/courseware/accordion.html | 24 ++--- lms/templates/courseware/course_about.html | 39 ++++++-- lms/templates/courseware/progress.html | 12 +-- .../dashboard/_dashboard_course_listing.html | 50 +++++++--- .../content/course_overviews/models.py | 2 +- .../core/lib/tests/test_time_zone_utils.py | 16 ---- openedx/core/lib/time_zone_utils.py | 10 -- 21 files changed, 294 insertions(+), 190 deletions(-) create mode 100644 lms/djangoapps/courseware/context_processor.py create mode 100644 lms/djangoapps/courseware/tests/test_context_processor.py diff --git a/common/test/acceptance/tests/lms/test_lms_dashboard.py b/common/test/acceptance/tests/lms/test_lms_dashboard.py index 276cbf4340..b600dcfe68 100644 --- a/common/test/acceptance/tests/lms/test_lms_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_dashboard.py @@ -10,8 +10,8 @@ from common.test.acceptance.fixtures.course import CourseFixture from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.dashboard import DashboardPage -DEFAULT_SHORT_DATE_FORMAT = "%b %d, %Y" -DEFAULT_DAY_AND_TIME_FORMAT = "%A at %-I%P" +DEFAULT_SHORT_DATE_FORMAT = '{dt:%b} {dt.day}, {dt.year}' +TEST_DATE_FORMAT = '{dt:%b} {dt.day}, {dt.year} {dt.hour}:{dt.minute:02}' class BaseLmsDashboardTest(UniqueCourseTest): @@ -193,7 +193,7 @@ class LmsDashboardPageTest(BaseLmsDashboardTest): }) self.course_fixture.configure_course() - end_date = course_end_date.strftime(DEFAULT_SHORT_DATE_FORMAT) + end_date = DEFAULT_SHORT_DATE_FORMAT.format(dt=course_end_date) expected_course_date = "Ended - {end_date}".format(end_date=end_date) # reload the page for changes to course date changes to appear in dashboard @@ -226,7 +226,7 @@ class LmsDashboardPageTest(BaseLmsDashboardTest): }) self.course_fixture.configure_course() - start_date = course_start_date.strftime(DEFAULT_SHORT_DATE_FORMAT) + start_date = DEFAULT_SHORT_DATE_FORMAT.format(dt=course_start_date) expected_course_date = "Started - {start_date}".format(start_date=start_date) # reload the page for changes to course date changes to appear in dashboard @@ -259,7 +259,7 @@ class LmsDashboardPageTest(BaseLmsDashboardTest): }) self.course_fixture.configure_course() - start_date = course_start_date.strftime(DEFAULT_SHORT_DATE_FORMAT) + start_date = DEFAULT_SHORT_DATE_FORMAT.format(dt=course_start_date) expected_course_date = "Starts - {start_date}".format(start_date=start_date) # reload the page for changes to course date changes to appear in dashboard @@ -293,8 +293,8 @@ class LmsDashboardPageTest(BaseLmsDashboardTest): }) self.course_fixture.configure_course() - start_date = course_start_date.strftime(DEFAULT_DAY_AND_TIME_FORMAT) - expected_course_date = "Starts - {start_date} UTC".format(start_date=start_date) + start_date = TEST_DATE_FORMAT.format(dt=course_start_date) + expected_course_date = "Starts - {start_date} GMT".format(start_date=start_date) # reload the page for changes to course date changes to appear in dashboard self.dashboard_page.visit() diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 1a8fc76fe3..6d7e8ddb91 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -229,18 +229,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): # # of sql queries to default, # # of mongo queries, # ) - ('no_overrides', 1, True, False): (18, 6), - ('no_overrides', 2, True, False): (18, 6), - ('no_overrides', 3, True, False): (18, 6), - ('ccx', 1, True, False): (18, 6), - ('ccx', 2, True, False): (18, 6), - ('ccx', 3, True, False): (18, 6), - ('no_overrides', 1, False, False): (18, 6), - ('no_overrides', 2, False, False): (18, 6), - ('no_overrides', 3, False, False): (18, 6), - ('ccx', 1, False, False): (18, 6), - ('ccx', 2, False, False): (18, 6), - ('ccx', 3, False, False): (18, 6), + ('no_overrides', 1, True, False): (21, 6), + ('no_overrides', 2, True, False): (21, 6), + ('no_overrides', 3, True, False): (21, 6), + ('ccx', 1, True, False): (21, 6), + ('ccx', 2, True, False): (21, 6), + ('ccx', 3, True, False): (21, 6), + ('no_overrides', 1, False, False): (21, 6), + ('no_overrides', 2, False, False): (21, 6), + ('no_overrides', 3, False, False): (21, 6), + ('ccx', 1, False, False): (21, 6), + ('ccx', 2, False, False): (21, 6), + ('ccx', 3, False, False): (21, 6), } @@ -252,19 +252,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - ('no_overrides', 1, True, False): (18, 3), - ('no_overrides', 2, True, False): (18, 3), - ('no_overrides', 3, True, False): (18, 3), - ('ccx', 1, True, False): (18, 3), - ('ccx', 2, True, False): (18, 3), - ('ccx', 3, True, False): (18, 3), - ('ccx', 1, True, True): (19, 3), - ('ccx', 2, True, True): (19, 3), - ('ccx', 3, True, True): (19, 3), - ('no_overrides', 1, False, False): (18, 3), - ('no_overrides', 2, False, False): (18, 3), - ('no_overrides', 3, False, False): (18, 3), - ('ccx', 1, False, False): (18, 3), - ('ccx', 2, False, False): (18, 3), - ('ccx', 3, False, False): (18, 3), + ('no_overrides', 1, True, False): (21, 3), + ('no_overrides', 2, True, False): (21, 3), + ('no_overrides', 3, True, False): (21, 3), + ('ccx', 1, True, False): (21, 3), + ('ccx', 2, True, False): (21, 3), + ('ccx', 3, True, False): (21, 3), + ('ccx', 1, True, True): (22, 3), + ('ccx', 2, True, True): (22, 3), + ('ccx', 3, True, True): (22, 3), + ('no_overrides', 1, False, False): (21, 3), + ('no_overrides', 2, False, False): (21, 3), + ('no_overrides', 3, False, False): (21, 3), + ('ccx', 1, False, False): (21, 3), + ('ccx', 2, False, False): (21, 3), + ('ccx', 3, False, False): (21, 3), } diff --git a/lms/djangoapps/courseware/context_processor.py b/lms/djangoapps/courseware/context_processor.py new file mode 100644 index 0000000000..07f98191a0 --- /dev/null +++ b/lms/djangoapps/courseware/context_processor.py @@ -0,0 +1,46 @@ +""" +This is the courseware context_processor module. + +This is meant to simplify the process of sending user preferences (espec. time_zone and pref-lang) +to the templates without having to append every view file. + +""" +from openedx.core.djangoapps.user_api.errors import UserNotFound, UserAPIInternalError +from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences +import request_cache + +RETRIEVABLE_PREFERENCES = { + 'user_timezone': 'time_zone', + 'user_language': 'pref-lang' +} +CACHE_NAME = "context_processor.user_timezone_preferences" + + +def user_timezone_locale_prefs(request): + """ + Checks if request has an authenticated user. + If so, sends set (or none if unset) time_zone and language prefs. + + This interacts with the DateUtils to either display preferred or attempt to determine + system/browser set time_zones and languages + + """ + cached_value = request_cache.get_cache(CACHE_NAME) + if not cached_value: + user_prefs = { + 'user_timezone': None, + 'user_language': None, + } + if hasattr(request, 'user') and request.user.is_authenticated(): + try: + user_preferences = get_user_preferences(request.user) + except (UserNotFound, UserAPIInternalError): + cached_value.update(user_prefs) + else: + user_prefs = { + key: user_preferences.get(pref_name, None) + for key, pref_name in RETRIEVABLE_PREFERENCES.iteritems() + } + + cached_value.update(user_prefs) + return cached_value diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index abda7d96af..e6aaf5524f 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -4,6 +4,7 @@ page. Each block gives information about a particular course-run-specific date which will be displayed to the user. """ from datetime import datetime +from pytz import timezone, utc from babel.dates import format_timedelta from django.core.urlresolvers import reverse @@ -12,13 +13,12 @@ from django.utils.translation import ugettext_lazy from django.utils.translation import to_locale, get_language from edxmako.shortcuts import render_to_string from lazy import lazy -from pytz import utc from course_modes.models import CourseMode from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification -from openedx.core.lib.time_zone_utils import get_time_zone_abbr, get_user_time_zone from student.models import CourseEnrollment +from openedx.core.lib.time_zone_utils import get_time_zone_abbr class DateSummary(object): @@ -67,8 +67,12 @@ class DateSummary(object): @property def time_zone(self): - """The time zone to display in""" - return get_user_time_zone(self.user) + """ + The time zone in which to display -- defaults to UTC + """ + return timezone( + self.user.preferences.model.get_value(self.user, "time_zone", "UTC") + ) def __init__(self, course, user): self.course = course diff --git a/lms/djangoapps/courseware/tests/test_context_processor.py b/lms/djangoapps/courseware/tests/test_context_processor.py new file mode 100644 index 0000000000..73b86a36f7 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_context_processor.py @@ -0,0 +1,43 @@ +""" +Unit tests for courseware context_processor +""" +from django.contrib.auth.models import AnonymousUser +from mock import Mock + +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from openedx.core.djangoapps.user_api.preferences.api import set_user_preference + +from courseware.context_processor import user_timezone_locale_prefs + + +class UserPrefContextProcessorUnitTest(ModuleStoreTestCase): + """ + Unit test for courseware context_processor + """ + def setUp(self): + super(UserPrefContextProcessorUnitTest, self).setUp() + + self.user = UserFactory.create() + self.request = Mock() + self.request.user = self.user + + def test_anonymous_user(self): + self.request.user = AnonymousUser() + context = user_timezone_locale_prefs(self.request) + self.assertIsNone(context['user_timezone']) + self.assertIsNone(context['user_language']) + + def test_no_timezone_preference(self): + set_user_preference(self.user, 'pref-lang', 'en') + context = user_timezone_locale_prefs(self.request) + self.assertIsNone(context['user_timezone']) + self.assertIsNotNone(context['user_language']) + self.assertEqual(context['user_language'], 'en') + + def test_no_language_preference(self): + set_user_preference(self.user, 'time_zone', 'Asia/Tokyo') + context = user_timezone_locale_prefs(self.request) + self.assertIsNone(context['user_language']) + self.assertIsNotNone(context['user_timezone']) + self.assertEqual(context['user_timezone'], 'Asia/Tokyo') diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index 5d9f3702ef..ccef2c7a1e 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -316,7 +316,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest self.assertEqual(resp.status_code, 200) def test_num_queries_instructor_paced(self): - self.fetch_course_info_with_queries(self.instructor_paced_course, 18, 4) + self.fetch_course_info_with_queries(self.instructor_paced_course, 21, 4) def test_num_queries_self_paced(self): - self.fetch_course_info_with_queries(self.self_paced_course, 18, 4) + self.fetch_course_info_with_queries(self.self_paced_course, 21, 4) diff --git a/lms/djangoapps/courseware/tests/test_credit_requirements.py b/lms/djangoapps/courseware/tests/test_credit_requirements.py index fb3d241ad9..6412c13fe7 100644 --- a/lms/djangoapps/courseware/tests/test_credit_requirements.py +++ b/lms/djangoapps/courseware/tests/test_credit_requirements.py @@ -14,7 +14,6 @@ from django.core.urlresolvers import reverse from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory -from util.date_utils import get_time_display, DEFAULT_SHORT_DATE_FORMAT from course_modes.models import CourseMode from openedx.core.djangoapps.credit import api as credit_api @@ -122,7 +121,8 @@ class ProgressPageCreditRequirementsTest(SharedModuleStoreTestCase): response, "{}, you have met the requirements for credit in this course.".format(self.USER_FULL_NAME) ) - self.assertContains(response, "Completed by {date}".format(date=self._now_formatted_date())) + self.assertContains(response, "Completed by {date}") + self.assertContains(response, datetime.datetime.now(UTC).strftime('%Y-%m-%d %H')) self.assertNotContains(response, "95%") def test_credit_requirements_not_eligible(self): @@ -172,11 +172,3 @@ class ProgressPageCreditRequirementsTest(SharedModuleStoreTestCase): """Load the progress page for the course the user is enrolled in. """ url = reverse("progress", kwargs={"course_id": unicode(self.course.id)}) return self.client.get(url) - - def _now_formatted_date(self): - """Retrieve the formatted current date. """ - return get_time_display( - datetime.datetime.now(UTC), - DEFAULT_SHORT_DATE_FORMAT, - settings.TIME_ZONE - ) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index af31a7c724..53f3ca0ce5 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -209,6 +209,7 @@ class ViewsTestCase(ModuleStoreTestCase): parent_location=self.chapter.location, due=datetime(2013, 9, 18, 11, 30, 00), display_name='Sequential 1', + format='Homework' ) self.vertical = ItemFactory.create( category='vertical', @@ -907,33 +908,22 @@ class ViewsTestCase(ModuleStoreTestCase): self.assertEqual(response.status_code, 200) def test_accordion(self): - request = RequestFactory().get('foo') - request.user = self.user - table_of_contents = toc_for_course( - request.user, - request, - self.course, - unicode(self.course.get_children()[0].scope_ids.usage_id), - None, - None - ) - - # removes newlines and whitespace from the returned view string - view = ''.join(render_accordion(request, self.course, table_of_contents['chapters'], 'en').split()) - # the course id unicode is re-encoded here because the quote function does not accept unicode + """ + This needs a response_context, which is not included in the render_accordion's main method + returning a render_to_string, so we will render via the courseware URL in order to include + the needed context + """ course_id = quote(unicode(self.course.id).encode("utf-8")) - - self.assertIn( - u'href="/courses/{}/courseware/Chapter_1/Sequential_1/">Sequential1

' - .format(course_id.decode("utf-8")), - view - ) - - self.assertIn( - u'href="/courses/{}/courseware/Chapter_1/Sequential_2/">Sequential2

' - .format(course_id.decode("utf-8")), - view + response = self.client.get( + reverse('courseware', args=[unicode(course_id)]), + follow=True ) + test_responses = [ + '

Sequential 1 current section

', + '

Sequential 2

' + ] + for test in test_responses: + self.assertContains(response, test) @attr(shard=1) @@ -960,7 +950,8 @@ class BaseDueDateTests(ModuleStoreTestCase): section = ItemFactory.create( category='sequential', parent_location=chapter.location, - due=datetime(2013, 9, 18, 11, 30, 00) + due=datetime(2013, 9, 18, 11, 30, 00), + format='homework' ) vertical = ItemFactory.create(category='vertical', parent_location=section.location) ItemFactory.create(category='problem', parent_location=vertical.location) @@ -975,8 +966,7 @@ class BaseDueDateTests(ModuleStoreTestCase): self.user = UserFactory.create() self.assertTrue(self.client.login(username=self.user.username, password='test')) - self.time_with_tz = "due Sep 18, 2013 at 11:30 UTC" - self.time_without_tz = "due Sep 18, 2013 at 11:30" + self.time_with_tz = "2013-09-18 11:30:00+00:00" def test_backwards_compatability(self): # The test course being used has show_timezone = False in the policy file @@ -985,8 +975,7 @@ class BaseDueDateTests(ModuleStoreTestCase): # remove the timezone. course = self.set_up_course(due_date_display_format=None, show_timezone=False) response = self.get_response(course) - self.assertContains(response, self.time_without_tz) - self.assertNotContains(response, self.time_with_tz) + self.assertContains(response, self.time_with_tz) # Test that show_timezone has been cleared (which means you get the default value of True). self.assertTrue(course.show_timezone) @@ -1001,25 +990,11 @@ class BaseDueDateTests(ModuleStoreTestCase): response = self.get_response(course) self.assertContains(response, self.time_with_tz) - def test_format_plain_text(self): - # plain text due date - course = self.set_up_course(due_date_display_format="foobar") - response = self.get_response(course) - self.assertNotContains(response, self.time_with_tz) - self.assertContains(response, "due foobar") - def test_format_date(self): # due date with no time course = self.set_up_course(due_date_display_format=u"%b %d %y") response = self.get_response(course) - self.assertNotContains(response, self.time_with_tz) - self.assertContains(response, "due Sep 18 13") - - def test_format_hidden(self): - # hide due date completely - course = self.set_up_course(due_date_display_format=u"") - response = self.get_response(course) - self.assertNotContains(response, "due ") + self.assertContains(response, self.time_with_tz) def test_format_invalid(self): # improperly formatted due_date_display_format falls through to default @@ -1049,7 +1024,10 @@ class TestAccordionDueDate(BaseDueDateTests): def get_response(self, course): """ Returns the HTML for the accordion """ - return self.client.get(reverse('courseware', args=[unicode(course.id)]), follow=True) + return self.client.get( + reverse('courseware', args=[unicode(course.id)]), + follow=True + ) @attr(shard=1) @@ -1089,14 +1067,17 @@ class StartDateTests(ModuleStoreTestCase): course = self.set_up_course() response = self.get_about_response(course.id) # The start date is set in the set_up_course function above. - self.assertContains(response, "2013-SEPTEMBER-16") + # This should return in the format '%Y-%m-%dT%H:%M:%S%z' + self.assertContains(response, "2013-09-16T07:17:28+0000") - @patch('util.date_utils.pgettext', fake_pgettext(translations={ - ("abbreviated month name", "Jul"): "JULY", - })) - @patch('util.date_utils.ugettext', fake_ugettext(translations={ - "SHORT_DATE_FORMAT": "%Y-%b-%d", - })) + @patch( + 'util.date_utils.pgettext', + fake_pgettext(translations={("abbreviated month name", "Jul"): "JULY", }) + ) + @patch( + 'util.date_utils.ugettext', + fake_ugettext(translations={"SHORT_DATE_FORMAT": "%Y-%b-%d", }) + ) @unittest.skip def test_format_localized_in_xml_course(self): response = self.get_about_response(SlashSeparatedCourseKey('edX', 'toy', 'TT_2012_Fall')) @@ -1345,17 +1326,17 @@ class ProgressPageTests(ModuleStoreTestCase): """Test that query counts remain the same for self-paced and instructor-paced courses.""" SelfPacedConfiguration(enabled=self_paced_enabled).save() self.setup_course(self_paced=self_paced) - with self.assertNumQueries(35), check_mongo_calls(4): + with self.assertNumQueries(38), check_mongo_calls(4): self._get_progress_page() def test_progress_queries(self): self.setup_course() - with self.assertNumQueries(35), check_mongo_calls(4): + with self.assertNumQueries(38), check_mongo_calls(4): self._get_progress_page() # subsequent accesses to the progress page require fewer queries. for _ in range(2): - with self.assertNumQueries(21), check_mongo_calls(4): + with self.assertNumQueries(24), check_mongo_calls(4): self._get_progress_page() @patch( diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index d3fe5938ef..337ebb57a5 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -24,7 +24,6 @@ import urllib from xblock.fragment import Fragment from opaque_keys.edx.keys import CourseKey -from openedx.core.lib.time_zone_utils import get_user_time_zone from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from shoppingcart.models import CourseRegistrationCode @@ -402,7 +401,6 @@ class CoursewareIndex(View): self.request, self.course, table_of_contents['chapters'], - courseware_context['language_preference'], ) # entrance exam data @@ -499,7 +497,7 @@ class CoursewareIndex(View): raise -def render_accordion(request, course, table_of_contents, language_preference): +def render_accordion(request, course, table_of_contents): """ Returns the HTML that renders the navigation for the given course. Expects the table_of_contents to have data on each chapter and section, @@ -511,9 +509,6 @@ def render_accordion(request, course, table_of_contents, language_preference): ('course_id', unicode(course.id)), ('csrf', csrf(request)['csrf_token']), ('due_date_display_format', course.due_date_display_format), - ('time_zone', request.user.preferences.model.get_value(request.user, "time_zone", None)), - ('language', language_preference), - ] + TEMPLATE_IMPORTS.items() ) return render_to_string('courseware/accordion.html', context) diff --git a/lms/envs/common.py b/lms/envs/common.py index 80cd61d865..70b40c8627 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -521,6 +521,9 @@ TEMPLATES = [ # Shoppingcart processor (detects if request.user has a cart) 'shoppingcart.context_processor.user_has_cart_context_processor', + # Timezone processor (sends language and time_zone preference) + 'courseware.context_processor.user_timezone_locale_prefs', + # Allows the open edX footer to be leveraged in Django Templates. 'edxmako.shortcuts.footer_context_processor', diff --git a/lms/static/js/dateutil_factory.js b/lms/static/js/dateutil_factory.js index 8638d3279a..d0b5b6c6a5 100644 --- a/lms/static/js/dateutil_factory.js +++ b/lms/static/js/dateutil_factory.js @@ -36,8 +36,8 @@ context = { datetime: $(this).data('datetime'), timezone: $(this).data('timezone'), - language: $(this).attr('lang'), - format: $(this).data('format') + language: $(this).data('language'), + format: DateUtils.dateFormatEnum[$(this).data('format')] }; displayDatetime = stringHandler( localizedTime(context), @@ -45,6 +45,11 @@ $(this).data('datetoken') ); $(this).text(displayDatetime); + } else { + displayDatetime = stringHandler( + $(this).data('string') + ); + $(this).text(displayDatetime); } }); }; diff --git a/lms/static/js/spec/dateutil_factory_spec.js b/lms/static/js/spec/dateutil_factory_spec.js index 852ee51a68..9e09e7bd30 100644 --- a/lms/static/js/spec/dateutil_factory_spec.js +++ b/lms/static/js/spec/dateutil_factory_spec.js @@ -35,7 +35,7 @@ define(['../dateutil_factory.js'], function(DateUtilIterator) { 'data-string="Due {date}">' ); Object.keys(testLangs).forEach(function(key) { - $form.attr('lang', String(key)); + $form.attr('data-language', String(key)); $(document.body).append($form); DateUtilIterator.transform(iterationKey); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 86fca0e190..c03ac83aba 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -116,9 +116,10 @@ title: gettext('Time Zone'), valueAttribute: 'time_zone', helpMessage: gettext( - 'Select the time zone for displaying course dates. If you do not specify a ' + - 'time zone here, course dates, including assignment deadlines, are displayed in ' + - 'Coordinated Universal Time (UTC).' + 'Select the time zone for displaying course dates. ' + + 'If you do not specify a time zone, course dates, ' + + 'including assignment deadlines, will be displayed ' + + 'in your browser\'s local time zone.' ), groupOptions: [{ groupTitle: gettext('All Time Zones'), diff --git a/lms/templates/course.html b/lms/templates/course.html index dfc53ef0ba..5ae2774d79 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -19,14 +19,31 @@ from django.core.urlresolvers import reverse ${course.display_number_with_default} ${course.display_name_with_default} - - + <% + if course.start is not None: + course_date_string = course.start.strftime('%Y-%m-%dT%H:%M:%S%z') + else: + course_date_string = '' + %> + % if isinstance(course_start_date, str): + + % else: + + % endif +
  • ${course.display_org_with_default}
  • ${course.display_number_with_default}
  • -
  • ${_("Starts")}:
  • -
+ %if isinstance(course_start_date, str): +
  • ${_("Starts")}:
  • + %else: +
  • ${_("Starts")}:
  • + %endif +
    +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform(iterationKey=".localized_datetime"); + diff --git a/lms/templates/courseware/accordion.html b/lms/templates/courseware/accordion.html index 621ee43842..2327f028de 100644 --- a/lms/templates/courseware/accordion.html +++ b/lms/templates/courseware/accordion.html @@ -32,21 +32,19 @@ else: span_start=HTML(''), span_end=HTML(''), ) if 'active' in section and section['active'] else ''}

    - <% - if section.get('due') is None: - due_date = '' - else: - formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=time_zone) - due_date = '' if len(formatted_string)==0 else _('due {date}').format(date=formatted_string) - %> - ## There is behavior differences between + ## There are behavior differences between ## rendering of sections which have proctoring/timed examinations ## and those that do not. ## ## Proctoring exposes a exam status message field as well as ## a status icon - + <% + if section.get('due') is None: + data_string = '' + else: + data_string = _('due {date}') + %> % if section['format'] or due_date or 'proctoring' in section:

    % if 'proctoring' in section: @@ -57,12 +55,12 @@ else: ## completed proctored exam statuses should not show the due date ## since the exam has already been submitted by the user % if not section['proctoring'].get('in_completed_state', False): - ${due_date} + % endif % else: ## non-proctored section, we just show the exam format and the due date ## this is the standard case in edx-platform - ${section['format']} ${due_date} + % if 'graded' in section and section['graded']: @@ -87,4 +85,8 @@ else: <%static:require_module_async module_name="js/courseware/accordion_events" class_name="AccordionEvents"> AccordionEvents(); + +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform(iterationKey=".localized-datetime"); + % endif diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index bfa62e86ee..8d6b0ae5e8 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -223,21 +223,40 @@ from openedx.core.lib.courses import course_image_url

    1. ${_("Course Number")}

      ${course.display_number_with_default | h}
    2. % if not course.start_date_is_still_default: -
    3. ${_("Classes Start")}

      ${course.start_datetime_text()}
    4. + <% + course_start_date = course.start + %> +
    5. + +

      ${_("Classes Start")}

      + % if isinstance(course_start_date, str): + ${course_start_date} + % else: + <% + course_date_string = course_start_date.strftime('%Y-%m-%dT%H:%M:%S%z') + %> + + % endif +
    6. % endif ## We plan to ditch end_date (which is not stored in course metadata), ## but for backwards compatibility, show about/end_date blob if it exists. % if get_course_about_section(request, course, "end_date") or course.end: + <% + course_end_date = course.end + %> +
    7. ${_("Classes End")}

      - - % if get_course_about_section(request, course, "end_date"): - ${get_course_about_section(request, course, "end_date")} - % else: - ${course.end_datetime_text()} - % endif - + % if isinstance(course_end_date, str): + ${course_end_date} + % else: + <% + course_date_string = course_end_date.strftime('%Y-%m-%dT%H:%M:%S%z') + %> + + % endif
    8. % endif @@ -323,3 +342,7 @@ from openedx.core.lib.courses import course_image_url %endif <%include file="../video_modal.html" /> + +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform(iterationKey=".localized_datetime"); + diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index da4be72103..dc9061cc84 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -6,7 +6,6 @@ from course_modes.models import CourseMode from certificates.models import CertificateStatuses from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from util.date_utils import get_time_display, DEFAULT_SHORT_DATE_FORMAT from django.conf import settings from django.utils.http import urlquote_plus %> @@ -121,7 +120,7 @@ from django.utils.http import urlquote_plus ${_("Verification Declined" )} %elif requirement['status'] == 'satisfied': - ${_("Completed by")} ${get_time_display(requirement['status_date'], DEFAULT_SHORT_DATE_FORMAT, settings.TIME_ZONE)} + %endif %else: ${_("Upcoming")} @@ -168,11 +167,7 @@ from django.utils.http import urlquote_plus %if section.due is not None:

      - <% - formatted_string = get_time_display(section.due, course.due_date_display_format, coerce_tz=settings.TIME_ZONE_DISPLAYED_FOR_DEADLINES) - due_date = '' if len(formatted_string)==0 else _(u'due {date}').format(date=formatted_string) - %> - ${due_date | h} +

      %endif %if len(section.scores) > 0: @@ -197,3 +192,6 @@ from django.utils.http import urlquote_plus +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform(iterationKey=".localized-datetime"); + diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 14774c0e6a..52637a3ade 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -10,7 +10,6 @@ from course_modes.models import CourseMode from course_modes.helpers import enrollment_mode_display from openedx.core.djangolib.js_utils import dump_js_escaped_json from openedx.core.djangolib.markup import HTML, Text -from openedx.core.lib.time_zone_utils import get_user_time_zone from student.helpers import ( VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, @@ -99,20 +98,37 @@ from student.helpers import (
      ${course_overview.display_org_with_default} - ${course_overview.display_number_with_default} - - <% time_zone = get_user_time_zone(user) %> - % if course_overview.has_ended(): - ${_("Ended - {end_date}").format(end_date=course_overview.end_datetime_text("SHORT_DATE", time_zone))} - % elif course_overview.has_started(): - ${_("Started - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE", time_zone))} - % elif course_overview.start_date_is_still_default: # Course start date TBD - ${_("Coming Soon")} - % elif course_overview.starts_within(days=5): # hasn't started yet - ${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("DAY_AND_TIME", time_zone))} - % else: # hasn't started yet - ${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE", time_zone))} - % endif - + <% + if course_overview.start_date_is_still_default: + container_string = _("Coming Soon") + course_date = None + else: + format = 'shortDate' + if course_overview.has_ended(): + container_string = _("Ended - {date}") + course_date = course_overview.end + elif course_overview.has_started(): + container_string = _("Started - {date}") + course_date = course_overview.start + elif course_overview.starts_within(days=5): + container_string = _("Starts - {date}") + course_date = course_overview.start + format = 'defaultFormat' + else: ## hasn't started yet + container_string = _("Starts - {date}") + course_date = course_overview.start + endif + endif + %> + + % if isinstance(course_date, str): + ${_(container_string).format(date=course_date)} + % elif course_date is not None: + <% + course_date_string = course_date.strftime('%Y-%m-%dT%H:%M:%S%z') + %> + + % endif
      @@ -439,3 +455,7 @@ from student.helpers import ( } }); + +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform(iterationKey=".localized-datetime"); + diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 4b15b17125..d9cede9f58 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -357,7 +357,6 @@ class CourseOverview(TimeStampedModel): """ Returns True if the course starts with-in given number of days otherwise returns False. """ - return course_metadata_utils.course_starts_within(self.start, days) def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc): @@ -389,6 +388,7 @@ class CourseOverview(TimeStampedModel): def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc): """ Returns the end date or datetime for the course formatted as a string. + """ return course_metadata_utils.course_end_datetime_text( self.end, diff --git a/openedx/core/lib/tests/test_time_zone_utils.py b/openedx/core/lib/tests/test_time_zone_utils.py index f1ad4be79a..082b46cadd 100644 --- a/openedx/core/lib/tests/test_time_zone_utils.py +++ b/openedx/core/lib/tests/test_time_zone_utils.py @@ -1,12 +1,10 @@ """Tests covering time zone utilities.""" from freezegun import freeze_time from student.tests.factories import UserFactory -from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.lib.time_zone_utils import ( get_display_time_zone, get_time_zone_abbr, get_time_zone_offset, - get_user_time_zone, ) from pytz import timezone, utc from unittest import TestCase @@ -25,20 +23,6 @@ class TestTimeZoneUtils(TestCase): self.user = UserFactory.build() self.user.save() - def test_get_user_time_zone(self): - """ - Test to ensure get_user_time_zone() returns the correct time zone - or UTC if user has not specified time zone. - """ - # User time zone should be UTC when no time zone has been chosen - user_tz = get_user_time_zone(self.user) - self.assertEqual(user_tz, utc) - - # User time zone should change when user specifies time zone - set_user_preference(self.user, 'time_zone', 'Asia/Tokyo') - user_tz = get_user_time_zone(self.user) - self.assertEqual(user_tz, timezone('Asia/Tokyo')) - def _display_time_zone_helper(self, time_zone_string): """ Helper function to return all info from get_display_time_zone() diff --git a/openedx/core/lib/time_zone_utils.py b/openedx/core/lib/time_zone_utils.py index 442d5f7d60..27bad00626 100644 --- a/openedx/core/lib/time_zone_utils.py +++ b/openedx/core/lib/time_zone_utils.py @@ -2,19 +2,9 @@ Utilities related to timezones """ from datetime import datetime - from pytz import common_timezones, timezone, utc -def get_user_time_zone(user): - """ - Returns pytz time zone object of the user's time zone if available or UTC time zone if unavailable - """ - #TODO: exception for unknown timezones? - time_zone = user.preferences.model.get_value(user, "time_zone", 'utc') - return timezone(time_zone) - - def _format_time_zone_string(time_zone, date_time, format_string): """ Returns a string, specified by format string, of the current date/time of the time zone.