diff --git a/common/lib/xmodule/xmodule/course_metadata_utils.py b/common/lib/xmodule/xmodule/course_metadata_utils.py index 534fe157d9..500ef8485f 100644 --- a/common/lib/xmodule/xmodule/course_metadata_utils.py +++ b/common/lib/xmodule/xmodule/course_metadata_utils.py @@ -5,9 +5,10 @@ This is a place to put simple functions that operate on course metadata. It 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 datetime import datetime, timedelta +import dateutil.parser +from math import exp from django.utils.timezone import UTC @@ -222,3 +223,43 @@ def may_certify_for_course(certificates_display_behavior, certificates_show_befo or certificates_show_before_end ) return show_early or has_ended + + +def sorting_score(start, advertised_start, announcement): + """ + Returns a tuple that can be used to sort the courses according + to how "new" they are. The "newness" score is computed using a + heuristic that takes into account the announcement and + (advertised) start dates of the course if available. + + The lower the number the "newer" the course. + """ + # Make courses that have an announcement date have a lower + # score than courses than don't, older courses should have a + # higher score. + announcement, start, now = sorting_dates(start, advertised_start, announcement) + scale = 300.0 # about a year + if announcement: + days = (now - announcement).days + score = -exp(-days / scale) + else: + days = (now - start).days + score = exp(days / scale) + return score + + +def sorting_dates(start, advertised_start, announcement): + """ + Utility function to get datetime objects for dates used to + compute the is_new flag and the sorting_score. + """ + try: + start = dateutil.parser.parse(advertised_start) + if start.tzinfo is None: + start = start.replace(tzinfo=UTC()) + except (ValueError, AttributeError): + start = start + + now = datetime.now(UTC()) + + return announcement, start, now diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 31033b7649..6847599257 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -3,12 +3,10 @@ Django module container for classes and operations related to the "Course Module """ import logging from cStringIO import StringIO -from math import exp from lxml import etree from path import Path as path import requests from datetime import datetime -import dateutil.parser from lazy import lazy from xmodule import course_metadata_utils @@ -1264,7 +1262,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): flag = self.is_new if flag is None: # Use a heuristic if the course has not been flagged - announcement, start, now = self._sorting_dates() + announcement, start, now = course_metadata_utils.sorting_dates( + self.start, self.advertised_start, self.announcement + ) if announcement and (now - announcement).days < 30: # The course has been announced for less that month return True @@ -1284,41 +1284,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): Returns a tuple that can be used to sort the courses according the how "new" they are. The "newness" score is computed using a heuristic that takes into account the announcement and - (advertized) start dates of the course if available. + (advertised) start dates of the course if available. The lower the number the "newer" the course. """ - # Make courses that have an announcement date shave a lower - # score than courses than don't, older courses should have a - # higher score. - announcement, start, now = self._sorting_dates() - scale = 300.0 # about a year - if announcement: - days = (now - announcement).days - score = -exp(-days / scale) - else: - days = (now - start).days - score = exp(days / scale) - return score - - def _sorting_dates(self): - # utility function to get datetime objects for dates used to - # compute the is_new flag and the sorting_score - - announcement = self.announcement - if announcement is not None: - announcement = announcement - - try: - start = dateutil.parser.parse(self.advertised_start) - if start.tzinfo is None: - start = start.replace(tzinfo=UTC()) - except (ValueError, AttributeError): - start = self.start - - now = datetime.now(UTC()) - - return announcement, start, now + return course_metadata_utils.sorting_score(self.start, self.advertised_start, self.announcement) @lazy def grading_context(self): diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 23c0f330ed..ffd63fed5b 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -150,14 +150,14 @@ class IsNewCourseTestCase(unittest.TestCase): # Needed for test_is_newish datetime_patcher = patch.object( - xmodule.course_module, 'datetime', + xmodule.course_metadata_utils, 'datetime', Mock(wraps=datetime) ) mocked_datetime = datetime_patcher.start() mocked_datetime.now.return_value = NOW self.addCleanup(datetime_patcher.stop) - @patch('xmodule.course_module.datetime.now') + @patch('xmodule.course_metadata_utils.datetime.now') def test_sorting_score(self, gmtime_mock): gmtime_mock.return_value = NOW @@ -208,7 +208,7 @@ class IsNewCourseTestCase(unittest.TestCase): (xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False, 'January 2014'), ] - @patch('xmodule.course_module.datetime.now') + @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: @@ -216,7 +216,7 @@ class IsNewCourseTestCase(unittest.TestCase): print "Checking start=%s advertised=%s" % (s[0], s[1]) self.assertEqual(d.start_datetime_text(), s[2]) - @patch('xmodule.course_module.datetime.now') + @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: