diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index cd59b811d7..dd96cd213a 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -16,13 +16,13 @@ from xmodule.contentstore.django import contentstore from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.contentstore.content import StaticContent -from xmodule.util.date_utils import get_default_time_display from xmodule.modulestore import InvalidLocationError from xmodule.exceptions import NotFoundError from django.core.exceptions import PermissionDenied from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.locator import BlockUsageLocator +from util.date_utils import get_default_time_display from util.json_request import JsonResponse from django.http import HttpResponseNotFound from django.utils.translation import ugettext as _ diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 8c1bcb7a9d..7b8eb5b252 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -10,8 +10,8 @@ from django.conf import settings from xmodule.modulestore.exceptions import ItemNotFoundError from edxmako.shortcuts import render_to_response +from util.date_utils import get_default_time_display from xmodule.modulestore.django import modulestore -from xmodule.util.date_utils import get_default_time_display from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.locator import BlockUsageLocator diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index be4c042bc7..4103c0752b 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! import logging - from xmodule.util.date_utils import get_default_time_display, almost_same_datetime + from util.date_utils import get_default_time_display, almost_same_datetime from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse %> diff --git a/cms/templates/overview.html b/cms/templates/overview.html index e7aa8da9ad..dc74f5c799 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! import logging - from xmodule.util import date_utils + from util.date_utils import get_default_time_display from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from xmodule.modulestore.django import loc_mapper @@ -188,7 +188,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ${_("Schedule")} %else: ${_("Release date:")} - ${date_utils.get_default_time_display(section.start)} + ${get_default_time_display(section.start)} ${_("Edit section release date")} %endif diff --git a/common/djangoapps/util/date_utils.py b/common/djangoapps/util/date_utils.py new file mode 100644 index 0000000000..11ca25d087 --- /dev/null +++ b/common/djangoapps/util/date_utils.py @@ -0,0 +1,373 @@ +""" +Convenience methods for working with datetime objects +""" + +from datetime import timedelta +import re + +from pytz import timezone, UTC, UnknownTimeZoneError +from django.utils.translation import pgettext, ugettext + + +def get_default_time_display(dtime): + """ + Converts a datetime to a string representation. This is the default + representation used in Studio and LMS. + + It will use the "DATE_TIME" format in the current language, if provided, + or defaults to "Apr 09, 2013 at 16:00 UTC". + + If None is passed in for dt, an empty string will be returned. + + """ + if dtime is None: + return u"" + if dtime.tzinfo is not None: + try: + timezone = u" " + dtime.tzinfo.tzname(dtime) + except NotImplementedError: + timezone = dtime.strftime('%z') + else: + timezone = u" UTC" + + localized = strftime_localized(dtime, "DATE_TIME") + return (localized + timezone).strip() + + +def get_time_display(dtime, format_string=None, coerce_tz=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`. + + Coerces aware datetime to tz=coerce_tz if set. coerce_tz should be a pytz timezone string + like "US/Pacific", or None + + format_string should be a unicode string that is a valid argument for datetime's strftime method. + """ + if dtime is not None and dtime.tzinfo is not None and coerce_tz: + try: + to_tz = timezone(coerce_tz) + except UnknownTimeZoneError: + to_tz = UTC + dtime = to_tz.normalize(dtime.astimezone(to_tz)) + if dtime is None or format_string is None: + return get_default_time_display(dtime) + try: + return unicode(strftime_localized(dtime, format_string)) + except ValueError: + return get_default_time_display(dtime) + + +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 + or timezone aren't same) + + :param dt1: + :param dt2: + """ + return abs(dt1 - dt2) < allowed_delta + + +DEFAULT_SHORT_DATE_FORMAT = "%b %d, %Y" +DEFAULT_LONG_DATE_FORMAT = "%A, %B %d, %Y" +DEFAULT_TIME_FORMAT = "%I:%M:%S %p" +DEFAULT_DATE_TIME_FORMAT = "%b %d, %Y at %H:%M" + + +def strftime_localized(dtime, format): # pylint: disable=redefined-builtin + """ + Format a datetime, just like the built-in strftime, but with localized words. + + The format string can also be one of: + + * "SHORT_DATE" for a date in brief form, localized. + + * "LONG_DATE" for a longer form of date, localized. + + * "DATE_TIME" for a date and time together, localized. + + * "TIME" for just the time, localized. + + The localization is based on the current language Django is using for the + request. The exact format strings used for each of the names above is + determined by the translator for each language. + + Args: + dtime (datetime): The datetime value to format. + + format (str): The format string to use, as specified by + :ref:`datetime.strftime`. + + Returns: + A unicode string with the formatted datetime. + + """ + + if format == "SHORT_DATE": + format = "%x" + elif format == "LONG_DATE": + # Translators: the translation for "LONG_DATE_FORMAT" must be a format + # string for formatting dates in a long form. For example, the + # American English form is "%A, %B %d %Y". + # See http://strftime.org for details. + format = ugettext("LONG_DATE_FORMAT") + if format == "LONG_DATE_FORMAT": + format = DEFAULT_LONG_DATE_FORMAT + elif format == "DATE_TIME": + # Translators: the translation for "DATE_TIME_FORMAT" must be a format + # string for formatting dates with times. For example, the American + # English form is "%b %d, %Y at %H:%M". + # See http://strftime.org for details. + format = ugettext("DATE_TIME_FORMAT") + if format == "DATE_TIME_FORMAT": + format = DEFAULT_DATE_TIME_FORMAT + elif format == "TIME": + format = "%X" + + def process_percent_code(match): + """ + Convert one percent-prefixed code in the format string. + + Called by re.sub just below. + + """ + code = match.group() + if code == "%": + # This only happens if the string ends with a %, which is not legal. + raise ValueError("strftime format ends with raw %") + + if code == "%a": + part = pgettext('abbreviated weekday name', WEEKDAYS_ABBREVIATED[dtime.weekday()]) + elif code == "%A": + part = pgettext('weekday name', WEEKDAYS[dtime.weekday()]) + elif code == "%b": + part = pgettext('abbreviated month name', MONTHS_ABBREVIATED[dtime.month]) + elif code == "%B": + part = pgettext('month name', MONTHS[dtime.month]) + elif code == "%p": + part = pgettext('am/pm indicator', AM_PM[dtime.hour // 12]) + elif code == "%x": + # Get the localized short date format, and recurse. + # Translators: the translation for "SHORT_DATE_FORMAT" must be a + # format string for formatting dates in a brief form. For example, + # the American English form is "%b %d %Y". + # See http://strftime.org for details. + actual_format = ugettext("SHORT_DATE_FORMAT") + if actual_format == "SHORT_DATE_FORMAT": + actual_format = DEFAULT_SHORT_DATE_FORMAT + if "%x" in actual_format: + # Prevent infinite accidental recursion. + actual_format = DEFAULT_SHORT_DATE_FORMAT + part = strftime_localized(dtime, actual_format) + elif code == "%X": + # Get the localized time format, and recurse. + # Translators: the translation for "TIME_FORMAT" must be a format + # string for formatting times. For example, the American English + # form is "%H:%M:%S". See http://strftime.org for details. + actual_format = ugettext("TIME_FORMAT") + if actual_format == "TIME_FORMAT": + actual_format = DEFAULT_TIME_FORMAT + if "%X" in actual_format: + # Prevent infinite accidental recursion. + actual_format = DEFAULT_TIME_FORMAT + part = strftime_localized(dtime, actual_format) + else: + # All the other format codes: just let built-in strftime take + # care of them. + part = dtime.strftime(code) + + return part + + formatted_date = re.sub(r"%.|%", process_percent_code, format) + return formatted_date + + +# In order to extract the strings below, we have to mark them with pgettext. +# But we'll do the actual pgettext later, so use a no-op for now, and save the +# real pgettext so we can assign it back to the global name later. +real_pgettext = pgettext +pgettext = lambda context, text: text # pylint: disable=invalid-name + +AM_PM = { + # Translators: This is an AM/PM indicator for displaying times. It is + # used for the %p directive in date-time formats. See http://strftime.org + # for details. + 0: pgettext('am/pm indicator', 'AM'), + # Translators: This is an AM/PM indicator for displaying times. It is + # used for the %p directive in date-time formats. See http://strftime.org + # for details. + 1: pgettext('am/pm indicator', 'PM'), +} + +WEEKDAYS = { + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Monday Februrary 10, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 0: pgettext('weekday name', 'Monday'), + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Tuesday Februrary 11, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 1: pgettext('weekday name', 'Tuesday'), + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Wednesday Februrary 12, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 2: pgettext('weekday name', 'Wednesday'), + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Thursday Februrary 13, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 3: pgettext('weekday name', 'Thursday'), + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Friday Februrary 14, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 4: pgettext('weekday name', 'Friday'), + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Saturday Februrary 15, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 5: pgettext('weekday name', 'Saturday'), + # Translators: this is a weekday name that will be used when displaying + # dates, as in "Sunday Februrary 16, 2014". It is used for the %A + # directive in date-time formats. See http://strftime.org for details. + 6: pgettext('weekday name', 'Sunday'), +} + +WEEKDAYS_ABBREVIATED = { + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Mon Feb 10, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 0: pgettext('abbreviated weekday name', 'Mon'), + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Tue Feb 11, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 1: pgettext('abbreviated weekday name', 'Tue'), + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Wed Feb 12, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 2: pgettext('abbreviated weekday name', 'Wed'), + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Thu Feb 13, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 3: pgettext('abbreviated weekday name', 'Thu'), + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Fri Feb 14, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 4: pgettext('abbreviated weekday name', 'Fri'), + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Sat Feb 15, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 5: pgettext('abbreviated weekday name', 'Sat'), + # Translators: this is an abbreviated weekday name that will be used when + # displaying dates, as in "Sun Feb 16, 2014". It is used for the %a + # directive in date-time formats. See http://strftime.org for details. + 6: pgettext('abbreviated weekday name', 'Sun'), +} + +MONTHS_ABBREVIATED = { + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Jan 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 1: pgettext('abbreviated month name', 'Jan'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Feb 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 2: pgettext('abbreviated month name', 'Feb'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Mar 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 3: pgettext('abbreviated month name', 'Mar'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Apr 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 4: pgettext('abbreviated month name', 'Apr'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "May 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 5: pgettext('abbreviated month name', 'May'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Jun 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 6: pgettext('abbreviated month name', 'Jun'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Jul 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 7: pgettext('abbreviated month name', 'Jul'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Aug 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 8: pgettext('abbreviated month name', 'Aug'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Sep 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 9: pgettext('abbreviated month name', 'Sep'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Oct 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 10: pgettext('abbreviated month name', 'Oct'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Nov 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 11: pgettext('abbreviated month name', 'Nov'), + # Translators: this is an abbreviated month name that will be used when + # displaying dates, as in "Dec 10, 2014". It is used for the %b + # directive in date-time formats. See http://strftime.org for details. + 12: pgettext('abbreviated month name', 'Dec'), +} + +MONTHS = { + # Translators: this is a month name that will be used when displaying + # dates, as in "January 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 1: pgettext('month name', 'January'), + # Translators: this is a month name that will be used when displaying + # dates, as in "February 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 2: pgettext('month name', 'February'), + # Translators: this is a month name that will be used when displaying + # dates, as in "March 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 3: pgettext('month name', 'March'), + # Translators: this is a month name that will be used when displaying + # dates, as in "April 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 4: pgettext('month name', 'April'), + # Translators: this is a month name that will be used when displaying + # dates, as in "May 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 5: pgettext('month name', 'May'), + # Translators: this is a month name that will be used when displaying + # dates, as in "June 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 6: pgettext('month name', 'June'), + # Translators: this is a month name that will be used when displaying + # dates, as in "July 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 7: pgettext('month name', 'July'), + # Translators: this is a month name that will be used when displaying + # dates, as in "August 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 8: pgettext('month name', 'August'), + # Translators: this is a month name that will be used when displaying + # dates, as in "September 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 9: pgettext('month name', 'September'), + # Translators: this is a month name that will be used when displaying + # dates, as in "October 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 10: pgettext('month name', 'October'), + # Translators: this is a month name that will be used when displaying + # dates, as in "November 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 11: pgettext('month name', 'November'), + # Translators: this is a month name that will be used when displaying + # dates, as in "December 10, 2014". It is used for the %B directive in + # date-time formats. See http://strftime.org for details. + 12: pgettext('month name', 'December'), +} + +# Now that we are done defining constants, we have to restore the real pgettext +# so that the functions in this module will have the right definition. +pgettext = real_pgettext diff --git a/common/djangoapps/util/tests/test_date_utils.py b/common/djangoapps/util/tests/test_date_utils.py new file mode 100644 index 0000000000..9dddd46456 --- /dev/null +++ b/common/djangoapps/util/tests/test_date_utils.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +""" +Tests for util.date_utils +""" + +from datetime import datetime, timedelta, tzinfo +from functools import partial +import unittest + +import ddt +from mock import patch +from nose.tools import assert_equals, assert_false # pylint: disable=E0611 +from pytz import UTC + +from util.date_utils import ( + get_default_time_display, get_time_display, almost_same_datetime, + strftime_localized, +) + + +def test_get_default_time_display(): + assert_equals("", get_default_time_display(None)) + test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC) + assert_equals( + "Mar 12, 1992 at 15:03 UTC", + get_default_time_display(test_time)) + + +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)) + + +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, "%")) + + +def test_get_time_display_coerce(): + test_time_standard = datetime(1992, 1, 12, 15, 3, 30, tzinfo=UTC) + test_time_daylight = datetime(1992, 7, 12, 15, 3, 30, tzinfo=UTC) + assert_equals("Jan 12, 1992 at 07:03 PST", + get_time_display(test_time_standard, None, coerce_tz="US/Pacific")) + assert_equals("Jan 12, 1992 at 15:03 UTC", + get_time_display(test_time_standard, None, coerce_tz="NONEXISTENTTZ")) + assert_equals("Jan 12 07:03", + get_time_display(test_time_standard, '%b %d %H:%M', coerce_tz="US/Pacific")) + assert_equals("Jul 12, 1992 at 08:03 PDT", + get_time_display(test_time_daylight, None, coerce_tz="US/Pacific")) + assert_equals("Jul 12, 1992 at 15:03 UTC", + get_time_display(test_time_daylight, None, coerce_tz="NONEXISTENTTZ")) + assert_equals("Jul 12 08:03", + get_time_display(test_time_daylight, '%b %d %H:%M', coerce_tz="US/Pacific")) + + +# pylint: disable=W0232 +class NamelessTZ(tzinfo): + """Static timezone for testing""" + + def utcoffset(self, _dt): + return timedelta(hours=-3) + + def dst(self, _dt): + return timedelta(0) + + +def test_get_default_time_display_no_tzname(): + assert_equals("", get_default_time_display(None)) + test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=NamelessTZ()) + assert_equals( + "Mar 12, 1992 at 15:03-0300", + get_default_time_display(test_time)) + + +def test_almost_same_datetime(): + assert almost_same_datetime( + datetime(2013, 5, 3, 10, 20, 30), + datetime(2013, 5, 3, 10, 21, 29) + ) + + assert almost_same_datetime( + datetime(2013, 5, 3, 11, 20, 30), + datetime(2013, 5, 3, 10, 21, 29), + timedelta(hours=1) + ) + + assert_false( + almost_same_datetime( + datetime(2013, 5, 3, 11, 20, 30), + datetime(2013, 5, 3, 10, 21, 29) + ) + ) + + assert_false( + almost_same_datetime( + datetime(2013, 5, 3, 11, 20, 30), + datetime(2013, 5, 3, 10, 21, 29), + timedelta(minutes=10) + ) + ) + + +def fake_ugettext(text, translations): + """ + A fake implementation of ugettext, for testing. + """ + return translations.get(text, text) + + +def fake_pgettext(context, text, translations): + """ + A fake implementation of pgettext, for testing. + """ + return translations.get((context, text), text) + + +@ddt.ddt +class StrftimeLocalizedTest(unittest.TestCase): + """ + Tests for strftime_localized. + """ + @ddt.data( + ("%Y", "2013"), + ("%m/%d/%y", "02/14/13"), + ("hello", "hello"), + (u'%Y년 %m월 %d일', u"2013년 02월 14일"), + ("%a, %b %d, %Y", "Thu, Feb 14, 2013"), + ("%I:%M:%S %p", "04:41:17 PM"), + ) + def test_usual_strftime_behavior(self, (fmt, expected)): + dtime = datetime(2013, 02, 14, 16, 41, 17) + self.assertEqual(expected, strftime_localized(dtime, fmt)) + # strftime doesn't like Unicode, so do the work in UTF8. + self.assertEqual(expected, dtime.strftime(fmt.encode('utf8')).decode('utf8')) + + @ddt.data( + ("SHORT_DATE", "Feb 14, 2013"), + ("LONG_DATE", "Thursday, February 14, 2013"), + ("TIME", "04:41:17 PM"), + ("%x %X!", "Feb 14, 2013 04:41:17 PM!"), + ) + def test_shortcuts(self, (fmt, expected)): + dtime = datetime(2013, 02, 14, 16, 41, 17) + self.assertEqual(expected, strftime_localized(dtime, fmt)) + + @patch('util.date_utils.pgettext', partial(fake_pgettext, translations={ + ("abbreviated month name", "Feb"): "XXfebXX", + ("month name", "February"): "XXfebruaryXX", + ("abbreviated weekday name", "Thu"): "XXthuXX", + ("weekday name", "Thursday"): "XXthursdayXX", + ("am/pm indicator", "PM"): "XXpmXX", + })) + @ddt.data( + ("SHORT_DATE", "XXfebXX 14, 2013"), + ("LONG_DATE", "XXthursdayXX, XXfebruaryXX 14, 2013"), + ("DATE_TIME", "XXfebXX 14, 2013 at 16:41"), + ("TIME", "04:41:17 XXpmXX"), + ("%x %X!", "XXfebXX 14, 2013 04:41:17 XXpmXX!"), + ) + def test_translated_words(self, (fmt, expected)): + dtime = datetime(2013, 02, 14, 16, 41, 17) + self.assertEqual(expected, strftime_localized(dtime, fmt)) + + @patch('util.date_utils.ugettext', partial(fake_ugettext, translations={ + "SHORT_DATE_FORMAT": "date(%Y.%m.%d)", + "LONG_DATE_FORMAT": "date(%A.%Y.%B.%d)", + "DATE_TIME_FORMAT": "date(%Y.%m.%d@%H.%M)", + "TIME_FORMAT": "%Hh.%Mm.%Ss", + })) + @ddt.data( + ("SHORT_DATE", "date(2013.02.14)"), + ("Look: %x", "Look: date(2013.02.14)"), + ("LONG_DATE", "date(Thursday.2013.February.14)"), + ("DATE_TIME", "date(2013.02.14@16.41)"), + ("TIME", "16h.41m.17s"), + ("The time is: %X", "The time is: 16h.41m.17s"), + ("%x %X", "date(2013.02.14) 16h.41m.17s"), + ) + def test_translated_formats(self, (fmt, expected)): + dtime = datetime(2013, 02, 14, 16, 41, 17) + self.assertEqual(expected, strftime_localized(dtime, fmt)) + + @patch('util.date_utils.ugettext', partial(fake_ugettext, translations={ + "SHORT_DATE_FORMAT": "oops date(%Y.%x.%d)", + "TIME_FORMAT": "oops %Hh.%Xm.%Ss", + })) + @ddt.data( + ("SHORT_DATE", "Feb 14, 2013"), + ("TIME", "04:41:17 PM"), + ) + def test_recursion_protection(self, (fmt, expected)): + dtime = datetime(2013, 02, 14, 16, 41, 17) + self.assertEqual(expected, strftime_localized(dtime, fmt)) + + @ddt.data( + "%", + "Hello%" + "%Y/%m/%d%", + ) + def test_invalid_format_strings(self, fmt): + dtime = datetime(2013, 02, 14, 16, 41, 17) + with self.assertRaises(ValueError): + strftime_localized(dtime, fmt) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 2b5ec749f1..66b3c0630f 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -828,13 +828,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): Returns the desired text corresponding the course's start date. Prefers .advertised_start, then falls back to .start """ + i18n = self.runtime.service(self, "i18n") + _ = i18n.ugettext + strftime = i18n.strftime + def try_parse_iso_8601(text): try: result = Date().from_json(text) if result is None: result = text.title() else: - result = result.strftime("%b %d, %Y") + result = strftime(result, "SHORT_DATE") except ValueError: result = text.title() @@ -843,12 +847,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): if isinstance(self.advertised_start, basestring): return try_parse_iso_8601(self.advertised_start) elif self.start_date_is_still_default: - _ = self.runtime.service(self, "i18n").ugettext # Translators: TBD stands for 'To Be Determined' and is used when a course # does not yet have an announced start date. return _('TBD') else: - return (self.advertised_start or self.start).strftime("%b %d, %Y") + when = self.advertised_start or self.start + return strftime(when, "SHORT_DATE") @property def start_date_is_still_default(self): @@ -865,7 +869,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): If the course does not have an end date set (course.end is None), an empty string will be returned. """ - return '' if self.end is None else self.end.strftime("%b %d, %Y") + if self.end is None: + return '' + else: + strftime = self.runtime.service(self, "i18n").strftime + return strftime(self.end, "SHORT_DATE") @property def forum_posts_allowed(self): diff --git a/common/lib/xmodule/xmodule/tests/test_date_utils.py b/common/lib/xmodule/xmodule/tests/test_date_utils.py deleted file mode 100644 index ae6fd7f109..0000000000 --- a/common/lib/xmodule/xmodule/tests/test_date_utils.py +++ /dev/null @@ -1,106 +0,0 @@ -"""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, get_time_display, almost_same_datetime -from datetime import datetime, timedelta, tzinfo -from pytz import UTC, timezone - - -def test_get_default_time_display(): - assert_equals("", get_default_time_display(None)) - test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=UTC) - assert_equals( - "Mar 12, 1992 at 15:03 UTC", - get_default_time_display(test_time)) - - -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)) - - -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, "%")) - - -def test_get_time_display_coerce(): - test_time_standard = datetime(1992, 1, 12, 15, 3, 30, tzinfo=UTC) - test_time_daylight = datetime(1992, 7, 12, 15, 3, 30, tzinfo=UTC) - assert_equals("Jan 12, 1992 at 07:03 PST", - get_time_display(test_time_standard, None, coerce_tz="US/Pacific")) - assert_equals("Jan 12, 1992 at 15:03 UTC", - get_time_display(test_time_standard, None, coerce_tz="NONEXISTENTTZ")) - assert_equals("Jan 12 07:03", - get_time_display(test_time_standard, '%b %d %H:%M', coerce_tz="US/Pacific")) - assert_equals("Jul 12, 1992 at 08:03 PDT", - get_time_display(test_time_daylight, None, coerce_tz="US/Pacific")) - assert_equals("Jul 12, 1992 at 15:03 UTC", - get_time_display(test_time_daylight, None, coerce_tz="NONEXISTENTTZ")) - assert_equals("Jul 12 08:03", - get_time_display(test_time_daylight, '%b %d %H:%M', coerce_tz="US/Pacific")) - - -# pylint: disable=W0232 -class NamelessTZ(tzinfo): - """Static timezone for testing""" - - def utcoffset(self, _dt): - return timedelta(hours=-3) - - def dst(self, _dt): - return timedelta(0) - - -def test_get_default_time_display_no_tzname(): - assert_equals("", get_default_time_display(None)) - test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=NamelessTZ()) - assert_equals( - "Mar 12, 1992 at 15:03-0300", - get_default_time_display(test_time)) - - -def test_almost_same_datetime(): - assert almost_same_datetime( - datetime(2013, 5, 3, 10, 20, 30), - datetime(2013, 5, 3, 10, 21, 29) - ) - - assert almost_same_datetime( - datetime(2013, 5, 3, 11, 20, 30), - datetime(2013, 5, 3, 10, 21, 29), - timedelta(hours=1) - ) - - assert_false( - almost_same_datetime( - datetime(2013, 5, 3, 11, 20, 30), - datetime(2013, 5, 3, 10, 21, 29) - ) - ) - - assert_false( - almost_same_datetime( - datetime(2013, 5, 3, 11, 20, 30), - datetime(2013, 5, 3, 10, 21, 29), - timedelta(minutes=10) - ) - ) diff --git a/common/lib/xmodule/xmodule/util/date_utils.py b/common/lib/xmodule/xmodule/util/date_utils.py deleted file mode 100644 index a862bc7d31..0000000000 --- a/common/lib/xmodule/xmodule/util/date_utils.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Convenience methods for working with datetime objects -""" -from datetime import timedelta -from pytz import timezone, UTC, UnknownTimeZoneError - -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 UTC". - - If None is passed in for dt, an empty string will be returned. - """ - if dtime is None: - return u"" - 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, coerce_tz=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`. - - Coerces aware datetime to tz=coerce_tz if set. coerce_tz should be a pytz timezone string - like "US/Pacific", or None - - format_string should be a unicode string that is a valid argument for datetime's strftime method. - """ - if dtime is not None and dtime.tzinfo is not None and coerce_tz: - try: - to_tz = timezone(coerce_tz) - except UnknownTimeZoneError: - to_tz = UTC - dtime = to_tz.normalize(dtime.astimezone(to_tz)) - 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)): - """ - Returns true if these are w/in a minute of each other. (in case secs saved to db - or timezone aren't same) - - :param dt1: - :param dt2: - """ - return abs(dt1 - dt2) < allowed_delta diff --git a/conf/locale/eo/LC_MESSAGES/django.mo b/conf/locale/eo/LC_MESSAGES/django.mo index 79f0d07160..d3e1a12b80 100644 Binary files a/conf/locale/eo/LC_MESSAGES/django.mo and b/conf/locale/eo/LC_MESSAGES/django.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/django.po b/conf/locale/eo/LC_MESSAGES/django.po index 8a06f0ed7c..a3d301bd8c 100644 --- a/conf/locale/eo/LC_MESSAGES/django.po +++ b/conf/locale/eo/LC_MESSAGES/django.po @@ -37,7 +37,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2014-02-18 13:34-0500\n" +"POT-Creation-Date: 2014-02-18 16:36-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -338,6 +338,357 @@ msgstr "Nämé réqüïréd Ⱡ'#" msgid "Invalid ID" msgstr "Ìnvälïd ÌD Ⱡ#" +#. Translators: the translation for "LONG_DATE_FORMAT" must be a format +#. string for formatting dates in a long form. For example, the +#. American English form is "%A, %B %d %Y". +#. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgid "LONG_DATE_FORMAT" +msgstr "" + +#. Translators: the translation for "DATE_TIME_FORMAT" must be a format +#. string for formatting dates with times. For example, the American +#. English form is "%b %d, %Y at %H:%M". +#. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgid "DATE_TIME_FORMAT" +msgstr "" + +#. Translators: the translation for "SHORT_DATE_FORMAT" must be a +#. format string for formatting dates in a brief form. For example, +#. the American English form is "%b %d %Y". +#. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgid "SHORT_DATE_FORMAT" +msgstr "" + +#. Translators: the translation for "TIME_FORMAT" must be a format +#. string for formatting times. For example, the American English +#. form is "%H:%M:%S". See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgid "TIME_FORMAT" +msgstr "" + +#. Translators: This is an AM/PM indicator for displaying times. It is +#. used for the %p directive in date-time formats. See http://strftime.org +#. for details. +#: common/djangoapps/util/date_utils.py +msgctxt "am/pm indicator" +msgid "AM" +msgstr "ÀM Ⱡ'#" + +#. Translators: This is an AM/PM indicator for displaying times. It is +#. used for the %p directive in date-time formats. See http://strftime.org +#. for details. +#: common/djangoapps/util/date_utils.py +msgctxt "am/pm indicator" +msgid "PM" +msgstr "PM Ⱡ'#" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Monday Februrary 10, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Monday" +msgstr "Möndäý Ⱡ'σяєм ιρѕ#" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Tuesday Februrary 11, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Tuesday" +msgstr "Tüésdäý #" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Wednesday Februrary 12, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Wednesday" +msgstr "Wédnésdäý #" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Thursday Februrary 13, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Thursday" +msgstr "Thürsdäý #" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Friday Februrary 14, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Friday" +msgstr "Frïdäý Ⱡ'σяєм ιρѕ#" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Saturday Februrary 15, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Saturday" +msgstr "Sätürdäý #" + +#. Translators: this is a weekday name that will be used when displaying +#. dates, as in "Sunday Februrary 16, 2014". It is used for the %A +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "weekday name" +msgid "Sunday" +msgstr "Sündäý Ⱡ'σяєм ιρѕ#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Mon Feb 10, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Mon" +msgstr "Mön Ⱡ'σя#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Tue Feb 11, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Tue" +msgstr "Tüé Ⱡ'σя#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Wed Feb 12, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Wed" +msgstr "Wéd Ⱡ'σя#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Thu Feb 13, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Thu" +msgstr "Thü Ⱡ'σя#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Fri Feb 14, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Fri" +msgstr "Frï Ⱡ'σя#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Sat Feb 15, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Sat" +msgstr "Sät Ⱡ'σя#" + +#. Translators: this is an abbreviated weekday name that will be used when +#. displaying dates, as in "Sun Feb 16, 2014". It is used for the %a +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated weekday name" +msgid "Sun" +msgstr "Sün Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Jan 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Jan" +msgstr "Jän Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Feb 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Feb" +msgstr "Féß Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Mar 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Mar" +msgstr "Mär Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Apr 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Apr" +msgstr "Àpr Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "May 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "May" +msgstr "Mäý Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Jun 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Jun" +msgstr "Jün Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Jul 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Jul" +msgstr "Jül Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Aug 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Aug" +msgstr "Àüg Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Sep 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Sep" +msgstr "Sép Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Oct 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Oct" +msgstr "Öçt Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Nov 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Nov" +msgstr "Növ Ⱡ'σя#" + +#. Translators: this is an abbreviated month name that will be used when +#. displaying dates, as in "Dec 10, 2014". It is used for the %b +#. directive in date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "abbreviated month name" +msgid "Dec" +msgstr "Déç Ⱡ'σя#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "January 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "January" +msgstr "Jänüärý #" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "February 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "February" +msgstr "Féßrüärý #" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "March 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "March" +msgstr "Märçh Ⱡ'σяєм ι#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "April 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "April" +msgstr "Àprïl Ⱡ'σяєм ι#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "May 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "May" +msgstr "Mäý Ⱡ'σя#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "June 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "June" +msgstr "Jüné Ⱡ'σяєм#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "July 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "July" +msgstr "Jülý Ⱡ'σяєм#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "August 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "August" +msgstr "Àügüst Ⱡ'σяєм ιρѕ#" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "September 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "September" +msgstr "Séptémßér #" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "October 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "October" +msgstr "Öçtößér #" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "November 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "November" +msgstr "Növémßér #" + +#. Translators: this is a month name that will be used when displaying +#. dates, as in "December 10, 2014". It is used for the %B directive in +#. date-time formats. See http://strftime.org for details. +#: common/djangoapps/util/date_utils.py +msgctxt "month name" +msgid "December" +msgstr "Déçémßér #" + #: common/djangoapps/util/password_policy_validators.py msgid "Invalid Length ({0})" msgstr "Ìnvälïd Léngth ({0}) Ⱡ'σя#" diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.mo b/conf/locale/eo/LC_MESSAGES/djangojs.mo index 271498c7e0..be17cb59c1 100644 Binary files a/conf/locale/eo/LC_MESSAGES/djangojs.mo and b/conf/locale/eo/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.po b/conf/locale/eo/LC_MESSAGES/djangojs.po index 942fc28f82..2710944286 100644 --- a/conf/locale/eo/LC_MESSAGES/djangojs.po +++ b/conf/locale/eo/LC_MESSAGES/djangojs.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2014-02-18 13:34-0500\n" -"PO-Revision-Date: 2014-02-18 18:34:51.762964\n" +"POT-Creation-Date: 2014-02-18 16:36-0500\n" +"PO-Revision-Date: 2014-02-18 21:36:33.541469\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" diff --git a/i18n/dummy.py b/i18n/dummy.py index d72c31eb55..27309fc342 100755 --- a/i18n/dummy.py +++ b/i18n/dummy.py @@ -23,7 +23,9 @@ generates output conf/locale/$DUMMY_LOCALE/LC_MESSAGES, where $DUMMY_LOCALE is the dummy_locale value set in the i18n config """ +import re import sys + import polib from path import path @@ -173,6 +175,10 @@ def make_dummy(filename, locale, converter): raise IOError('File does not exist: %r' % filename) pofile = polib.pofile(filename) for msg in pofile: + # Some strings are actually formatting strings, don't dummy-ify them, + # or dates will look like "DÀTÉ_TÌMÉ_FÖRMÀT Ⱡ'σ# EST" + if re.match(r"^[A-Z_]+_FORMAT$", msg.msgid): + continue converter.convert_msg(msg) # Apply declaration for English pluralization rules so that ngettext will diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index cb2036a04c..382131f262 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -25,8 +25,6 @@ from lms.lib.xblock.runtime import LmsModuleSystem, unquote_slashes from edxmako.shortcuts import render_to_string from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from student.models import anonymous_id_for_user, user_by_anonymous_id -from util.json_request import JsonResponse -from util.sandboxing import can_execute_unsafe_code from xblock.core import XBlock from xblock.fields import Scope from xblock.runtime import KvsFieldData, KeyValueStore @@ -42,6 +40,10 @@ from xmodule_modifiers import replace_course_urls, replace_jump_to_id_urls, repl from xmodule.lti_module import LTIModule from xmodule.x_module import XModuleDescriptor +from util.date_utils import strftime_localized +from util.json_request import JsonResponse +from util.sandboxing import can_execute_unsafe_code + log = logging.getLogger(__name__) @@ -428,10 +430,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours wrappers=block_wrappers, get_real_user=user_by_anonymous_id, services={ - # django.utils.translation implements the gettext.Translations - # interface (it has ugettext, ungettext, etc), so we can use it - # directly as the runtime i18n service. - 'i18n': django.utils.translation, + 'i18n': ModuleI18nService(), }, get_user_role=lambda: get_user_role(user, course_id), ) @@ -652,3 +651,20 @@ def _check_files_limits(files): return msg return None + + +class ModuleI18nService(object): + """ + Implement the XBlock runtime "i18n" service. + + Mostly a pass-through to Django's translation module. + django.utils.translation implements the gettext.Translations interface (it + has ugettext, ungettext, etc), so we can use it directly as the runtime + i18n service. + + """ + def __getattr__(self, name): + return getattr(django.utils.translation, name) + + def strftime(self, *args, **kwargs): + return strftime_localized(*args, **kwargs) diff --git a/lms/templates/courseware/accordion.html b/lms/templates/courseware/accordion.html index d857fc3ba0..ae344bdcc5 100644 --- a/lms/templates/courseware/accordion.html +++ b/lms/templates/courseware/accordion.html @@ -1,6 +1,6 @@ <%! from django.core.urlresolvers import reverse - from xmodule.util.date_utils import get_time_display + from util.date_utils import get_time_display from django.utils.translation import ugettext as _ from django.conf import settings %> @@ -31,7 +31,7 @@ due_date = '' else: formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=settings.TIME_ZONE) - due_date = '' if len(formatted_string)==0 else _('due {date}'.format(date=formatted_string)) + due_date = '' if len(formatted_string)==0 else _('due {date}').format(date=formatted_string) %>

${section['format']} ${due_date}

diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 7eeea8da59..02cec1f373 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -17,7 +17,7 @@ %> <%! -from xmodule.util.date_utils import get_time_display +from util.date_utils import get_time_display from django.conf import settings %> diff --git a/lms/templates/folditbasic.html b/lms/templates/folditbasic.html index e05ceb1452..e5073751bb 100644 --- a/lms/templates/folditbasic.html +++ b/lms/templates/folditbasic.html @@ -1,7 +1,7 @@ <%! from django.utils.translation import ugettext as _ %> <%! -from xmodule.util.date_utils import get_default_time_display +from util.date_utils import get_default_time_display %>

${_("Due:")} ${get_default_time_display(due)} diff --git a/lms/templates/index.html b/lms/templates/index.html index d8e6d75278..b2fdfe0633 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -1,7 +1,6 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> -<%! from time import strftime %> <%inherit file="main.html" /> <%namespace name='static' file='static_content.html'/> diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index d32df83bb1..f08722afed 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -15,7 +15,7 @@ -e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk # Our libraries: --e git+https://github.com/edx/XBlock.git@6d431d786587bd8f3a19a893364914d6e2d6c28f#egg=XBlock +-e git+https://github.com/edx/XBlock.git@893cd83dfb24405ce81b07f49c1c2e3053cdc865#egg=XBlock -e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail -e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool