Commit ef69b738 by Carlos Andrés Rocha

Sort courses announcement date

If there is no announcement date (in the policy metadata) then use a
heuristic to sort them.

If there is an announcement date, the use it in the heuristic to
determine if the course is new or not.
parent 7ac36d2f
...@@ -42,7 +42,7 @@ from xmodule.modulestore.django import modulestore ...@@ -42,7 +42,7 @@ from xmodule.modulestore.django import modulestore
#from datetime import date #from datetime import date
from collections import namedtuple from collections import namedtuple
from courseware.courses import get_courses from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access from courseware.access import has_access
from statsd import statsd from statsd import statsd
...@@ -78,10 +78,7 @@ def index(request, extra_context={}, user=None): ...@@ -78,10 +78,7 @@ def index(request, extra_context={}, user=None):
domain = request.META.get('HTTP_HOST') domain = request.META.get('HTTP_HOST')
courses = get_courses(None, domain=domain) courses = get_courses(None, domain=domain)
courses = sort_by_announcement(courses)
# Sort courses by how far are they from they start day
key = lambda course: course.days_until_start
courses = sorted(courses, key=key, reverse=True)
# Get the 3 most recent news # Get the 3 most recent news
top_news = _get_news(top=3) top_news = _get_news(top=3)
......
import logging import logging
from math import exp, erf
from lxml import etree from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests import requests
...@@ -183,35 +184,66 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -183,35 +184,66 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def is_new(self): def is_new(self):
# The course is "new" if either if the metadata flag is_new is """
# true or if the course has not started yet Returns if the course has been flagged as new in the metadata. If
there is no flag, return a heuristic value considering the
announcement and the start dates.
"""
flag = self.metadata.get('is_new', None) flag = self.metadata.get('is_new', None)
if flag is None: if flag is None:
return self.days_until_start > 1 # Use a heuristic if the course has not been flagged
announcement, start, now = self._sorting_dates()
if announcement and (now - announcement).days < 30:
# The course has been announced for less that month
return True
elif (now - start).days < 1:
# The course has not started yet
return True
else:
return False
elif isinstance(flag, basestring): elif isinstance(flag, basestring):
return flag.lower() in ['true', 'yes', 'y'] return flag.lower() in ['true', 'yes', 'y']
else: else:
return bool(flag) return bool(flag)
@property @property
def days_until_start(self): def sorting_score(self):
def convert_to_datetime(timestamp): """
Returns a number 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.
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
def to_datetime(timestamp):
return datetime.fromtimestamp(time.mktime(timestamp)) return datetime.fromtimestamp(time.mktime(timestamp))
start_date = convert_to_datetime(self.start) def get_date(field):
timetuple = self._try_parse_time(field)
return to_datetime(timetuple) if timetuple else None
# Try to use course advertised date if we can parse it announcement = get_date('announcement')
advertised_start = self.metadata.get('advertised_start', None) start = get_date('advertised_start') or to_datetime(self.start)
if advertised_start: now = to_datetime(time.gmtime())
try:
start_date = datetime.strptime(advertised_start, return announcement, start, now
"%Y-%m-%dT%H:%M")
except ValueError:
pass # Invalid date, keep using 'start''
now = convert_to_datetime(time.gmtime())
days_until_start = (start_date - now).days
return days_until_start
@lazyproperty @lazyproperty
def grading_context(self): def grading_context(self):
......
import unittest import unittest
from time import strptime, gmtime from time import strptime
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
...@@ -39,52 +39,81 @@ class DummySystem(ImportSystem): ...@@ -39,52 +39,81 @@ class DummySystem(ImportSystem):
class IsNewCourseTestCase(unittest.TestCase): class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses""" """Make sure the property is_new works on courses"""
@staticmethod @staticmethod
def get_dummy_course(start, is_new=None, load_error_modules=True): def get_dummy_course(start, announcement=None, is_new=None):
"""Get a dummy course""" """Get a dummy course"""
system = DummySystem(load_error_modules) system = DummySystem(load_error_modules=True)
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
def to_attrb(n, v):
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
is_new = to_attrb('is_new', is_new)
announcement = to_attrb('announcement', announcement)
start_xml = ''' start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
graceperiod="1 day" url_name="test" graceperiod="1 day" url_name="test"
start="{start}" start="{start}"
{announcement}
{is_new}> {is_new}>
<chapter url="hi" url_name="ch" display_name="CH"> <chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html> <html url_name="h" display_name="H">Two houses, ...</html>
</chapter> </chapter>
</course> </course>
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new) '''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
announcement=announcement)
return system.process_xml(start_xml) return system.process_xml(start_xml)
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.time.gmtime')
def test_non_started_yet(self, gmtime_mock): def test_sorting_score(self, gmtime_mock):
descriptor = self.get_dummy_course(start='2013-01-05T12:00')
gmtime_mock.return_value = NOW
assert(descriptor.is_new == True)
assert(descriptor.days_until_start == 4)
@patch('xmodule.course_module.time.gmtime')
def test_already_started(self, gmtime_mock):
gmtime_mock.return_value = NOW gmtime_mock.return_value = NOW
dates = [('2012-10-01T12:00', '2012-09-01T12:00'), # 0
('2012-12-01T12:00', '2012-11-01T12:00'), # 1
('2013-02-01T12:00', '2012-12-01T12:00'), # 2
('2013-02-01T12:00', '2012-11-10T12:00'), # 3
('2013-02-01T12:00', None), # 4
('2013-03-01T12:00', None), # 5
('2013-04-01T12:00', None), # 6
('2012-11-01T12:00', None), # 7
('2012-09-01T12:00', None), # 8
('1990-01-01T12:00', None), # 9
('2013-01-02T12:00', None), # 10
('2013-01-10T12:00', '2012-12-31T12:00'), # 11
('2013-01-10T12:00', '2013-01-01T12:00'), # 12
]
data = []
for i, d in enumerate(dates):
descriptor = self.get_dummy_course(start=d[0], announcement=d[1])
score = descriptor.sorting_score
data.append((score, i))
result = [d[1] for d in sorted(data)]
assert(result == [12, 11, 2, 3, 1, 0, 6, 5, 4, 10, 7, 8, 9])
descriptor = self.get_dummy_course(start='2012-12-02T12:00')
assert(descriptor.is_new == False)
assert(descriptor.days_until_start < 0)
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.time.gmtime')
def test_is_new_set(self, gmtime_mock): def test_is_new(self, gmtime_mock):
gmtime_mock.return_value = NOW gmtime_mock.return_value = NOW
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
assert(descriptor.is_new == True) assert(descriptor.is_new is True)
assert(descriptor.days_until_start < 0)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False) descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
assert(descriptor.is_new == False) assert(descriptor.is_new is False)
assert(descriptor.days_until_start > 0)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True) descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
assert(descriptor.is_new == True) assert(descriptor.is_new is True)
assert(descriptor.days_until_start > 0)
descriptor = self.get_dummy_course(start='2013-01-15T12:00')
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2013-03-00T12:00')
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2012-10-15T12:00')
assert(descriptor.is_new is False)
descriptor = self.get_dummy_course(start='2012-12-31T12:00')
assert(descriptor.is_new is True)
...@@ -64,6 +64,7 @@ def course_image_url(course): ...@@ -64,6 +64,7 @@ def course_image_url(course):
path = course.metadata['data_dir'] + "/images/course_image.jpg" path = course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path) return try_staticfiles_lookup(path)
def find_file(fs, dirs, filename): def find_file(fs, dirs, filename):
""" """
Looks for a filename in a list of dirs on a filesystem, in the specified order. Looks for a filename in a list of dirs on a filesystem, in the specified order.
...@@ -80,6 +81,7 @@ def find_file(fs, dirs, filename): ...@@ -80,6 +81,7 @@ def find_file(fs, dirs, filename):
return filepath return filepath
raise ResourceNotFoundError("Could not find {0}".format(filename)) raise ResourceNotFoundError("Could not find {0}".format(filename))
def get_course_about_section(course, section_key): def get_course_about_section(course, section_key):
""" """
This returns the snippet of html to be rendered on the course about page, This returns the snippet of html to be rendered on the course about page,
...@@ -234,4 +236,18 @@ def get_courses(user, domain=None): ...@@ -234,4 +236,18 @@ def get_courses(user, domain=None):
courses = [c for c in courses if has_access(user, c, 'see_exists')] courses = [c for c in courses if has_access(user, c, 'see_exists')]
courses = sorted(courses, key=lambda course:course.number) courses = sorted(courses, key=lambda course:course.number)
return courses
def sort_by_announcement(courses):
"""
Sorts a list of courses by their announcement date. If the date is
not available, sort them by their start date.
"""
# Sort courses by how far are they from they start day
key = lambda course: course.sorting_score
courses = sorted(courses, key=key)
return courses return courses
...@@ -17,7 +17,8 @@ from django.views.decorators.cache import cache_control ...@@ -17,7 +17,8 @@ from django.views.decorators.cache import cache_control
from courseware import grades from courseware import grades
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university) from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement)
import courseware.tabs as tabs import courseware.tabs as tabs
from courseware.models import StudentModuleCache from courseware.models import StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module from module_render import toc_for_course, get_module, get_instance_module
...@@ -67,11 +68,8 @@ def courses(request): ...@@ -67,11 +68,8 @@ def courses(request):
''' '''
Render "find courses" page. The course selection work is done in courseware.courses. Render "find courses" page. The course selection work is done in courseware.courses.
''' '''
courses = get_courses(request.user, domain=request.META.get('HTTP_HOST')) courses = get_courses(request.user, request.META.get('HTTP_HOST'))
courses = sort_by_announcement(courses)
# Sort courses by how far are they from they start day
key = lambda course: course.days_until_start
courses = sorted(courses, key=key, reverse=True)
return render_to_response("courseware/courses.html", {'courses': courses}) return render_to_response("courseware/courses.html", {'courses': courses})
...@@ -438,10 +436,7 @@ def university_profile(request, org_id): ...@@ -438,10 +436,7 @@ def university_profile(request, org_id):
# Only grab courses for this org... # Only grab courses for this org...
courses = get_courses_by_university(request.user, courses = get_courses_by_university(request.user,
domain=request.META.get('HTTP_HOST'))[org_id] domain=request.META.get('HTTP_HOST'))[org_id]
courses = sort_by_announcement(courses)
# Sort courses by how far are they from they start day
key = lambda course: course.days_until_start
courses = sorted(courses, key=key, reverse=True)
context = dict(courses=courses, org_id=org_id) context = dict(courses=courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower() template_file = "university_profile/{0}.html".format(org_id).lower()
......
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