Commit 2291483c by Diana Huang

Merge branch 'master' into diana/open-ended-ui-updates

parents f5009584 44e7d6b2
...@@ -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)
...@@ -339,19 +339,6 @@ class ImportTestCase(unittest.TestCase): ...@@ -339,19 +339,6 @@ class ImportTestCase(unittest.TestCase):
self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml) self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml)
def test_selfassessment_import(self):
'''
Check to see if definition_from_xml in self_assessment_module.py
works properly. Pulls data from the self_assessment directory in the test data directory.
'''
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['self_assessment'])
sa_id = "edX/sa_test/2012_Fall"
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
sa_sample = modulestore.get_instance(sa_id, location)
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
self.assertEqual(sa_sample.metadata['attempts'], '10')
def test_graphicslidertool_import(self): def test_graphicslidertool_import(self):
''' '''
......
...@@ -4,6 +4,7 @@ import unittest ...@@ -4,6 +4,7 @@ import unittest
from xmodule.self_assessment_module import SelfAssessmentModule from xmodule.self_assessment_module import SelfAssessmentModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree
from . import test_system from . import test_system
...@@ -26,22 +27,37 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -26,22 +27,37 @@ class SelfAssessmentTest(unittest.TestCase):
state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"], state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"],
'scores': [0, 1], 'scores': [0, 1],
'hints': ['o hai'], 'hints': ['o hai'],
'state': SelfAssessmentModule.ASSESSING, 'state': SelfAssessmentModule.INITIAL,
'attempts': 2}) 'attempts': 2})
rubric = '''<rubric><rubric>
<category>
<description>Response Quality</description>
<option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
</category>
</rubric></rubric>'''
prompt = etree.XML("<prompt>Text</prompt>")
static_data = {
'max_attempts': 10,
'rubric': etree.XML(rubric),
'prompt': prompt,
'max_score': 1
}
module = SelfAssessmentModule(test_system, self.location, module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor, self.definition, self.descriptor,
state, {}, metadata=self.metadata) static_data, state, metadata=self.metadata)
self.assertEqual(module.get_score()['score'], 0) self.assertEqual(module.get_score()['score'], 0)
self.assertTrue('answer 3' in module.get_html())
self.assertFalse('answer 2' in module.get_html())
module.save_assessment({'assessment': '0'}) module.save_answer({'student_answer': "I am an answer"}, test_system)
self.assertEqual(module.state, module.REQUEST_HINT) self.assertEqual(module.state, module.ASSESSING)
module.save_hint({'hint': 'hint for ans 3'}) module.save_assessment({'assessment': '0'}, test_system)
self.assertEqual(module.state, module.POST_ASSESSMENT)
module.save_hint({'hint': 'this is a hint'}, test_system)
self.assertEqual(module.state, module.DONE) self.assertEqual(module.state, module.DONE)
d = module.reset({}) d = module.reset({})
...@@ -49,6 +65,6 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -49,6 +65,6 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(module.state, module.INITIAL) self.assertEqual(module.state, module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state # if we now assess as right, skip the REQUEST_HINT state
module.save_answer({'student_answer': 'answer 4'}) module.save_answer({'student_answer': 'answer 4'}, test_system)
module.save_assessment({'assessment': '1'}) module.save_assessment({'assessment': '1'}, test_system)
self.assertEqual(module.state, module.DONE) self.assertEqual(module.state, module.DONE)
...@@ -338,6 +338,10 @@ def course_beta_test_group_name(location): ...@@ -338,6 +338,10 @@ def course_beta_test_group_name(location):
""" """
return 'beta_testers_{0}'.format(Location(location).course) return 'beta_testers_{0}'.format(Location(location).course)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
def _course_instructor_group_name(location): def _course_instructor_group_name(location):
""" """
......
...@@ -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()
......
...@@ -111,6 +111,7 @@ def instructor_dashboard(request, course_id): ...@@ -111,6 +111,7 @@ def instructor_dashboard(request, course_id):
except Group.DoesNotExist: except Group.DoesNotExist:
group = Group(name=grpname) # create the group group = Group(name=grpname) # create the group
group.save() group.save()
return group
def get_beta_group(course): def get_beta_group(course):
""" """
......
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