Commit 399e5209 by Ned Batchelder

Merge pull request #2608 from edx/ned/date-l10n

strftime_localized
parents c246f7da de5f3028
......@@ -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 _
......
......@@ -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
......
<%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
%>
......
<%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
<a href="#" class="edit-release-date action" data-date="" data-time="" data-locator="${section_locator}"><i class="icon-time"></i> <span class="sr">${_("Schedule")}</span></a>
%else:
<span class="published-status"><strong>${_("Release date:")}</strong>
${date_utils.get_default_time_display(section.start)}</span>
${get_default_time_display(section.start)}</span>
<a href="#" class="edit-release-date action" data-date="${start_date_str}" data-time="${start_time_str}" data-locator="${section_locator}"><i class="icon-time"></i> <span class="sr">${_("Edit section release date")}</span></a>
%endif
</div>
......
"""Tests for xmodule.util.date_utils"""
# -*- coding: utf-8 -*-
"""
Tests for 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
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():
......@@ -104,3 +116,106 @@ def test_almost_same_datetime():
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)
......@@ -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):
......
"""
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
......@@ -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 <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n"
......
......@@ -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
......
......@@ -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)
<%!
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)
%>
<p class="subtitle">${section['format']} ${due_date}</p>
</a>
......
......@@ -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
%>
......
<%! 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
%>
<div class="folditbasic">
<p><strong>${_("Due:")}</strong> ${get_default_time_display(due)}
......
<%! 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'/>
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment