Commit 147639ab by Calen Pennington Committed by GitHub

Merge pull request #14197 from cpennington/fix-release-merge-conflict

Fix release merge conflict
parents b34d110b a24ac515
...@@ -19,7 +19,7 @@ import calc ...@@ -19,7 +19,7 @@ import calc
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey, UsageKey
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
...@@ -53,6 +53,26 @@ def ensure_valid_course_key(view_func): ...@@ -53,6 +53,26 @@ def ensure_valid_course_key(view_func):
return inner return inner
def ensure_valid_usage_key(view_func):
"""
This decorator should only be used with views which have argument usage_key_string.
If usage_key_string is not valid raise 404.
"""
@wraps(view_func)
def inner(request, *args, **kwargs): # pylint: disable=missing-docstring
usage_key = kwargs.get('usage_key_string')
if usage_key is not None:
try:
UsageKey.from_string(usage_key)
except InvalidKeyError:
raise Http404
response = view_func(request, *args, **kwargs)
return response
return inner
def require_global_staff(func): def require_global_staff(func):
"""View decorator that requires that the user have global staff permissions. """ """View decorator that requires that the user have global staff permissions. """
@wraps(func) @wraps(func)
......
...@@ -190,10 +190,7 @@ class LoncapaResponse(object): ...@@ -190,10 +190,7 @@ class LoncapaResponse(object):
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
for prop in self.required_attributes: for prop in self.required_attributes:
prop_value = xml.get(prop) if not xml.get(prop):
if prop_value: # Stripping off the empty strings
prop_value = prop_value.strip()
if not prop_value:
msg = "Error in problem specification: %s missing required attribute %s" % ( msg = "Error in problem specification: %s missing required attribute %s" % (
unicode(self), prop) unicode(self), prop)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
......
...@@ -974,12 +974,12 @@ class StringResponseTest(ResponseTest): # pylint: disable=missing-docstring ...@@ -974,12 +974,12 @@ class StringResponseTest(ResponseTest): # pylint: disable=missing-docstring
hint = correct_map.get_hint('1_2_1') hint = correct_map.get_hint('1_2_1')
self.assertEqual(hint, self._get_random_number_result(problem.seed)) self.assertEqual(hint, self._get_random_number_result(problem.seed))
def test_empty_answer_problem_creation_not_allowed(self): def test_empty_answer_graded_as_incorrect(self):
""" """
Tests that empty answer string is not allowed to create a problem Tests that problem should be graded incorrect if blank space is chosen as answer
""" """
with self.assertRaises(LoncapaProblemError): problem = self.build_problem(answer=" ", case_sensitive=False, regexp=True)
self.build_problem(answer=" ", case_sensitive=False, regexp=True) self.assert_grade(problem, u" ", "incorrect")
class CodeResponseTest(ResponseTest): # pylint: disable=missing-docstring class CodeResponseTest(ResponseTest): # pylint: disable=missing-docstring
......
...@@ -10,11 +10,8 @@ from datetime import datetime, timedelta ...@@ -10,11 +10,8 @@ from datetime import datetime, timedelta
import dateutil.parser import dateutil.parser
from math import exp from math import exp
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from pytz import utc from pytz import utc
from .fields import Date
DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc) DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=utc)
...@@ -96,83 +93,6 @@ def course_start_date_is_default(start, advertised_start): ...@@ -96,83 +93,6 @@ def course_start_date_is_default(start, advertised_start):
return advertised_start is None and start == DEFAULT_START_DATE return advertised_start is None and start == DEFAULT_START_DATE
def _datetime_to_string(date_time, format_string, time_zone, strftime_localized):
"""
Formats the given datetime with the given function and format string.
Adds time zone abbreviation to the resulting string if the format is DATE_TIME or TIME.
Arguments:
date_time (datetime): the datetime to be formatted
format_string (str): the date format type, as passed to strftime
time_zone (pytz time zone): the time zone to convert to
strftime_localized ((datetime, str) -> str): a nm localized string
formatting function
"""
result = strftime_localized(date_time.astimezone(time_zone), format_string)
abbr = get_time_zone_abbr(time_zone, date_time)
return (
result + ' ' + abbr if format_string in ['DATE_TIME', 'TIME', 'DAY_AND_TIME']
else result
)
def course_start_datetime_text(start_date, advertised_start, format_string, time_zone, ugettext, strftime_localized):
"""
Calculates text to be shown to user regarding a course's start
datetime in specified time zone.
Prefers .advertised_start, then falls back to .start.
Arguments:
start_date (datetime): the course's start datetime
advertised_start (str): the course's advertised start date
format_string (str): the date format type, as passed to strftime
time_zone (pytz time zone): the time zone to convert to
ugettext ((str) -> str): a text localization function
strftime_localized ((datetime, str) -> str): a localized string
formatting function
"""
if advertised_start is not None:
# TODO: This will return an empty string if advertised_start == ""... consider changing this behavior?
try:
# from_json either returns a Date, returns None, or raises a ValueError
parsed_advertised_start = Date().from_json(advertised_start)
if parsed_advertised_start is not None:
# In the Django implementation of strftime_localized, if
# the year is <1900, _datetime_to_string will raise a ValueError.
return _datetime_to_string(parsed_advertised_start, format_string, time_zone, strftime_localized)
except ValueError:
pass
return advertised_start.title()
elif start_date != DEFAULT_START_DATE:
return _datetime_to_string(start_date, format_string, time_zone, strftime_localized)
else:
_ = ugettext
# Translators: TBD stands for 'To Be Determined' and is used when a course
# does not yet have an announced start date.
return _('TBD')
def course_end_datetime_text(end_date, format_string, time_zone, strftime_localized):
"""
Returns a formatted string for a course's end date or datetime.
If end_date is None, an empty string will be returned.
Arguments:
end_date (datetime): the end datetime of a course
format_string (str): the date format type, as passed to strftime
time_zone (pytz time zone): the time zone to convert to
strftime_localized ((datetime, str) -> str): a localized string
formatting function
"""
return (
_datetime_to_string(end_date, format_string, time_zone, strftime_localized) if end_date is not None
else ''
)
def may_certify_for_course(certificates_display_behavior, certificates_show_before_end, has_ended): def may_certify_for_course(certificates_display_behavior, certificates_show_before_end, has_ended):
""" """
Returns whether it is acceptable to show the student a certificate download Returns whether it is acceptable to show the student a certificate download
......
...@@ -197,10 +197,11 @@ class CourseFields(object): ...@@ -197,10 +197,11 @@ class CourseFields(object):
scope=Scope.settings, scope=Scope.settings,
) )
advertised_start = String( advertised_start = String(
display_name=_("Course Advertised Start Date"), display_name=_("Course Advertised Start"),
help=_( help=_(
"Enter the date you want to advertise as the course start date, if this date is different from the set " "Enter the text that you want to use as the advertised starting time frame for the course, "
"start date. To advertise the set start date, enter null." "such as \"Winter 2018\". If you enter null for this value, the start date that you have set "
"for this course is used."
), ),
scope=Scope.settings scope=Scope.settings
) )
...@@ -1211,21 +1212,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): ...@@ -1211,21 +1212,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
"""Return the course_id for this course""" """Return the course_id for this course"""
return self.location.course_key return self.location.course_key
def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the desired text corresponding the course's start date and time in specified time zone, defaulted
to UTC. Prefers .advertised_start, then falls back to .start
"""
i18n = self.runtime.service(self, "i18n")
return course_metadata_utils.course_start_datetime_text(
self.start,
self.advertised_start,
format_string,
time_zone,
i18n.ugettext,
i18n.strftime
)
@property @property
def start_date_is_still_default(self): def start_date_is_still_default(self):
""" """
...@@ -1237,17 +1223,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): ...@@ -1237,17 +1223,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
self.advertised_start self.advertised_start
) )
def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the end date or date_time for the course formatted as a string.
"""
return course_metadata_utils.course_end_datetime_text(
self.end,
format_string,
time_zone,
self.runtime.service(self, "i18n").strftime
)
def get_discussion_blackout_datetimes(self): def get_discussion_blackout_datetimes(self):
""" """
Get a list of dicts with start and end fields with datetime values from Get a list of dicts with start and end fields with datetime values from
......
...@@ -5,7 +5,7 @@ from collections import namedtuple ...@@ -5,7 +5,7 @@ from collections import namedtuple
from datetime import timedelta, datetime from datetime import timedelta, datetime
from unittest import TestCase from unittest import TestCase
from pytz import timezone, utc from pytz import utc
from xmodule.block_metadata_utils import ( from xmodule.block_metadata_utils import (
url_name_for_block, url_name_for_block,
display_name_with_default, display_name_with_default,
...@@ -18,11 +18,8 @@ from xmodule.course_metadata_utils import ( ...@@ -18,11 +18,8 @@ from xmodule.course_metadata_utils import (
has_course_ended, has_course_ended,
DEFAULT_START_DATE, DEFAULT_START_DATE,
course_start_date_is_default, course_start_date_is_default,
course_start_datetime_text,
course_end_datetime_text,
may_certify_for_course, may_certify_for_course,
) )
from xmodule.fields import Date
from xmodule.modulestore.tests.utils import ( from xmodule.modulestore.tests.utils import (
MongoModulestoreBuilder, MongoModulestoreBuilder,
VersioningModulestoreBuilder, VersioningModulestoreBuilder,
...@@ -112,12 +109,6 @@ class CourseMetadataUtilsTestCase(TestCase): ...@@ -112,12 +109,6 @@ class CourseMetadataUtilsTestCase(TestCase):
test_datetime = datetime(1945, 2, 6, 4, 20, 00, tzinfo=utc) test_datetime = datetime(1945, 2, 6, 4, 20, 00, tzinfo=utc)
advertised_start_parsable = "2038-01-19 03:14:07" advertised_start_parsable = "2038-01-19 03:14:07"
advertised_start_bad_date = "215-01-01 10:10:10"
advertised_start_unparsable = "This coming fall"
time_zone_normal_parsable = "2016-03-27 00:59:00"
time_zone_normal_datetime = datetime(2016, 3, 27, 00, 59, 00, tzinfo=utc)
time_zone_daylight_parsable = "2016-03-27 01:00:00"
time_zone_daylight_datetime = datetime(2016, 3, 27, 1, 00, 00, tzinfo=utc)
FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name
TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name
...@@ -169,79 +160,6 @@ class CourseMetadataUtilsTestCase(TestCase): ...@@ -169,79 +160,6 @@ class CourseMetadataUtilsTestCase(TestCase):
TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False), TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False),
TestScenario((DEFAULT_START_DATE, None), True), TestScenario((DEFAULT_START_DATE, None), True),
]), ]),
FunctionTest(course_start_datetime_text, [
# Test parsable advertised start date.
# Expect start datetime to be parsed and formatted back into a string.
TestScenario(
(DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME',
utc, noop_gettext, mock_strftime_localized),
mock_strftime_localized(Date().from_json(advertised_start_parsable), 'DATE_TIME') + " UTC"
),
# Test un-parsable advertised start date.
# Expect date parsing to throw a ValueError, and the advertised
# start to be returned in Title Case.
TestScenario(
(test_datetime, advertised_start_unparsable, 'DATE_TIME',
utc, noop_gettext, mock_strftime_localized),
advertised_start_unparsable.title()
),
# Test parsable advertised start date from before January 1, 1900.
# Expect mock_strftime_localized to throw a ValueError, and the
# advertised start to be returned in Title Case.
TestScenario(
(test_datetime, advertised_start_bad_date, 'DATE_TIME',
utc, noop_gettext, mock_strftime_localized),
advertised_start_bad_date.title()
),
# Test without advertised start date, but with a set start datetime.
# Expect formatted datetime to be returned.
TestScenario(
(test_datetime, None, 'SHORT_DATE', utc, noop_gettext, mock_strftime_localized),
mock_strftime_localized(test_datetime, 'SHORT_DATE')
),
# Test without advertised start date and with default start datetime.
# Expect TBD to be returned.
TestScenario(
(DEFAULT_START_DATE, None, 'SHORT_DATE', utc, noop_gettext, mock_strftime_localized),
'TBD'
),
# Test correctly formatted start datetime is returned during normal daylight hours
TestScenario(
(DEFAULT_START_DATE, time_zone_normal_parsable, 'DATE_TIME',
timezone('Europe/Paris'), noop_gettext, mock_strftime_localized),
"DATE_TIME " + "2016-03-27 01:59:00 CET"
),
# Test correctly formatted start datetime is returned during daylight savings hours
TestScenario(
(DEFAULT_START_DATE, time_zone_daylight_parsable, 'DATE_TIME',
timezone('Europe/Paris'), noop_gettext, mock_strftime_localized),
"DATE_TIME " + "2016-03-27 03:00:00 CEST"
)
]),
FunctionTest(course_end_datetime_text, [
# Test with a set end datetime.
# Expect formatted datetime to be returned.
TestScenario(
(test_datetime, 'TIME', utc, mock_strftime_localized),
mock_strftime_localized(test_datetime, 'TIME') + " UTC"
),
# Test with default end datetime.
# Expect empty string to be returned.
TestScenario(
(None, 'TIME', utc, mock_strftime_localized),
""
),
# Test correctly formatted end datetime is returned during normal daylight hours
TestScenario(
(time_zone_normal_datetime, 'TIME', timezone('Europe/Paris'), mock_strftime_localized),
"TIME " + "2016-03-27 01:59:00 CET"
),
# Test correctly formatted end datetime is returned during daylight savings hours
TestScenario(
(time_zone_daylight_datetime, 'TIME', timezone('Europe/Paris'), mock_strftime_localized),
"TIME " + "2016-03-27 03:00:00 CEST"
)
]),
FunctionTest(may_certify_for_course, [ FunctionTest(may_certify_for_course, [
TestScenario(('early_with_info', True, True), True), TestScenario(('early_with_info', True, True), True),
TestScenario(('early_no_info', False, False), True), TestScenario(('early_no_info', False, False), True),
......
...@@ -6,7 +6,7 @@ from datetime import datetime, timedelta ...@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
import itertools import itertools
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
from pytz import timezone, utc from pytz import utc
from xblock.runtime import KvsFieldData, DictKeyValueStore from xblock.runtime import KvsFieldData, DictKeyValueStore
import xmodule.course_module import xmodule.course_module
...@@ -209,36 +209,6 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -209,36 +209,6 @@ class IsNewCourseTestCase(unittest.TestCase):
(xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False, 'January 2014'), (xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False, 'January 2014'),
] ]
@patch('xmodule.course_metadata_utils.datetime.now')
def test_start_date_text(self, gmtime_mock):
gmtime_mock.return_value = NOW
for s in self.start_advertised_settings:
d = get_dummy_course(start=s[0], advertised_start=s[1])
print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_datetime_text(), s[2])
@patch('xmodule.course_metadata_utils.datetime.now')
def test_start_date_time_text(self, gmtime_mock):
gmtime_mock.return_value = NOW
for setting in self.start_advertised_settings:
course = get_dummy_course(start=setting[0], advertised_start=setting[1])
print "Checking start=%s advertised=%s" % (setting[0], setting[1])
self.assertEqual(course.start_datetime_text("DATE_TIME"), setting[4])
@ddt.data(("2015-11-01T08:59", 'Nov 01, 2015', u'Nov 01, 2015 at 01:59 PDT'),
("2015-11-01T09:00", 'Nov 01, 2015', u'Nov 01, 2015 at 01:00 PST'))
@ddt.unpack
def test_start_date_time_zone(self, course_date, expected_short_date, expected_date_time):
"""
Test that start datetime text correctly formats datetimes
for normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
course = get_dummy_course(start=course_date, advertised_start=course_date)
self.assertEqual(course.start_datetime_text(time_zone=time_zone), expected_short_date)
self.assertEqual(course.start_datetime_text("DATE_TIME", time_zone), expected_date_time)
def test_start_date_is_default(self): def test_start_date_is_default(self):
for s in self.start_advertised_settings: for s in self.start_advertised_settings:
d = get_dummy_course(start=s[0], advertised_start=s[1]) d = get_dummy_course(start=s[0], advertised_start=s[1])
...@@ -276,36 +246,6 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -276,36 +246,6 @@ class IsNewCourseTestCase(unittest.TestCase):
descriptor = get_dummy_course(start='2012-12-31T12:00') descriptor = get_dummy_course(start='2012-12-31T12:00')
assert descriptor.is_newish is True assert descriptor.is_newish is True
def test_end_date_text(self):
# No end date set, returns empty string.
d = get_dummy_course('2012-12-02T12:00')
self.assertEqual('', d.end_datetime_text())
d = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014', d.end_datetime_text())
def test_end_date_time_text(self):
# No end date set, returns empty string.
course = get_dummy_course('2012-12-02T12:00')
self.assertEqual('', course.end_datetime_text("DATE_TIME"))
course = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014 at 12:00 UTC', course.end_datetime_text("DATE_TIME"))
@ddt.data(("2015-11-01T08:59", 'Nov 01, 2015', u'Nov 01, 2015 at 01:59 PDT'),
("2015-11-01T09:00", 'Nov 01, 2015', u'Nov 01, 2015 at 01:00 PST'))
@ddt.unpack
def test_end_date_time_zone(self, course_date, expected_short_date, expected_date_time):
"""
Test that end datetime text correctly formats datetimes
for normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
course = get_dummy_course(course_date, end=course_date)
self.assertEqual(course.end_datetime_text(time_zone=time_zone), expected_short_date)
self.assertEqual(course.end_datetime_text("DATE_TIME", time_zone), expected_date_time)
class DiscussionTopicsTestCase(unittest.TestCase): class DiscussionTopicsTestCase(unittest.TestCase):
def test_default_discussion_topics(self): def test_default_discussion_topics(self):
......
...@@ -421,7 +421,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, AcceptanceTest): ...@@ -421,7 +421,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, AcceptanceTest):
self._test_dropdown_field( self._test_dropdown_field(
u'time_zone', u'time_zone',
u'Time Zone', u'Time Zone',
u'', u'Default (Local Time Zone)',
[ [
u'Europe/Kiev ({abbr}, UTC{offset})'.format(abbr=kiev_abbr, offset=kiev_offset), u'Europe/Kiev ({abbr}, UTC{offset})'.format(abbr=kiev_abbr, offset=kiev_offset),
u'US/Pacific ({abbr}, UTC{offset})'.format(abbr=pacific_abbr, offset=pacific_offset), u'US/Pacific ({abbr}, UTC{offset})'.format(abbr=pacific_abbr, offset=pacific_offset),
......
...@@ -12,7 +12,6 @@ from pytz import utc ...@@ -12,7 +12,6 @@ from pytz import utc
from lazy import lazy from lazy import lazy
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, LocationKeyField from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -84,36 +83,6 @@ class CustomCourseForEdX(models.Model): ...@@ -84,36 +83,6 @@ class CustomCourseForEdX(models.Model):
return datetime.now(utc) > self.due return datetime.now(utc) > self.due
def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""Returns the desired text representation of the CCX start datetime
The returned value is in specified time zone, defaulted to UTC.
"""
i18n = self.course.runtime.service(self.course, "i18n")
strftime = i18n.strftime
value = strftime(self.start.astimezone(time_zone), format_string)
if format_string == 'DATE_TIME':
value += ' ' + get_time_zone_abbr(time_zone, self.start)
return value
def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""Returns the desired text representation of the CCX due datetime
If the due date for the CCX is not set, the value returned is the empty
string.
The returned value is in specified time zone, defaulted to UTC.
"""
if self.due is None:
return ''
i18n = self.course.runtime.service(self.course, "i18n")
strftime = i18n.strftime
value = strftime(self.due.astimezone(time_zone), format_string)
if format_string == 'DATE_TIME':
value += ' ' + get_time_zone_abbr(time_zone, self.due)
return value
@property @property
def structure(self): def structure(self):
""" """
......
...@@ -4,14 +4,12 @@ tests for the models ...@@ -4,14 +4,12 @@ tests for the models
import ddt import ddt
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from pytz import timezone, utc from pytz import utc
from student.roles import CourseCcxCoachRole from student.roles import CourseCcxCoachRole
from student.tests.factories import ( from student.tests.factories import (
AdminFactory, AdminFactory,
) )
from util.tests.test_date_utils import fake_ugettext
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, ModuleStoreTestCase,
TEST_DATA_SPLIT_MODULESTORE TEST_DATA_SPLIT_MODULESTORE
...@@ -152,95 +150,6 @@ class TestCCX(ModuleStoreTestCase): ...@@ -152,95 +150,6 @@ class TestCCX(ModuleStoreTestCase):
"""verify that a ccx without a due date has not ended""" """verify that a ccx without a due date has not ended"""
self.assertFalse(self.ccx.has_ended()) # pylint: disable=no-member self.assertFalse(self.ccx.has_ended()) # pylint: disable=no-member
# ensure that the expected localized format will be found by the i18n
# service
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"SHORT_DATE_FORMAT": "%b %d, %Y",
}))
def test_start_datetime_short_date(self):
"""verify that the start date for a ccx formats properly by default"""
start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015"
self.set_ccx_override('start', start)
actual = self.ccx.start_datetime_text() # pylint: disable=no-member
self.assertEqual(expected, actual)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
def test_start_datetime_date_time_format(self):
"""verify that the DATE_TIME format also works as expected"""
start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015 at 12:00 UTC"
self.set_ccx_override('start', start)
actual = self.ccx.start_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
@ddt.data((datetime(2015, 11, 1, 8, 59, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:59 PDT"),
(datetime(2015, 11, 1, 9, 00, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:00 PST"))
@ddt.unpack
def test_start_date_time_zone(self, start_date_time, expected_short_date, expected_date_time):
"""
verify that start date is correctly converted when time zone specified
during normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
self.set_ccx_override('start', start_date_time)
actual_short_date = self.ccx.start_datetime_text(time_zone=time_zone) # pylint: disable=no-member
actual_datetime = self.ccx.start_datetime_text('DATE_TIME', time_zone) # pylint: disable=no-member
self.assertEqual(expected_short_date, actual_short_date)
self.assertEqual(expected_date_time, actual_datetime)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"SHORT_DATE_FORMAT": "%b %d, %Y",
}))
def test_end_datetime_short_date(self):
"""verify that the end date for a ccx formats properly by default"""
end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015"
self.set_ccx_override('due', end)
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
self.assertEqual(expected, actual)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
def test_end_datetime_date_time_format(self):
"""verify that the DATE_TIME format also works as expected"""
end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015 at 12:00 UTC"
self.set_ccx_override('due', end)
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
@ddt.data((datetime(2015, 11, 1, 8, 59, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:59 PDT"),
(datetime(2015, 11, 1, 9, 00, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:00 PST"))
@ddt.unpack
def test_end_datetime_time_zone(self, end_date_time, expected_short_date, expected_date_time):
"""
verify that end date is correctly converted when time zone specified
during normal daylight hours and daylight savings hours
"""
time_zone = timezone('America/Los_Angeles')
self.set_ccx_override('due', end_date_time)
actual_short_date = self.ccx.end_datetime_text(time_zone=time_zone) # pylint: disable=no-member
actual_datetime = self.ccx.end_datetime_text('DATE_TIME', time_zone) # pylint: disable=no-member
self.assertEqual(expected_short_date, actual_short_date)
self.assertEqual(expected_date_time, actual_datetime)
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
def test_end_datetime_no_due_date(self):
"""verify that without a due date, the end date is an empty string"""
expected = ''
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
self.assertEqual(expected, actual)
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
def test_ccx_max_student_enrollment_correct(self): def test_ccx_max_student_enrollment_correct(self):
""" """
Verify the override value for max_student_enrollments_allowed Verify the override value for max_student_enrollments_allowed
......
...@@ -1992,11 +1992,19 @@ class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase): ...@@ -1992,11 +1992,19 @@ class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase):
reload_django_url_config() reload_django_url_config()
super(TestRenderXBlock, self).setUp() super(TestRenderXBlock, self).setUp()
def get_response(self, url_encoded_params=None): def test_render_xblock_with_invalid_usage_key(self):
"""
Test XBlockRendering with invalid usage key
"""
response = self.get_response(usage_key='some_invalid_usage_key')
self.assertEqual(response.status_code, 404)
self.assertIn('Page not found', response.content)
def get_response(self, url_encoded_params=None, usage_key=None):
""" """
Overridable method to get the response from the endpoint that is being tested. Overridable method to get the response from the endpoint that is being tested.
""" """
url = reverse('render_xblock', kwargs={"usage_key_string": unicode(self.html_block.location)}) url = reverse('render_xblock', kwargs={'usage_key_string': unicode(usage_key)})
if url_encoded_params: if url_encoded_params:
url += '?' + url_encoded_params url += '?' + url_encoded_params
return self.client.get(url) return self.client.get(url)
......
...@@ -41,12 +41,13 @@ class RenderXBlockTestMixin(object): ...@@ -41,12 +41,13 @@ class RenderXBlockTestMixin(object):
] ]
@abstractmethod @abstractmethod
def get_response(self, url_encoded_params=None): def get_response(self, url_encoded_params=None, usage_key=None):
""" """
Abstract method to get the response from the endpoint that is being tested. Abstract method to get the response from the endpoint that is being tested.
Arguments: Arguments:
url_encoded_params - URL encoded parameters that should be appended to the requested URL. url_encoded_params - URL encoded parameters that should be appended to the requested URL.
usage_key - course usage key.
""" """
pass # pragma: no cover pass # pragma: no cover
...@@ -96,7 +97,7 @@ class RenderXBlockTestMixin(object): ...@@ -96,7 +97,7 @@ class RenderXBlockTestMixin(object):
""" """
if url_params: if url_params:
url_params = urlencode(url_params) url_params = urlencode(url_params)
response = self.get_response(url_params) response = self.get_response(url_params, self.html_block.location)
if expected_response_code == 200: if expected_response_code == 200:
self.assertContains(response, self.html_block.data, status_code=expected_response_code) self.assertContains(response, self.html_block.data, status_code=expected_response_code)
for chrome_element in [self.COURSEWARE_CHROME_HTML_ELEMENTS + self.XBLOCK_REMOVED_HTML_ELEMENTS]: for chrome_element in [self.COURSEWARE_CHROME_HTML_ELEMENTS + self.XBLOCK_REMOVED_HTML_ELEMENTS]:
......
...@@ -88,7 +88,7 @@ from util.date_utils import strftime_localized ...@@ -88,7 +88,7 @@ from util.date_utils import strftime_localized
from util.db import outer_atomic from util.db import outer_atomic
from util.milestones_helpers import get_prerequisite_courses_display from util.milestones_helpers import get_prerequisite_courses_display
from util.views import _record_feedback_in_zendesk from util.views import _record_feedback_in_zendesk
from util.views import ensure_valid_course_key from util.views import ensure_valid_course_key, ensure_valid_usage_key
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.tabs import CourseTabList from xmodule.tabs import CourseTabList
...@@ -1236,12 +1236,14 @@ def _track_successful_certificate_generation(user_id, course_id): # pylint: dis ...@@ -1236,12 +1236,14 @@ def _track_successful_certificate_generation(user_id, course_id): # pylint: dis
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
@ensure_valid_usage_key
def render_xblock(request, usage_key_string, check_if_enrolled=True): def render_xblock(request, usage_key_string, check_if_enrolled=True):
""" """
Returns an HttpResponse with HTML content for the xBlock with the given usage_key. Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware). The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
""" """
usage_key = UsageKey.from_string(usage_key_string) usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
course_key = usage_key.course_key course_key = usage_key.course_key
......
...@@ -20,6 +20,7 @@ from django.db import models ...@@ -20,6 +20,7 @@ from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from eventtracking import tracker from eventtracking import tracker
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from track import contexts
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from coursewarehistoryextended.fields import UnsignedBigIntAutoField from coursewarehistoryextended.fields import UnsignedBigIntAutoField
...@@ -433,8 +434,12 @@ class PersistentSubsectionGrade(TimeStampedModel): ...@@ -433,8 +434,12 @@ class PersistentSubsectionGrade(TimeStampedModel):
Emits an edx.grades.subsection.grade_calculated event Emits an edx.grades.subsection.grade_calculated event
with data from the passed grade. with data from the passed grade.
""" """
# TODO: remove this context manager after completion of AN-6134
event_name = u'edx.grades.subsection.grade_calculated'
context = contexts.course_context_from_course_id(grade.course_id)
with tracker.get_tracker().context(event_name, context):
tracker.emit( tracker.emit(
u'edx.grades.subsection.grade_calculated', event_name,
{ {
'user_id': unicode(grade.user_id), 'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id), 'course_id': unicode(grade.course_id),
...@@ -543,8 +548,12 @@ class PersistentCourseGrade(TimeStampedModel): ...@@ -543,8 +548,12 @@ class PersistentCourseGrade(TimeStampedModel):
Emits an edx.grades.course.grade_calculated event Emits an edx.grades.course.grade_calculated event
with data from the passed grade. with data from the passed grade.
""" """
# TODO: remove this context manager after completion of AN-6134
event_name = u'edx.grades.course.grade_calculated'
context = contexts.course_context_from_course_id(grade.course_id)
with tracker.get_tracker().context(event_name, context):
tracker.emit( tracker.emit(
u'edx.grades.course.grade_calculated', event_name,
{ {
'user_id': unicode(grade.user_id), 'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id), 'course_id': unicode(grade.course_id),
......
...@@ -130,8 +130,14 @@ class SubsectionGrade(object): ...@@ -130,8 +130,14 @@ class SubsectionGrade(object):
Compute score for the given block. If persisted_values Compute score for the given block. If persisted_values
is provided, it is used for possible and weight. is provided, it is used for possible and weight.
""" """
try:
block = course_structure[block_key] block = course_structure[block_key]
except KeyError:
# It's possible that the user's access to that
# block has changed since the subsection grade
# was last persisted.
pass
else:
if getattr(block, 'has_score', False): if getattr(block, 'has_score', False):
problem_score = get_score( problem_score = get_score(
submissions_scores, submissions_scores,
......
"""
Test grading with access changes.
"""
# pylint: disable=protected-access
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.course_blocks.api import get_course_blocks
from openedx.core.djangolib.testing.utils import get_mock_request
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
from ...new.subsection_grade import SubsectionGradeFactory
class GradesAccessIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
"""
Tests integration between grading and block access.
"""
@classmethod
def setUpClass(cls):
super(GradesAccessIntegrationTest, cls).setUpClass()
cls.store = modulestore()
cls.course = CourseFactory.create()
cls.chapter = ItemFactory.create(
parent=cls.course,
category="chapter",
display_name="Test Chapter"
)
cls.sequence = ItemFactory.create(
parent=cls.chapter,
category='sequential',
display_name="Test Sequential 1",
graded=True,
format="Homework"
)
cls.vertical = ItemFactory.create(
parent=cls.sequence,
category='vertical',
display_name='Test Vertical 1'
)
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
question_text='The correct answer is Choice 2',
choices=[False, False, True, False],
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
)
cls.problem = ItemFactory.create(
parent=cls.vertical,
category="problem",
display_name="p1",
data=problem_xml,
metadata={'weight': 2}
)
cls.problem_2 = ItemFactory.create(
parent=cls.vertical,
category="problem",
display_name="p2",
data=problem_xml,
metadata={'weight': 2}
)
def setUp(self):
super(GradesAccessIntegrationTest, self).setUp()
self.request = get_mock_request(UserFactory())
self.student = self.request.user
self.client.login(username=self.student.username, password="test")
CourseEnrollment.enroll(self.student, self.course.id)
self.instructor = UserFactory.create(is_staff=True, username=u'test_instructor', password=u'test')
self.refresh_course()
def test_subsection_access_changed(self):
"""
Tests retrieving a subsection grade before and after losing access
to a block in the subsection.
"""
# submit answers
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
self.submit_question_answer('p2', {'2_1': 'choice_choice_2'})
# check initial subsection grade
course_structure = get_course_blocks(self.request.user, self.course.location)
subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, course_structure)
grade = subsection_grade_factory.create(self.sequence, read_only=True)
self.assertEqual(grade.graded_total.earned, 4.0)
self.assertEqual(grade.graded_total.possible, 4.0)
# set a block in the subsection to be visible to staff only
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
problem_2 = self.store.get_item(self.problem_2.location)
problem_2.visible_to_staff_only = True
self.store.update_item(problem_2, self.instructor.id)
self.store.publish(self.course.location, self.instructor.id)
course_structure = get_course_blocks(self.student, self.course.location)
# ensure that problem_2 is not accessible for the student
self.assertNotIn(problem_2.location, course_structure)
# make sure we can still get the subsection grade
subsection_grade_factory = SubsectionGradeFactory(self.student, self.course, course_structure)
grade = subsection_grade_factory.create(self.sequence, read_only=True)
self.assertEqual(grade.graded_total.earned, 4.0)
self.assertEqual(grade.graded_total.possible, 4.0)
""" """
Test grading event across apps. Test grading events across apps.
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
...@@ -80,8 +80,8 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe ...@@ -80,8 +80,8 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'}) self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
# check logging to make sure id's are tracked correctly across events # check logging to make sure id's are tracked correctly across events
event_transaction_id = handlers_tracker.method_calls[0][1][1]['event_transaction_id'] event_transaction_id = handlers_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
for call in models_tracker.method_calls: for call in models_tracker.emit.mock_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id']) self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(SUBMITTED_TYPE), call[1][1]['event_transaction_type']) self.assertEqual(unicode(SUBMITTED_TYPE), call[1][1]['event_transaction_type'])
...@@ -123,7 +123,7 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe ...@@ -123,7 +123,7 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
event_transaction_id = enrollment_tracker.method_calls[0][1][1]['event_transaction_id'] event_transaction_id = enrollment_tracker.method_calls[0][1][1]['event_transaction_id']
# make sure the id is propagated throughout the event flow # make sure the id is propagated throughout the event flow
for call in models_tracker.method_calls: for call in models_tracker.emit.mock_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id']) self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(STATE_DELETED_TYPE), call[1][1]['event_transaction_type']) self.assertEqual(unicode(STATE_DELETED_TYPE), call[1][1]['event_transaction_type'])
...@@ -188,13 +188,23 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe ...@@ -188,13 +188,23 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
) )
# check logging to make sure id's are tracked correctly across # check logging to make sure id's are tracked correctly across
# events # events
event_transaction_id = instructor_task_tracker.method_calls[0][1][1]['event_transaction_id'] event_transaction_id = instructor_task_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
self.assertEqual(
instructor_task_tracker.get_tracker().context.call_args[0],
('edx.grades.problem.rescored', {'course_id': unicode(self.course.id), 'org_id': unicode(self.course.org)})
)
# make sure the id is propagated throughout the event flow # make sure the id is propagated throughout the event flow
for call in models_tracker.method_calls: for call in models_tracker.emit.mock_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id']) self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(RESCORE_TYPE), call[1][1]['event_transaction_type']) self.assertEqual(unicode(RESCORE_TYPE), call[1][1]['event_transaction_type'])
# make sure the models calls have re-added the course id to the context
for args in models_tracker.get_tracker().context.call_args_list:
self.assertEqual(
args[0][1],
{'course_id': unicode(self.course.id), 'org_id': unicode(self.course.org)}
)
handlers_tracker.assert_not_called() handlers_tracker.assert_not_called()
instructor_task_tracker.emit.assert_called_with( instructor_task_tracker.emit.assert_called_with(
......
...@@ -28,6 +28,7 @@ from lms.djangoapps.instructor.paidcourse_enrollment_report import PaidCourseEnr ...@@ -28,6 +28,7 @@ from lms.djangoapps.instructor.paidcourse_enrollment_report import PaidCourseEnr
from lms.djangoapps.teams.models import CourseTeamMembership from lms.djangoapps.teams.models import CourseTeamMembership
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from pytz import UTC from pytz import UTC
from track import contexts
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.split_test_module import get_split_user_partitions from xmodule.split_test_module import get_split_user_partitions
...@@ -573,6 +574,10 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude ...@@ -573,6 +574,10 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
result['new_raw_possible'], result['new_raw_possible'],
module_descriptor.weight, module_descriptor.weight,
) )
# TODO: remove this context manager after completion of AN-6134
context = contexts.course_context_from_course_id(course_id)
with tracker.get_tracker().context(GRADES_RESCORE_EVENT_TYPE, context):
tracker.emit( tracker.emit(
unicode(GRADES_RESCORE_EVENT_TYPE), unicode(GRADES_RESCORE_EVENT_TYPE),
{ {
......
...@@ -12,6 +12,7 @@ from openedx.core.djangoapps.catalog.utils import get_programs as get_catalog_pr ...@@ -12,6 +12,7 @@ from openedx.core.djangoapps.catalog.utils import get_programs as get_catalog_pr
from openedx.core.djangoapps.credentials.utils import get_programs_credentials from openedx.core.djangoapps.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs import utils from openedx.core.djangoapps.programs import utils
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
@login_required @login_required
...@@ -75,7 +76,8 @@ def program_details(request, program_id): ...@@ -75,7 +76,8 @@ def program_details(request, program_id):
'show_program_listing': programs_config.show_program_listing, 'show_program_listing': programs_config.show_program_listing,
'nav_hidden': True, 'nav_hidden': True,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True 'uses_pattern_library': True,
'user_preferences': get_user_preferences(request.user)
} }
return render_to_response('learner_dashboard/program_details.html', context) return render_to_response('learner_dashboard/program_details.html', context)
...@@ -170,7 +170,7 @@ class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCa ...@@ -170,7 +170,7 @@ class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCa
This class overrides the get_response method, which is used by This class overrides the get_response method, which is used by
the tests defined in RenderXBlockTestMixin. the tests defined in RenderXBlockTestMixin.
""" """
def get_response(self, url_encoded_params=None): def get_response(self, url_encoded_params=None, usage_key=None):
""" """
Overridable method to get the response from the endpoint that is being tested. Overridable method to get the response from the endpoint that is being tested.
""" """
...@@ -178,7 +178,7 @@ class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCa ...@@ -178,7 +178,7 @@ class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCa
'lti_provider_launch', 'lti_provider_launch',
kwargs={ kwargs={
'course_id': unicode(self.course.id), 'course_id': unicode(self.course.id),
'usage_id': unicode(self.html_block.location) 'usage_id': unicode(usage_key)
} }
) )
if url_encoded_params: if url_encoded_params:
......
...@@ -332,7 +332,7 @@ class Order(models.Model): ...@@ -332,7 +332,7 @@ class Order(models.Model):
""" """
this function generates the csv file this function generates the csv file
""" """
course_info = [] course_names = []
csv_file = StringIO.StringIO() csv_file = StringIO.StringIO()
csv_writer = csv.writer(csv_file) csv_writer = csv.writer(csv_file)
csv_writer.writerow(['Course Name', 'Registration Code', 'URL']) csv_writer.writerow(['Course Name', 'Registration Code', 'URL'])
...@@ -340,15 +340,15 @@ class Order(models.Model): ...@@ -340,15 +340,15 @@ class Order(models.Model):
course_id = item.course_id course_id = item.course_id
course = get_course_by_id(item.course_id, depth=0) course = get_course_by_id(item.course_id, depth=0)
registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id, order=self) registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id, order=self)
course_info.append((course.display_name, ' (' + course.start_datetime_text() + '-' + course.end_datetime_text() + ')')) course_names.append(course.display_name)
for registration_code in registration_codes: for registration_code in registration_codes:
redemption_url = reverse('register_code_redemption', args=[registration_code.code]) redemption_url = reverse('register_code_redemption', args=[registration_code.code])
url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url) url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url)
csv_writer.writerow([unicode(course.display_name).encode("utf-8"), registration_code.code, url]) csv_writer.writerow([unicode(course.display_name).encode("utf-8"), registration_code.code, url])
return csv_file, course_info return csv_file, course_names
def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, courses_info): def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, course_names):
""" """
send confirmation e-mail send confirmation e-mail
""" """
...@@ -358,8 +358,7 @@ class Order(models.Model): ...@@ -358,8 +358,7 @@ class Order(models.Model):
joined_course_names = "" joined_course_names = ""
if self.recipient_email: if self.recipient_email:
recipient_list.append((self.recipient_name, self.recipient_email, 'email_recipient')) recipient_list.append((self.recipient_name, self.recipient_email, 'email_recipient'))
courses_names_with_dates = [course_info[0] + course_info[1] for course_info in courses_info] joined_course_names = " " + ", ".join(course_names)
joined_course_names = " " + ", ".join(courses_names_with_dates)
if not is_order_type_business: if not is_order_type_business:
subject = _("Order Payment Confirmation") subject = _("Order Payment Confirmation")
...@@ -387,7 +386,7 @@ class Order(models.Model): ...@@ -387,7 +386,7 @@ class Order(models.Model):
'recipient_type': recipient[2], 'recipient_type': recipient[2],
'site_name': site_name, 'site_name': site_name,
'order_items': orderitems, 'order_items': orderitems,
'course_names': ", ".join([course_info[0] for course_info in courses_info]), 'course_names': ", ".join(course_names),
'dashboard_url': dashboard_url, 'dashboard_url': dashboard_url,
'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
'order_placed_by': '{username} ({email})'.format( 'order_placed_by': '{username} ({email})'.format(
...@@ -477,13 +476,13 @@ class Order(models.Model): ...@@ -477,13 +476,13 @@ class Order(models.Model):
item.purchase_item() item.purchase_item()
csv_file = None csv_file = None
courses_info = [] course_names = []
if self.order_type == OrderTypes.BUSINESS: if self.order_type == OrderTypes.BUSINESS:
# #
# Generate the CSV file that contains all of the RegistrationCodes that have already been # Generate the CSV file that contains all of the RegistrationCodes that have already been
# generated when the purchase has transacted # generated when the purchase has transacted
# #
csv_file, courses_info = self.generate_registration_codes_csv(orderitems, site_name) csv_file, course_names = self.generate_registration_codes_csv(orderitems, site_name)
try: try:
pdf_file = self.generate_pdf_receipt(orderitems) pdf_file = self.generate_pdf_receipt(orderitems)
...@@ -494,7 +493,7 @@ class Order(models.Model): ...@@ -494,7 +493,7 @@ class Order(models.Model):
try: try:
self.send_confirmation_emails( self.send_confirmation_emails(
orderitems, self.order_type == OrderTypes.BUSINESS, orderitems, self.order_type == OrderTypes.BUSINESS,
csv_file, pdf_file, site_name, courses_info csv_file, pdf_file, site_name, course_names
) )
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
# Catch all exceptions here, since the Django view implicitly # Catch all exceptions here, since the Django view implicitly
......
...@@ -514,7 +514,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -514,7 +514,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
response, response,
unicode(course.id), unicode(course.id),
course.display_name, course.display_name,
course.start_datetime_text(),
courseware_url courseware_url
) )
...@@ -966,12 +965,11 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -966,12 +965,11 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
else: else:
self.assertFalse(displayed, msg="Expected '{req}' requirement to be hidden".format(req=req)) self.assertFalse(displayed, msg="Expected '{req}' requirement to be hidden".format(req=req))
def _assert_course_details(self, response, course_key, display_name, start_text, url): def _assert_course_details(self, response, course_key, display_name, url):
"""Check the course information on the page. """ """Check the course information on the page. """
response_dict = self._get_page_data(response) response_dict = self._get_page_data(response)
self.assertEqual(response_dict['course_key'], course_key) self.assertEqual(response_dict['course_key'], course_key)
self.assertEqual(response_dict['course_name'], display_name) self.assertEqual(response_dict['course_name'], display_name)
self.assertEqual(response_dict['course_start_date'], start_text)
self.assertEqual(response_dict['courseware_url'], url) self.assertEqual(response_dict['courseware_url'], url)
def _assert_user_details(self, response, full_name): def _assert_user_details(self, response, full_name):
...@@ -1001,7 +999,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -1001,7 +999,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
'full_name': pay_and_verify_div['data-full-name'], 'full_name': pay_and_verify_div['data-full-name'],
'course_key': pay_and_verify_div['data-course-key'], 'course_key': pay_and_verify_div['data-course-key'],
'course_name': pay_and_verify_div['data-course-name'], 'course_name': pay_and_verify_div['data-course-name'],
'course_start_date': pay_and_verify_div['data-course-start-date'],
'courseware_url': pay_and_verify_div['data-courseware-url'], 'courseware_url': pay_and_verify_div['data-courseware-url'],
'course_mode_name': pay_and_verify_div['data-course-mode-name'], 'course_mode_name': pay_and_verify_div['data-course-mode-name'],
'course_mode_slug': pay_and_verify_div['data-course-mode-slug'], 'course_mode_slug': pay_and_verify_div['data-course-mode-slug'],
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
// The text that appears on the dialog box when entering links. // The text that appears on the dialog box when entering links.
var linkDialogText = gettext('Insert Hyperlink'), var linkDialogText = gettext('Insert Hyperlink'),
linkUrlHelpText = gettext("e.g. 'http://google.com/'"), linkUrlHelpText = gettext("e.g. 'http://google.com'"),
linkDestinationLabel = gettext('Link Description'), linkDestinationLabel = gettext('Link Description'),
linkDestinationHelpText = gettext("e.g. 'google'"), linkDestinationHelpText = gettext("e.g. 'google'"),
linkDestinationError = gettext('Please provide a description of the link destination.'), linkDestinationError = gettext('Please provide a description of the link destination.'),
...@@ -1108,6 +1108,7 @@ ...@@ -1108,6 +1108,7 @@
imageIsDecorativeLabel: imageIsDecorativeLabel, imageIsDecorativeLabel: imageIsDecorativeLabel,
imageUploadHandler: imageUploadHandler imageUploadHandler: imageUploadHandler
}); });
dialog.setAttribute('dir', doc.head.getAttribute('dir'));
dialog.setAttribute('role', 'dialog'); dialog.setAttribute('role', 'dialog');
dialog.setAttribute('tabindex', '-1'); dialog.setAttribute('tabindex', '-1');
dialog.setAttribute('aria-labelledby', 'editorDialogTitle'); dialog.setAttribute('aria-labelledby', 'editorDialogTitle');
......
...@@ -5,17 +5,23 @@ ...@@ -5,17 +5,23 @@
'js/discovery/views/search_form', 'js/discovery/views/courses_listing', 'js/discovery/views/search_form', 'js/discovery/views/courses_listing',
'js/discovery/views/filter_bar', 'js/discovery/views/refine_sidebar'], 'js/discovery/views/filter_bar', 'js/discovery/views/refine_sidebar'],
function(Backbone, SearchState, Filters, SearchForm, CoursesListing, FilterBar, RefineSidebar) { function(Backbone, SearchState, Filters, SearchForm, CoursesListing, FilterBar, RefineSidebar) {
return function(meanings, searchQuery) { return function(meanings, searchQuery, userLanguage, userTimezone) {
var dispatcher = _.extend({}, Backbone.Events); var dispatcher = _.extend({}, Backbone.Events);
var search = new SearchState(); var search = new SearchState();
var filters = new Filters(); var filters = new Filters();
var listing = new CoursesListing({model: search.discovery});
var form = new SearchForm(); var form = new SearchForm();
var filterBar = new FilterBar({collection: filters}); var filterBar = new FilterBar({collection: filters});
var refineSidebar = new RefineSidebar({ var refineSidebar = new RefineSidebar({
collection: search.discovery.facetOptions, collection: search.discovery.facetOptions,
meanings: meanings meanings: meanings
}); });
var listing;
var courseListingModel = search.discovery;
courseListingModel.userPreferences = {
userLanguage: userLanguage,
userTimezone: userTimezone
};
listing = new CoursesListing({model: courseListingModel});
dispatcher.listenTo(form, 'search', function(query) { dispatcher.listenTo(form, 'search', function(query) {
filters.reset(); filters.reset();
......
...@@ -4,24 +4,19 @@ ...@@ -4,24 +4,19 @@
'underscore', 'underscore',
'backbone', 'backbone',
'gettext', 'gettext',
'date' 'edx-ui-toolkit/js/utils/date-utils'
], function($, _, Backbone, gettext, Date) { ], function($, _, Backbone, gettext, DateUtils) {
'use strict'; 'use strict';
function formatDate(date) { function formatDate(date, userLanguage, userTimezone) {
return dateUTC(date).toString('MMM dd, yyyy'); var context;
} context = {
datetime: date,
// Return a date object using UTC time instead of local time language: userLanguage,
function dateUTC(date) { timezone: userTimezone,
return new Date( format: DateUtils.dateFormatEnum.shortDate
date.getUTCFullYear(), };
date.getUTCMonth(), return DateUtils.localize(context);
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
);
} }
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -36,8 +31,26 @@ ...@@ -36,8 +31,26 @@
render: function() { render: function() {
var data = _.clone(this.model.attributes); var data = _.clone(this.model.attributes);
data.start = formatDate(new Date(data.start)); var userLanguage = '',
data.enrollment_start = formatDate(new Date(data.enrollment_start)); userTimezone = '';
if (this.model.userPreferences !== undefined) {
userLanguage = this.model.userPreferences.userLanguage;
userTimezone = this.model.userPreferences.userTimezone;
}
if (data.advertised_start !== undefined) {
data.start = data.advertised_start;
} else {
data.start = formatDate(
new Date(data.start),
userLanguage,
userTimezone
);
}
data.enrollment_start = formatDate(
new Date(data.enrollment_start),
userLanguage,
userTimezone
);
this.$el.html(this.tpl(data)); this.$el.html(this.tpl(data));
return this; return this;
} }
......
...@@ -31,12 +31,15 @@ ...@@ -31,12 +31,15 @@
}, },
renderItems: function() { renderItems: function() {
/* eslint no-param-reassign: [2, { "props": true }] */
var latest = this.model.latest(); var latest = this.model.latest();
var items = latest.map(function(result) { var items = latest.map(function(result) {
result.userPreferences = this.model.userPreferences;
var item = new CourseCardView({model: result}); var item = new CourseCardView({model: result});
return item.render().el; return item.render().el;
}, this); }, this);
this.$list.append(items); this.$list.append(items);
/* eslint no-param-reassign: [2, { "props": false }] */
}, },
attachScrollHandler: function() { attachScrollHandler: function() {
......
...@@ -4,14 +4,15 @@ ...@@ -4,14 +4,15 @@
(function(define) { (function(define) {
'use strict'; 'use strict';
define([ define([
'backbone' 'backbone',
'edx-ui-toolkit/js/utils/date-utils'
], ],
function(Backbone) { function(Backbone, DateUtils) {
return Backbone.Model.extend({ return Backbone.Model.extend({
initialize: function(data) { initialize: function(data) {
if (data) { if (data) {
this.context = data; this.context = data;
this.setActiveRunMode(this.getRunMode(data.run_modes)); this.setActiveRunMode(this.getRunMode(data.run_modes), data.user_preferences);
} }
}, },
...@@ -64,15 +65,44 @@ ...@@ -64,15 +65,44 @@
}); });
}, },
setActiveRunMode: function(runMode) { formatDate: function(date, userPreferences) {
var context,
userTimezone = '',
userLanguage = '';
if (userPreferences !== undefined) {
userTimezone = userPreferences.time_zone;
userLanguage = userPreferences['pref-lang'];
}
context = {
datetime: date,
timezone: userTimezone,
language: userLanguage,
format: DateUtils.dateFormatEnum.shortDate
};
return DateUtils.localize(context);
},
setActiveRunMode: function(runMode, userPreferences) {
var startDateString;
if (runMode) { if (runMode) {
if (runMode.advertised_start !== undefined && runMode.advertised_start !== 'None') {
startDateString = runMode.advertised_start;
} else {
startDateString = this.formatDate(
runMode.start_date,
userPreferences
);
}
this.set({ this.set({
certificate_url: runMode.certificate_url, certificate_url: runMode.certificate_url,
course_image_url: runMode.course_image_url || '', course_image_url: runMode.course_image_url || '',
course_key: runMode.course_key, course_key: runMode.course_key,
course_url: runMode.course_url || '', course_url: runMode.course_url || '',
display_name: this.context.display_name, display_name: this.context.display_name,
end_date: runMode.end_date, end_date: this.formatDate(
runMode.end_date,
userPreferences
),
enrollable_run_modes: this.getEnrollableRunModes(), enrollable_run_modes: this.getEnrollableRunModes(),
is_course_ended: runMode.is_course_ended, is_course_ended: runMode.is_course_ended,
is_enrolled: runMode.is_enrolled, is_enrolled: runMode.is_enrolled,
...@@ -81,13 +111,12 @@ ...@@ -81,13 +111,12 @@
marketing_url: runMode.marketing_url, marketing_url: runMode.marketing_url,
mode_slug: runMode.mode_slug, mode_slug: runMode.mode_slug,
run_key: runMode.run_key, run_key: runMode.run_key,
start_date: runMode.start_date, start_date: startDateString,
upcoming_run_modes: this.getUpcomingRunModes(), upcoming_run_modes: this.getUpcomingRunModes(),
upgrade_url: runMode.upgrade_url upgrade_url: runMode.upgrade_url
}); });
} }
}, },
setUnselected: function() { setUnselected: function() {
// Called to reset the model back to the unselected state. // Called to reset the model back to the unselected state.
var unselectedMode = this.getUnselectedRunMode(this.get('enrollable_run_modes')); var unselectedMode = this.getUnselectedRunMode(this.get('enrollable_run_modes'));
......
...@@ -33,7 +33,8 @@ ...@@ -33,7 +33,8 @@
this.options = options; this.options = options;
this.programModel = new Backbone.Model(this.options.programData); this.programModel = new Backbone.Model(this.options.programData);
this.courseCardCollection = new CourseCardCollection( this.courseCardCollection = new CourseCardCollection(
this.programModel.get('course_codes') this.programModel.get('course_codes'),
this.options.userPreferences
); );
this.render(); this.render();
}, },
......
...@@ -47,7 +47,7 @@ define([ ...@@ -47,7 +47,7 @@ define([
expect(this.view.$el.find('.course-name')).toContainHtml(data.org); expect(this.view.$el.find('.course-name')).toContainHtml(data.org);
expect(this.view.$el.find('.course-name')).toContainHtml(data.content.number); expect(this.view.$el.find('.course-name')).toContainHtml(data.content.number);
expect(this.view.$el.find('.course-name')).toContainHtml(data.content.display_name); expect(this.view.$el.find('.course-name')).toContainHtml(data.content.display_name);
expect(this.view.$el.find('.course-date')).toContainHtml('Jan 01, 1970'); expect(this.view.$el.find('.course-date').text().trim()).toEqual('Starts: Jan 1, 1970');
}); });
}); });
}); });
...@@ -30,8 +30,9 @@ define([ ...@@ -30,8 +30,9 @@ define([
context.run_modes[0].marketing_url context.run_modes[0].marketing_url
); );
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
expect(view.$('.course-details .course-text .run-period').html()) expect(view.$('.course-details .course-text .run-period').html()).toEqual(
.toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date); context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date
);
}; };
beforeEach(function() { beforeEach(function() {
...@@ -92,6 +93,15 @@ define([ ...@@ -92,6 +93,15 @@ define([
validateCourseInfoDisplay(); validateCourseInfoDisplay();
}); });
it('should show the course advertised start date', function() {
var advertisedStart = 'This is an advertised start';
context.run_modes[0].advertised_start = advertisedStart;
setupView(context, false);
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
advertisedStart + ' - ' + context.run_modes[0].end_date
);
});
it('should only show certificate status section if a certificate has been earned', function() { it('should only show certificate status section if a certificate has been earned', function() {
var certUrl = 'sample-certificate'; var certUrl = 'sample-certificate';
......
...@@ -54,7 +54,8 @@ define(['backbone', ...@@ -54,7 +54,8 @@ define(['backbone',
valueAttribute: 'time_zone', valueAttribute: 'time_zone',
groupOptions: [{ groupOptions: [{
groupTitle: gettext('All Time Zones'), groupTitle: gettext('All Time Zones'),
selectOptions: FieldViewsSpecHelpers.SELECT_OPTIONS selectOptions: FieldViewsSpecHelpers.SELECT_OPTIONS,
blankTitle: 'Default (Local Time Zone)'
}], }],
persistChanges: true, persistChanges: true,
required: true required: true
...@@ -97,7 +98,7 @@ define(['backbone', ...@@ -97,7 +98,7 @@ define(['backbone',
// expect time zone dropdown to have two subheaders (country/all time zone sub-headers) with new values // expect time zone dropdown to have two subheaders (country/all time zone sub-headers) with new values
expect(timeZoneView.$(groupsSelector).length).toBe(2); expect(timeZoneView.$(groupsSelector).length).toBe(2);
expect(timeZoneView.$(groupOptionsSelector).length).toBe(5); expect(timeZoneView.$(groupOptionsSelector).length).toBe(6);
expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('America/Guyana'); expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('America/Guyana');
// select time zone option from option // select time zone option from option
......
...@@ -118,7 +118,8 @@ ...@@ -118,7 +118,8 @@
helpMessage: gettext('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.'), // eslint-disable-line max-len helpMessage: gettext('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.'), // eslint-disable-line max-len
groupOptions: [{ groupOptions: [{
groupTitle: gettext('All Time Zones'), groupTitle: gettext('All Time Zones'),
selectOptions: fieldsData.time_zone.options selectOptions: fieldsData.time_zone.options,
blankTitle: gettext('Default (Local Time Zone)')
}], }],
persistChanges: true persistChanges: true
}) })
......
...@@ -75,7 +75,6 @@ var edx = edx || {}; ...@@ -75,7 +75,6 @@ var edx = edx || {};
'payment-confirmation-step': { 'payment-confirmation-step': {
courseKey: el.data('course-key'), courseKey: el.data('course-key'),
courseName: el.data('course-name'), courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url'), coursewareUrl: el.data('courseware-url'),
platformName: el.data('platform-name'), platformName: el.data('platform-name'),
requirements: el.data('requirements') requirements: el.data('requirements')
...@@ -94,7 +93,6 @@ var edx = edx || {}; ...@@ -94,7 +93,6 @@ var edx = edx || {};
}, },
'enrollment-confirmation-step': { 'enrollment-confirmation-step': {
courseName: el.data('course-name'), courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url'), coursewareUrl: el.data('courseware-url'),
platformName: el.data('platform-name') platformName: el.data('platform-name')
} }
......
...@@ -23,7 +23,6 @@ var edx = edx || {}; ...@@ -23,7 +23,6 @@ var edx = edx || {};
defaultContext: function() { defaultContext: function() {
return { return {
courseName: '', courseName: '',
courseStartDate: '',
coursewareUrl: '', coursewareUrl: '',
platformName: '' platformName: ''
}; };
......
...@@ -16,7 +16,6 @@ var edx = edx || {}; ...@@ -16,7 +16,6 @@ var edx = edx || {};
return { return {
courseKey: '', courseKey: '',
courseName: '', courseName: '',
courseStartDate: '',
coursewareUrl: '', coursewareUrl: '',
platformName: '', platformName: '',
requirements: [] requirements: []
......
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
.discussion-article { .discussion-article {
position: relative; position: relative;
a { a,
p {
word-wrap: break-word; word-wrap: break-word;
} }
} }
......
...@@ -98,4 +98,5 @@ ...@@ -98,4 +98,5 @@
background: $color; background: $color;
font-style: normal; font-style: normal;
color: white; color: white;
white-space: nowrap;
} }
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
position: absolute; position: absolute;
top: 100%; top: 100%;
pointer-events: none; pointer-events: none;
min-width: ($baseline*6.5); min-width: $actions-dropdown-width;
&.is-expanded { &.is-expanded {
display: block; display: block;
......
...@@ -139,6 +139,44 @@ ...@@ -139,6 +139,44 @@
> form > input[type="file"] { > form > input[type="file"] {
margin-bottom: 18px; margin-bottom: 18px;
} }
.field-group .field .field-hint {
@include margin-left(0);
width: 100%;
}
.field-input-label {
font-size: $forum-base-font-size;
}
.input-text {
width: calc(100% - 175px); // minus choose file button width
height: 40px; // align with choose file button
&.has-error {
border-color: $forum-color-error;
}
}
.field-message.has-error {
width: calc(100% - 175px); // align with input-text
background-color: $forum-color-error;
color: $white;
padding: $baseline / 2;
box-sizing: border-box;
}
.field-label {
cursor: pointer;
}
.input-checkbox {
@include margin-right($baseline / 5);
}
#new-url-input {
direction: ltr; // http url is always English
}
} }
.wmd-button-row { .wmd-button-row {
......
...@@ -35,6 +35,10 @@ $post-image-dimension: ($baseline*3) !default; // image size + margin ...@@ -35,6 +35,10 @@ $post-image-dimension: ($baseline*3) !default; // image size + margin
$response-image-dimension: ($baseline*2.5) !default; // image size + margin $response-image-dimension: ($baseline*2.5) !default; // image size + margin
$comment-image-dimension: ($baseline*2) !default; // image size + margin $comment-image-dimension: ($baseline*2) !default; // image size + margin
// action-dropdown
$actions-dropdown-width: 145px; // best estimate in RU
$actions-dropdown-offset: 100px; // actions dropdown expanded more menu
// font sizes // font sizes
$forum-base-font-size: 14px; $forum-base-font-size: 14px;
$forum-x-large-font-size: 21px; $forum-x-large-font-size: 21px;
......
...@@ -35,6 +35,10 @@ $post-image-dimension: ($baseline*3) !default; // image size + margin ...@@ -35,6 +35,10 @@ $post-image-dimension: ($baseline*3) !default; // image size + margin
$response-image-dimension: ($baseline*2.5) !default; // image size + margin $response-image-dimension: ($baseline*2.5) !default; // image size + margin
$comment-image-dimension: ($baseline*2) !default; // image size + margin $comment-image-dimension: ($baseline*2) !default; // image size + margin
// action-dropdown
$actions-dropdown-width: 145px; // best estimate in RU
$actions-dropdown-offset: 100px; // actions dropdown expanded more menu
// font sizes // font sizes
$forum-base-font-size: font-size(small); $forum-base-font-size: font-size(small);
$forum-x-large-font-size: font-size(x-large); $forum-x-large-font-size: font-size(x-large);
......
...@@ -12,15 +12,15 @@ ...@@ -12,15 +12,15 @@
border-radius: $forum-border-radius; border-radius: $forum-border-radius;
.forum-nav-bar { .forum-nav-bar {
padding: ($baseline / 2) ($baseline / 4);
color: $forum-color-navigation-bar; color: $forum-color-navigation-bar;
padding: ($baseline / 2) $baseline;
position: relative; position: relative;
.all-posts-btn { .all-posts-btn {
color: $forum-color-primary; color: $forum-color-primary;
.icon { .icon {
@include margin-left(-15px); @include margin-left($baseline / 2);
} }
} }
} }
......
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
} }
.discussion-response .response-body { .discussion-response .response-body {
@include padding(($baseline/2), $baseline, 0, 0); //ensures content doesn't overlap on post or response actions. @include padding(($baseline / 2), ($baseline * 1.5), 0, 0); //ensures content doesn't overlap on post or response actions.
margin-bottom: 0.2em; margin-bottom: 0.2em;
font-size: $forum-base-font-size; font-size: $forum-base-font-size;
} }
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
@include clearfix(); @include clearfix();
.post-header-content { .post-header-content {
max-width: calc(100% - 100px); max-width: calc(100% - #{$actions-dropdown-offset});
// post title // post title
.post-title { .post-title {
...@@ -164,6 +164,7 @@ ...@@ -164,6 +164,7 @@
.post-body { .post-body {
@extend %t-copy-sub1; @extend %t-copy-sub1;
padding: ($baseline/2) 0; padding: ($baseline/2) 0;
max-width: calc(100% - #{$actions-dropdown-offset});
} }
// post context // post context
......
...@@ -20,7 +20,9 @@ ...@@ -20,7 +20,9 @@
<%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory"> <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory">
DiscoveryFactory( DiscoveryFactory(
${course_discovery_meanings | n, dump_js_escaped_json}, ${course_discovery_meanings | n, dump_js_escaped_json},
getParameterByName('search_query') getParameterByName('search_query'),
"${user_language}",
"${user_timezone}"
); );
</%static:require_module> </%static:require_module>
</%block> </%block>
......
...@@ -20,10 +20,14 @@ ...@@ -20,10 +20,14 @@
<span class="u-field-value-readonly"></span> <span class="u-field-value-readonly"></span>
<% } else { %> <% } else { %>
<select name="select" id="u-field-select-<%- id %>" aria-describedby="u-field-help-message-<%- id %>"> <select name="select" id="u-field-select-<%- id %>" aria-describedby="u-field-help-message-<%- id %>">
<% _.each(groupOptions, function(groupOption) { %>
<% if (showBlankOption) { %> <% if (showBlankOption) { %>
<% if (groupOption.blankTitle) { %>
<option value=""><%- groupOption.blankTitle %></option>
<% } else { %>
<option value=""></option> <option value=""></option>
<% } %> <% } %>
<% _.each(groupOptions, function(groupOption) { %> <% } %>
<% if (groupOption.groupTitle != null) { %> <% if (groupOption.groupTitle != null) { %>
<optgroup label="<%- groupOption.groupTitle %>"> <optgroup label="<%- groupOption.groupTitle %>">
<% } %> <% } %>
......
...@@ -16,6 +16,7 @@ from openedx.core.djangolib.js_utils import ( ...@@ -16,6 +16,7 @@ from openedx.core.djangolib.js_utils import (
ProgramDetailsFactory({ ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json}, programData: ${program_data | n, dump_js_escaped_json},
urls: ${urls | n, dump_js_escaped_json}, urls: ${urls | n, dump_js_escaped_json},
userPreferences: ${user_preferences | n, dump_js_escaped_json},
}); });
</%static:require_module> </%static:require_module>
</%block> </%block>
......
...@@ -301,26 +301,6 @@ from openedx.core.lib.courses import course_image_url ...@@ -301,26 +301,6 @@ from openedx.core.lib.courses import course_image_url
<span class="course-registration-title">${_('Registration for:')}</span> <span class="course-registration-title">${_('Registration for:')}</span>
<span class="course-display-name">${ course.display_name | h }</span> <span class="course-display-name">${ course.display_name | h }</span>
</h3> </h3>
<p class="course-meta-info" aria-describedby="course-title">
<span class="course-dates-title">
<%
course_start_time = course.start_datetime_text()
course_end_time = course.end_datetime_text()
%>
% if course_start_time or course_end_time:
${_("Course Dates")}:
%endif
</span>
<span class="course-display-dates">
% if course_start_time:
${course_start_time}
%endif
-
% if course_end_time:
${course_end_time}
%endif
</span>
</p>
<hr> <hr>
<div class="three-col"> <div class="three-col">
% if item.status == "purchased": % if item.status == "purchased":
......
...@@ -34,12 +34,6 @@ from openedx.core.lib.courses import course_image_url ...@@ -34,12 +34,6 @@ from openedx.core.lib.courses import course_image_url
<div class="course-title"> <div class="course-title">
<h1> <h1>
${_("{course_name}").format(course_name=course.display_name) | h} ${_("{course_name}").format(course_name=course.display_name) | h}
<span class="course-dates">
${_("{start_date} - {end_date}").format(
start_date=course.start_datetime_text(),
end_date=course.end_datetime_text()
)}
</span>
</h1> </h1>
</div> </div>
<hr> <hr>
......
...@@ -34,11 +34,6 @@ from openedx.core.lib.courses import course_image_url ...@@ -34,11 +34,6 @@ from openedx.core.lib.courses import course_image_url
<div class="course-title"> <div class="course-title">
<h1> <h1>
${course.display_name | h} ${course.display_name | h}
<span class="course-dates">
${course.start_datetime_text()}
-
${course.end_datetime_text()}
</span>
</h1> </h1>
</div> </div>
<hr> <hr>
......
...@@ -74,10 +74,6 @@ from openedx.core.lib.courses import course_image_url ...@@ -74,10 +74,6 @@ from openedx.core.lib.courses import course_image_url
<span class="course-registration-title">${_('Registration for:')}</span> <span class="course-registration-title">${_('Registration for:')}</span>
<span class="course-display-name">${ course.display_name }</span> <span class="course-display-name">${ course.display_name }</span>
</h3> </h3>
<p class="course-meta-info" aria-describedby="course-title">
<span class="course-dates-title">${_('Course Dates:')}</span>
<span class="course-display-dates">${ course.start_datetime_text() } - ${ course.end_datetime_text() }</span>
</p>
<hr> <hr>
<div class="three-col"> <div class="three-col">
<div class="col-1"> <div class="col-1">
......
...@@ -13,16 +13,14 @@ ...@@ -13,16 +13,14 @@
<thead> <thead>
<tr> <tr>
<th scope="col" ><%- gettext( "Course" ) %></th> <th scope="col" ><%- gettext( "Course" ) %></th>
<th scope="col" ><%- gettext( "Status" ) %></th> <th scope="col" ></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><%- courseName %></td> <td><%- courseName %></td>
<td> <td></td>
<%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %>
</td>
</tr> </tr>
</tbody> </tbody>
......
...@@ -65,7 +65,6 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView ...@@ -65,7 +65,6 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
data-platform-name='${platform_name}' data-platform-name='${platform_name}'
data-course-key='${course_key}' data-course-key='${course_key}'
data-course-name='${course.display_name}' data-course-name='${course.display_name}'
data-course-start-date='${course.start_datetime_text()}'
data-courseware-url='${courseware_url}' data-courseware-url='${courseware_url}'
data-course-mode-name='${course_mode.name}' data-course-mode-name='${course_mode.name}'
data-course-mode-slug='${course_mode.slug}' data-course-mode-slug='${course_mode.slug}'
...@@ -124,6 +123,3 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView ...@@ -124,6 +123,3 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
</section> </section>
</div> </div>
</%block> </%block>
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
DateUtilFactory.transform(iterationKey=".localized-datetime");
</%static:require_module_async>
...@@ -9,7 +9,6 @@ from django.db import models, transaction ...@@ -9,7 +9,6 @@ from django.db import models, transaction
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.translation import ugettext
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -18,9 +17,7 @@ from opaque_keys.edx.keys import CourseKey ...@@ -18,9 +17,7 @@ from opaque_keys.edx.keys import CourseKey
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
from lms.djangoapps import django_comment_client from lms.djangoapps import django_comment_client
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from pytz import utc
from static_replace.models import AssetBaseUrlConfig from static_replace.models import AssetBaseUrlConfig
from util.date_utils import strftime_localized
from xmodule import course_metadata_utils, block_metadata_utils from xmodule import course_metadata_utils, block_metadata_utils
from xmodule.course_module import CourseDescriptor, DEFAULT_START_DATE from xmodule.course_module import CourseDescriptor, DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -359,21 +356,6 @@ class CourseOverview(TimeStampedModel): ...@@ -359,21 +356,6 @@ class CourseOverview(TimeStampedModel):
""" """
return course_metadata_utils.course_starts_within(self.start, days) return course_metadata_utils.course_starts_within(self.start, days)
def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""
Returns the desired text corresponding to the course's start date and
time in the specified time zone, or utc if no time zone given.
Prefers .advertised_start, then falls back to .start.
"""
return course_metadata_utils.course_start_datetime_text(
self.start,
self.advertised_start,
format_string,
time_zone,
ugettext,
strftime_localized
)
@property @property
def start_date_is_still_default(self): def start_date_is_still_default(self):
""" """
...@@ -385,18 +367,6 @@ class CourseOverview(TimeStampedModel): ...@@ -385,18 +367,6 @@ class CourseOverview(TimeStampedModel):
self.advertised_start, self.advertised_start,
) )
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,
format_string,
time_zone,
strftime_localized
)
@property @property
def sorting_score(self): def sorting_score(self):
""" """
......
...@@ -127,10 +127,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -127,10 +127,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
('clean_id', ('#',)), ('clean_id', ('#',)),
('has_ended', ()), ('has_ended', ()),
('has_started', ()), ('has_started', ()),
('start_datetime_text', ('SHORT_DATE',)),
('start_datetime_text', ('DATE_TIME',)),
('end_datetime_text', ('SHORT_DATE',)),
('end_datetime_text', ('DATE_TIME',)),
('may_certify', ()), ('may_certify', ()),
] ]
for method_name, method_args in methods_to_test: for method_name, method_args in methods_to_test:
......
...@@ -11,7 +11,6 @@ from django.core.cache import cache ...@@ -11,7 +11,6 @@ from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
import httpretty import httpretty
import mock import mock
...@@ -19,6 +18,7 @@ from nose.plugins.attrib import attr ...@@ -19,6 +18,7 @@ from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from edx_oauth2_provider.tests.factories import ClientFactory from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
from pytz import utc
from lms.djangoapps.certificates.api import MODES from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
...@@ -718,8 +718,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -718,8 +718,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.course = CourseFactory() self.course = CourseFactory()
self.course.start = timezone.now() - datetime.timedelta(days=1) self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
self.course.end = timezone.now() + datetime.timedelta(days=1) self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
self.organization = factories.Organization() self.organization = factories.Organization()
...@@ -739,14 +739,15 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -739,14 +739,15 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
course_image_url=course_overview.course_image_url, course_image_url=course_overview.course_image_url,
course_key=unicode(self.course.id), # pylint: disable=no-member course_key=unicode(self.course.id), # pylint: disable=no-member
course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member
end_date=strftime_localized(self.course.end, 'SHORT_DATE'), end_date=self.course.end.replace(tzinfo=utc),
enrollment_open_date=strftime_localized(utils.DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'), enrollment_open_date=strftime_localized(utils.DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'),
is_course_ended=self.course.end < timezone.now(), is_course_ended=self.course.end < datetime.datetime.now(utc),
is_enrolled=False, is_enrolled=False,
is_enrollment_open=True, is_enrollment_open=True,
marketing_url=MARKETING_URL, marketing_url=MARKETING_URL,
start_date=strftime_localized(self.course.start, 'SHORT_DATE'), start_date=self.course.start.replace(tzinfo=utc),
upgrade_url=None, upgrade_url=None,
advertised_start=None
), ),
**kwargs **kwargs
) )
...@@ -825,9 +826,12 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -825,9 +826,12 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
) )
@ddt.unpack @ddt.unpack
def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open): def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
"""Verify that course enrollment status is reflected correctly.""" """
self.course.enrollment_start = timezone.now() - datetime.timedelta(days=start_offset) Verify that course enrollment status is reflected correctly.
self.course.enrollment_end = timezone.now() - datetime.timedelta(days=end_offset) """
self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset)
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
...@@ -843,7 +847,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -843,7 +847,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
Regression test for ECOM-4973. Regression test for ECOM-4973.
""" """
self.course.enrollment_end = timezone.now() - datetime.timedelta(days=1) self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
...@@ -873,7 +877,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -873,7 +877,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@ddt.data(-1, 0, 1) @ddt.data(-1, 0, 1)
def test_course_course_ended(self, days_offset): def test_course_course_ended(self, days_offset):
self.course.end = timezone.now() + datetime.timedelta(days=days_offset) self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
......
...@@ -6,10 +6,9 @@ from urlparse import urljoin ...@@ -6,10 +6,9 @@ from urlparse import urljoin
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
import pytz from pytz import utc
from course_modes.models import CourseMode from course_modes.models import CourseMode
from lms.djangoapps.certificates import api as certificate_api from lms.djangoapps.certificates import api as certificate_api
...@@ -30,7 +29,7 @@ from util.organizations_helpers import get_organization_by_short_name ...@@ -30,7 +29,7 @@ from util.organizations_helpers import get_organization_by_short_name
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# The datetime module's strftime() methods require a year >= 1900. # The datetime module's strftime() methods require a year >= 1900.
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=pytz.UTC) DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
def get_programs(user, program_id=None): def get_programs(user, program_id=None):
...@@ -383,27 +382,30 @@ class ProgramDataExtender(object): ...@@ -383,27 +382,30 @@ class ProgramDataExtender(object):
run_mode['course_url'] = reverse('course_root', args=[self.course_key]) run_mode['course_url'] = reverse('course_root', args=[self.course_key])
def _attach_run_mode_end_date(self, run_mode): def _attach_run_mode_end_date(self, run_mode):
run_mode['end_date'] = self.course_overview.end_datetime_text() run_mode['end_date'] = self.course_overview.end
def _attach_run_mode_enrollment_open_date(self, run_mode): def _attach_run_mode_enrollment_open_date(self, run_mode):
run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE') run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE')
def _attach_run_mode_is_course_ended(self, run_mode): def _attach_run_mode_is_course_ended(self, run_mode):
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC) end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc)
run_mode['is_course_ended'] = end_date < timezone.now() run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc)
def _attach_run_mode_is_enrolled(self, run_mode): def _attach_run_mode_is_enrolled(self, run_mode):
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key) run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key)
def _attach_run_mode_is_enrollment_open(self, run_mode): def _attach_run_mode_is_enrollment_open(self, run_mode):
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC) enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc)
run_mode['is_enrollment_open'] = self.enrollment_start <= timezone.now() < enrollment_end run_mode['is_enrollment_open'] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
def _attach_run_mode_marketing_url(self, run_mode): def _attach_run_mode_marketing_url(self, run_mode):
run_mode['marketing_url'] = get_run_marketing_url(self.course_key, self.user) run_mode['marketing_url'] = get_run_marketing_url(self.course_key, self.user)
def _attach_run_mode_start_date(self, run_mode): def _attach_run_mode_start_date(self, run_mode):
run_mode['start_date'] = self.course_overview.start_datetime_text() run_mode['start_date'] = self.course_overview.start
def _attach_run_mode_advertised_start(self, run_mode):
run_mode['advertised_start'] = self.course_overview.advertised_start
def _attach_run_mode_upgrade_url(self, run_mode): def _attach_run_mode_upgrade_url(self, run_mode):
required_mode_slug = run_mode['mode_slug'] required_mode_slug = run_mode['mode_slug']
......
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