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
#from datetime import date
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 statsd import statsd
......@@ -78,10 +78,7 @@ def index(request, extra_context={}, user=None):
domain = request.META.get('HTTP_HOST')
courses = get_courses(None, domain=domain)
# 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)
courses = sort_by_announcement(courses)
# Get the 3 most recent news
top_news = _get_news(top=3)
......
import logging
from math import exp, erf
from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
......@@ -183,35 +184,66 @@ class CourseDescriptor(SequenceDescriptor):
@property
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)
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):
return flag.lower() in ['true', 'yes', 'y']
else:
return bool(flag)
@property
def days_until_start(self):
def convert_to_datetime(timestamp):
def sorting_score(self):
"""
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))
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
advertised_start = self.metadata.get('advertised_start', None)
if advertised_start:
try:
start_date = datetime.strptime(advertised_start,
"%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
announcement = get_date('announcement')
start = get_date('advertised_start') or to_datetime(self.start)
now = to_datetime(time.gmtime())
return announcement, start, now
@lazyproperty
def grading_context(self):
......
import unittest
from time import strptime, gmtime
from time import strptime
from fs.memoryfs import MemoryFS
from mock import Mock, patch
......@@ -39,52 +39,81 @@ class DummySystem(ImportSystem):
class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses"""
@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"""
system = DummySystem(load_error_modules)
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
system = DummySystem(load_error_modules=True)
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 = '''
<course org="{org}" course="{course}"
graceperiod="1 day" url_name="test"
start="{start}"
{announcement}
{is_new}>
<chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html>
</chapter>
</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)
@patch('xmodule.course_module.time.gmtime')
def test_non_started_yet(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):
def test_sorting_score(self, gmtime_mock):
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')
def test_is_new_set(self, gmtime_mock):
def test_is_new(self, gmtime_mock):
gmtime_mock.return_value = NOW
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
assert(descriptor.is_new == True)
assert(descriptor.days_until_start < 0)
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
assert(descriptor.is_new == False)
assert(descriptor.days_until_start > 0)
assert(descriptor.is_new is False)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
assert(descriptor.is_new == True)
assert(descriptor.days_until_start > 0)
assert(descriptor.is_new is True)
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):
path = course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
def find_file(fs, dirs, filename):
"""
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):
return filepath
raise ResourceNotFoundError("Could not find {0}".format(filename))
def get_course_about_section(course, section_key):
"""
This returns the snippet of html to be rendered on the course about page,
......@@ -234,4 +236,18 @@ def get_courses(user, domain=None):
courses = [c for c in courses if has_access(user, c, 'see_exists')]
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
......@@ -17,7 +17,8 @@ from django.views.decorators.cache import cache_control
from courseware import grades
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
from courseware.models import StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module
......@@ -67,11 +68,8 @@ def courses(request):
'''
Render "find courses" page. The course selection work is done in courseware.courses.
'''
courses = get_courses(request.user, domain=request.META.get('HTTP_HOST'))
# 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)
courses = get_courses(request.user, request.META.get('HTTP_HOST'))
courses = sort_by_announcement(courses)
return render_to_response("courseware/courses.html", {'courses': courses})
......@@ -438,10 +436,7 @@ def university_profile(request, org_id):
# Only grab courses for this org...
courses = get_courses_by_university(request.user,
domain=request.META.get('HTTP_HOST'))[org_id]
# 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)
courses = sort_by_announcement(courses)
context = dict(courses=courses, org_id=org_id)
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