diff --git a/common/djangoapps/util/date_utils.py b/common/djangoapps/util/date_utils.py index e9c4f905d3..d4bfff685d 100644 --- a/common/djangoapps/util/date_utils.py +++ b/common/djangoapps/util/date_utils.py @@ -98,6 +98,7 @@ DEFAULT_SHORT_DATE_FORMAT = "%b %d, %Y" DEFAULT_LONG_DATE_FORMAT = "%A, %B %d, %Y" DEFAULT_TIME_FORMAT = "%I:%M:%S %p" DEFAULT_DATE_TIME_FORMAT = "%b %d, %Y at %H:%M" +DEFAULT_DAY_AND_TIME_FORMAT = "%A at %-I%P" def strftime_localized(dtime, format): # pylint: disable=redefined-builtin @@ -147,6 +148,8 @@ def strftime_localized(dtime, format): # pylint: disable=redefined-builtin format = ugettext("DATE_TIME_FORMAT") if format == "DATE_TIME_FORMAT": format = DEFAULT_DATE_TIME_FORMAT + elif format == "DAY_AND_TIME": + format = DEFAULT_DAY_AND_TIME_FORMAT elif format == "TIME": format = "%X" @@ -204,7 +207,7 @@ def strftime_localized(dtime, format): # pylint: disable=redefined-builtin return part - formatted_date = re.sub(r"%.|%", process_percent_code, format) + formatted_date = re.sub(r"%-.|%.|%", process_percent_code, format) return formatted_date diff --git a/common/djangoapps/util/tests/test_date_utils.py b/common/djangoapps/util/tests/test_date_utils.py index de3308658e..05b2e56e4f 100644 --- a/common/djangoapps/util/tests/test_date_utils.py +++ b/common/djangoapps/util/tests/test_date_utils.py @@ -146,6 +146,7 @@ class StrftimeLocalizedTest(unittest.TestCase): (u'%Y년 %m월 %d일', u"2013년 02월 14일"), ("%a, %b %d, %Y", "Thu, Feb 14, 2013"), ("%I:%M:%S %p", "04:41:17 PM"), + ("%A at %-I%P", "Thursday at 4pm"), ) def test_usual_strftime_behavior(self, (fmt, expected)): dtime = datetime(2013, 02, 14, 16, 41, 17) @@ -157,6 +158,7 @@ class StrftimeLocalizedTest(unittest.TestCase): ("SHORT_DATE", "Feb 14, 2013"), ("LONG_DATE", "Thursday, February 14, 2013"), ("TIME", "04:41:17 PM"), + ("DAY_AND_TIME", "Thursday at 4pm"), ("%x %X!", "Feb 14, 2013 04:41:17 PM!"), ) def test_shortcuts(self, (fmt, expected)): diff --git a/common/lib/xmodule/xmodule/course_metadata_utils.py b/common/lib/xmodule/xmodule/course_metadata_utils.py index 832be14d1a..534fe157d9 100644 --- a/common/lib/xmodule/xmodule/course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/course_metadata_utils.py @@ -6,6 +6,7 @@ allows us to share code between the CourseDescriptor and CourseOverview classes, which both need these type of functions. """ from datetime import datetime +from datetime import timedelta from base64 import b32encode from django.utils.timezone import UTC @@ -105,6 +106,18 @@ def has_course_ended(end_date): return datetime.now(UTC()) > end_date if end_date is not None else False +def course_starts_within(start_date, look_ahead_days): + """ + Given a course's start datetime and look ahead days, returns True if + course's start date falls within look ahead days otherwise False + + Arguments: + start_date (datetime): The start datetime of the course in question. + look_ahead_days (int): number of days to see in future for course start date. + """ + return datetime.now(UTC()) + timedelta(days=look_ahead_days) > start_date + + def course_start_date_is_default(start, advertised_start): """ Returns whether a course's start date hasn't yet been set. @@ -132,7 +145,7 @@ def _datetime_to_string(date_time, format_string, strftime_localized): # TODO: Is manually appending UTC really the right thing to do here? What if date_time isn't UTC? result = strftime_localized(date_time, format_string) return ( - result + u" UTC" if format_string in ['DATE_TIME', 'TIME'] + result + u" UTC" if format_string in ['DATE_TIME', 'TIME', 'DAY_AND_TIME'] else result ) diff --git a/common/test/acceptance/fixtures/course.py b/common/test/acceptance/fixtures/course.py index 48c5218be8..90521be680 100644 --- a/common/test/acceptance/fixtures/course.py +++ b/common/test/acceptance/fixtures/course.py @@ -152,6 +152,21 @@ class CourseFixture(XBlockContainerFixture): """ return "".format(**self._course_dict) + def add_course_details(self, course_details): + """ + Add course details to dict of course details to be updated when configure_course or install is called. + + Arguments: + Dictionary containing key value pairs for course updates, + e.g. {'start_date': datetime.now() } + """ + if 'start_date' in course_details: + course_details['start_date'] = course_details['start_date'].isoformat() + if 'end_date' in course_details: + course_details['end_date'] = course_details['end_date'].isoformat() + + self._course_details.update(course_details) + def add_update(self, update): """ Add an update to the course. `update` should be a `CourseUpdateDesc`. @@ -201,6 +216,12 @@ class CourseFixture(XBlockContainerFixture): return self + def configure_course(self): + """ + Configure Course Settings, take new course settings from self._course_details dict object + """ + self._configure_course() + @property def _course_location(self): """ diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index 0e96e013a7..5a13629620 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -2,7 +2,6 @@ """ Student dashboard page. """ - from bok_choy.page_object import PageObject from . import BASE_URL @@ -157,6 +156,18 @@ class DashboardPage(PageObject): """ Retrieves the specified social sharing widget by its classification """ return self.q(css='a.action-{}'.format(widget_name)) + def get_courses(self): + """ + Get all courses shown in the dashboard + """ + return self.q(css='ul.listing-courses .course-item') + + def get_course_date(self): + """ + Get course date of the first course from dashboard + """ + return self.q(css='ul.listing-courses .course-item .info-date-block').first.text[0] + def click_username_dropdown(self): """ Click username dropdown. diff --git a/common/test/acceptance/tests/lms/test_lms_dashboard.py b/common/test/acceptance/tests/lms/test_lms_dashboard.py index b2923cac99..8373360434 100644 --- a/common/test/acceptance/tests/lms/test_lms_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_dashboard.py @@ -2,11 +2,16 @@ """ End-to-end tests for the main LMS Dashboard (aka, Student Dashboard). """ +import datetime + from ..helpers import UniqueCourseTest from ...fixtures.course import CourseFixture from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.dashboard import DashboardPage +DEFAULT_SHORT_DATE_FORMAT = "%b %d, %Y" +DEFAULT_DAY_AND_TIME_FORMAT = "%A at %-I%P" + class BaseLmsDashboardTest(UniqueCourseTest): """ Base test suite for the LMS Student Dashboard """ @@ -51,6 +56,12 @@ class BaseLmsDashboardTest(UniqueCourseTest): class LmsDashboardPageTest(BaseLmsDashboardTest): """ Test suite for the LMS Student Dashboard page """ + def setUp(self): + super(LmsDashboardPageTest, self).setUp() + + # now datetime for usage in tests + self.now = datetime.datetime.now() + def test_dashboard_course_listings(self): """ Perform a general validation of the course listings section @@ -81,3 +92,128 @@ class LmsDashboardPageTest(BaseLmsDashboardTest): self.assertEqual(facebook_widget.attrs('target')[0], '_blank') self.assertIn(facebook_url, facebook_widget.attrs('href')[0]) self.assertIn(facebook_url, facebook_widget.attrs('onclick')[0]) + + def test_ended_course_date(self): + """ + Scenario: + Course Date should have the format 'Ended - Sep 23, 2015' + if the course on student dashboard has ended. + + As a Student, + Given that I have enrolled to a course + And the course has ended in the past + When I visit dashboard page + Then the course date should have the following format "Ended - %b %d, %Y" e.g. "Ended - Sep 23, 2015" + """ + course_start_date = datetime.datetime(1970, 1, 1) + course_end_date = self.now - datetime.timedelta(days=90) + + self.course_fixture.add_course_details({'start_date': course_start_date, + 'end_date': course_end_date}) + self.course_fixture.configure_course() + + end_date = course_end_date.strftime(DEFAULT_SHORT_DATE_FORMAT) + expected_course_date = "Ended - {end_date}".format(end_date=end_date) + + # reload the page for changes to course date changes to appear in dashboard + self.dashboard_page.visit() + + course_date = self.dashboard_page.get_course_date() + + # Test that proper course date with 'ended' message is displayed if a course has already ended + self.assertEqual(course_date, expected_course_date) + + def test_running_course_date(self): + """ + Scenario: + Course Date should have the format 'Started - Sep 23, 2015' + if the course on student dashboard is running. + + As a Student, + Given that I have enrolled to a course + And the course has started + And the course is in progress + When I visit dashboard page + Then the course date should have the following format "Started - %b %d, %Y" e.g. "Started - Sep 23, 2015" + """ + course_start_date = datetime.datetime(1970, 1, 1) + course_end_date = self.now + datetime.timedelta(days=90) + + self.course_fixture.add_course_details({'start_date': course_start_date, + 'end_date': course_end_date}) + self.course_fixture.configure_course() + + start_date = course_start_date.strftime(DEFAULT_SHORT_DATE_FORMAT) + expected_course_date = "Started - {start_date}".format(start_date=start_date) + + # reload the page for changes to course date changes to appear in dashboard + self.dashboard_page.visit() + + course_date = self.dashboard_page.get_course_date() + + # Test that proper course date with 'started' message is displayed if a course is in running state + self.assertEqual(course_date, expected_course_date) + + def test_future_course_date(self): + """ + Scenario: + Course Date should have the format 'Starts - Sep 23, 2015' + if the course on student dashboard starts in future. + + As a Student, + Given that I have enrolled to a course + And the course starts in future + And the course does not start within 5 days + When I visit dashboard page + Then the course date should have the following format "Starts - %b %d, %Y" e.g. "Starts - Sep 23, 2015" + """ + course_start_date = self.now + datetime.timedelta(days=30) + course_end_date = self.now + datetime.timedelta(days=365) + + self.course_fixture.add_course_details({'start_date': course_start_date, + 'end_date': course_end_date}) + self.course_fixture.configure_course() + + start_date = course_start_date.strftime(DEFAULT_SHORT_DATE_FORMAT) + expected_course_date = "Starts - {start_date}".format(start_date=start_date) + + # reload the page for changes to course date changes to appear in dashboard + self.dashboard_page.visit() + + course_date = self.dashboard_page.get_course_date() + + # Test that proper course date with 'starts' message is displayed if a course is about to start in future, + # and course does not start within 5 days + self.assertEqual(course_date, expected_course_date) + + def test_near_future_course_date(self): + """ + Scenario: + Course Date should have the format 'Starts - Wednesday at 5am UTC' + if the course on student dashboard starts within 5 days. + + As a Student, + Given that I have enrolled to a course + And the course starts within 5 days + When I visit dashboard page + Then the course date should have the following format "Starts - %A at %-I%P UTC" + e.g. "Starts - Wednesday at 5am UTC" + """ + course_start_date = self.now + datetime.timedelta(days=2) + course_end_date = self.now + datetime.timedelta(days=365) + + self.course_fixture.add_course_details({'start_date': course_start_date, + 'end_date': course_end_date}) + self.course_fixture.configure_course() + + start_date = course_start_date.strftime(DEFAULT_DAY_AND_TIME_FORMAT) + expected_course_date = "Starts - {start_date} UTC".format(start_date=start_date) + + # reload the page for changes to course date changes to appear in dashboard + self.dashboard_page.visit() + + course_date = self.dashboard_page.get_course_date() + + # Test that proper course date with 'starts' message is displayed if a course is about to start in future, + # and course starts within 5 days + self.assertEqual(course_date, expected_course_date) diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index fac004c4d9..84331a8f93 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -94,6 +94,8 @@ from student.helpers import ( ${_("Started - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE"))} % elif course_overview.start_date_is_still_default: # Course start date TBD ${_("Coming Soon")} + % elif course_overview.starts_within(days=5): # hasn't started yet + ${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("DAY_AND_TIME"))} % else: # hasn't started yet ${_("Starts - {start_date}").format(start_date=course_overview.start_datetime_text("SHORT_DATE"))} % endif diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 1093f499d1..f5ec132f99 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -290,6 +290,13 @@ class CourseOverview(TimeStampedModel): """ return course_metadata_utils.has_course_ended(self.end) + def starts_within(self, days): + """ + Returns True if the course starts with-in given number of days otherwise returns False. + """ + + return course_metadata_utils.course_starts_within(self.start, days) + def start_datetime_text(self, format_string="SHORT_DATE"): """ Returns the desired text corresponding the course's start date and