Commit dd20226e by Diana Huang

Merge branch 'master' into feature/diana/rubric-input

Fix some merge issues involving rubric rendering

Conflicts:
	common/lib/xmodule/xmodule/combined_open_ended_rubric.py
	common/lib/xmodule/xmodule/open_ended_module.py
	common/lib/xmodule/xmodule/self_assessment_module.py
parents 18a3d5e7 44e7d6b2
...@@ -271,8 +271,9 @@ class TestCenterUserForm(ModelForm): ...@@ -271,8 +271,9 @@ class TestCenterUserForm(ModelForm):
new_user = self.save(commit=False) new_user = self.save(commit=False)
# create additional values here: # create additional values here:
new_user.user_updated_at = datetime.utcnow() new_user.user_updated_at = datetime.utcnow()
new_user.upload_status = ''
new_user.save() new_user.save()
log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.username)) log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.user.username))
# add validation: # add validation:
...@@ -533,6 +534,7 @@ class TestCenterRegistrationForm(ModelForm): ...@@ -533,6 +534,7 @@ class TestCenterRegistrationForm(ModelForm):
registration = self.save(commit=False) registration = self.save(commit=False)
# create additional values here: # create additional values here:
registration.user_updated_at = datetime.utcnow() registration.user_updated_at = datetime.utcnow()
registration.upload_status = ''
registration.save() registration.save()
log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code)) log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
......
...@@ -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)
...@@ -632,15 +629,18 @@ def begin_exam_registration(request, course_id): ...@@ -632,15 +629,18 @@ def begin_exam_registration(request, course_id):
user = request.user user = request.user
try: try:
course = (course_from_id(course_id)) course = course_from_id(course_id)
except ItemNotFoundError: except ItemNotFoundError:
# TODO: do more than just log!! The rest will fail, so we should fail right now. log.error("User {0} enrolled in non-existent course {1}".format(user.username, course_id))
log.error("User {0} enrolled in non-existent course {1}" raise Http404
.format(user.username, course_id))
# get the exam to be registered for: # get the exam to be registered for:
# (For now, we just assume there is one at most.) # (For now, we just assume there is one at most.)
# if there is no exam now (because someone bookmarked this stupid page),
# then return a 404:
exam_info = course.current_test_center_exam exam_info = course.current_test_center_exam
if exam_info is None:
raise Http404
# determine if the user is registered for this course: # determine if the user is registered for this course:
registration = exam_registration_info(user, course) registration = exam_registration_info(user, course)
...@@ -675,11 +675,18 @@ def create_exam_registration(request, post_override=None): ...@@ -675,11 +675,18 @@ def create_exam_registration(request, post_override=None):
username = post_vars['username'] username = post_vars['username']
user = User.objects.get(username=username) user = User.objects.get(username=username)
course_id = post_vars['course_id'] course_id = post_vars['course_id']
course = (course_from_id(course_id)) # assume it will be found.... course = course_from_id(course_id) # assume it will be found....
# make sure that any demographic data values received from the page have been stripped.
# Whitespace is not an acceptable response for any of these values
demographic_data = {}
for fieldname in TestCenterUser.user_provided_fields():
if fieldname in post_vars:
demographic_data[fieldname] = (post_vars[fieldname]).strip()
try: try:
testcenter_user = TestCenterUser.objects.get(user=user) testcenter_user = TestCenterUser.objects.get(user=user)
needs_updating = testcenter_user.needs_update(post_vars) needs_updating = testcenter_user.needs_update(demographic_data)
log.info("User {0} enrolled in course {1} {2}updating demographic info for exam registration".format(user.username, course_id, "" if needs_updating else "not ")) log.info("User {0} enrolled in course {1} {2}updating demographic info for exam registration".format(user.username, course_id, "" if needs_updating else "not "))
except TestCenterUser.DoesNotExist: except TestCenterUser.DoesNotExist:
# do additional initialization here: # do additional initialization here:
...@@ -691,7 +698,7 @@ def create_exam_registration(request, post_override=None): ...@@ -691,7 +698,7 @@ def create_exam_registration(request, post_override=None):
if needs_updating: if needs_updating:
# first perform validation on the user information # first perform validation on the user information
# using a Django Form. # using a Django Form.
form = TestCenterUserForm(instance=testcenter_user, data=post_vars) form = TestCenterUserForm(instance=testcenter_user, data=demographic_data)
if form.is_valid(): if form.is_valid():
form.update_and_save() form.update_and_save()
else: else:
......
...@@ -24,8 +24,6 @@ import open_ended_module ...@@ -24,8 +24,6 @@ import open_ended_module
from combined_open_ended_rubric import CombinedOpenEndedRubric from combined_open_ended_rubric import CombinedOpenEndedRubric
from .stringify import stringify_children from .stringify import stringify_children
from mitxmako.shortcuts import render_to_string
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
# Set the default number of max attempts. Should be 1 for production # Set the default number of max attempts. Should be 1 for production
...@@ -142,7 +140,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -142,7 +140,7 @@ class CombinedOpenEndedModule(XModule):
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
rubric_renderer = CombinedOpenEndedRubric(True) rubric_renderer = CombinedOpenEndedRubric(system, True)
success, rubric_feedback = rubric_renderer.render_rubric(stringify_children(definition['rubric'])) success, rubric_feedback = rubric_renderer.render_rubric(stringify_children(definition['rubric']))
if not success: if not success:
error_message="Could not parse rubric : {0}".format(definition['rubric']) error_message="Could not parse rubric : {0}".format(definition['rubric'])
...@@ -327,7 +325,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -327,7 +325,7 @@ class CombinedOpenEndedModule(XModule):
Output: HTML rendered directly via Mako Output: HTML rendered directly via Mako
""" """
context = self.get_context() context = self.get_context()
html = render_to_string('combined_open_ended.html', context) html = self.system.render_template('combined_open_ended.html', context)
return html return html
def get_html_base(self): def get_html_base(self):
...@@ -377,17 +375,17 @@ class CombinedOpenEndedModule(XModule): ...@@ -377,17 +375,17 @@ class CombinedOpenEndedModule(XModule):
self.static_data, instance_state=task_state) self.static_data, instance_state=task_state)
last_response = task.latest_answer() last_response = task.latest_answer()
last_score = task.latest_score() last_score = task.latest_score()
last_post_assessment = task.latest_post_assessment() last_post_assessment = task.latest_post_assessment(self.system)
last_post_feedback = "" last_post_feedback = ""
if task_type == "openended": if task_type == "openended":
last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False) last_post_assessment = task.latest_post_assessment(self.system, short_feedback=False, join_feedback=False)
if isinstance(last_post_assessment, list): if isinstance(last_post_assessment, list):
eval_list = [] eval_list = []
for i in xrange(0, len(last_post_assessment)): for i in xrange(0, len(last_post_assessment)):
eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i])) eval_list.append(task.format_feedback_with_evaluation(self.system, last_post_assessment[i]))
last_post_evaluation = "".join(eval_list) last_post_evaluation = "".join(eval_list)
else: else:
last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment)
last_post_assessment = last_post_evaluation last_post_assessment = last_post_evaluation
last_correctness = task.is_last_response_correct() last_correctness = task.is_last_response_correct()
max_score = task.max_score() max_score = task.max_score()
...@@ -450,7 +448,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -450,7 +448,7 @@ class CombinedOpenEndedModule(XModule):
self.update_task_states() self.update_task_states()
response_dict = self.get_last_response(task_number) response_dict = self.get_last_response(task_number)
context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1} context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1}
html = render_to_string('combined_open_ended_results.html', context) html = self.system.render_template('combined_open_ended_results.html', context)
return {'html': html, 'success': True} return {'html': html, 'success': True}
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
......
from mitxmako.shortcuts import render_to_string
import logging import logging
from lxml import etree from lxml import etree
log=logging.getLogger(__name__) log=logging.getLogger(__name__)
class CombinedOpenEndedRubric: class CombinedOpenEndedRubric(object):
def __init__ (self, view_only = False): def __init__ (self, system, view_only = False):
self.has_score = False self.has_score = False
self.view_only = view_only self.view_only = view_only
self.system
''' '''
render_rubric: takes in an xml string and outputs the corresponding render_rubric: takes in an xml string and outputs the corresponding
...@@ -23,7 +23,7 @@ class CombinedOpenEndedRubric: ...@@ -23,7 +23,7 @@ class CombinedOpenEndedRubric:
success = False success = False
try: try:
rubric_categories = self.extract_categories(rubric_xml) rubric_categories = self.extract_categories(rubric_xml)
html = render_to_string('open_ended_rubric.html', html = self.system.render_template('open_ended_rubric.html',
{'categories' : rubric_categories, {'categories' : rubric_categories,
'has_score': self.has_score, 'has_score': self.has_score,
'view_only': self.view_only}) 'view_only': self.view_only})
......
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):
......
...@@ -30,7 +30,6 @@ from xmodule.modulestore import Location ...@@ -30,7 +30,6 @@ from xmodule.modulestore import Location
from capa.util import * from capa.util import *
import openendedchild import openendedchild
from mitxmako.shortcuts import render_to_string
from numpy import median from numpy import median
from datetime import datetime from datetime import datetime
...@@ -256,7 +255,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -256,7 +255,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@param system: Modulesystem @param system: Modulesystem
@return: Boolean True (not useful currently) @return: Boolean True (not useful currently)
""" """
new_score_msg = self._parse_score_msg(score_msg) new_score_msg = self._parse_score_msg(score_msg, system)
if not new_score_msg['valid']: if not new_score_msg['valid']:
score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.'
...@@ -370,7 +369,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -370,7 +369,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return u"\n".join([feedback_list_part1, feedback_list_part2]) return u"\n".join([feedback_list_part1, feedback_list_part2])
def _format_feedback(self, response_items): def _format_feedback(self, response_items, system):
""" """
Input: Input:
Dictionary called feedback. Must contain keys seen below. Dictionary called feedback. Must contain keys seen below.
...@@ -382,14 +381,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -382,14 +381,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_feedback="" rubric_feedback=""
feedback = self._convert_longform_feedback_to_html(response_items) feedback = self._convert_longform_feedback_to_html(response_items)
if response_items['rubric_scores_complete']==True: if response_items['rubric_scores_complete']==True:
<<<<<<< HEAD
rubric_renderer = CombinedOpenEndedRubric(True) rubric_renderer = CombinedOpenEndedRubric(True)
success, rubric_feedback = rubric_renderer.render_rubric(response_items['rubric_xml']) success, rubric_feedback = rubric_renderer.render_rubric(response_items['rubric_xml'])
=======
rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml'], system)
>>>>>>> master
if not response_items['success']: if not response_items['success']:
return system.render_template("open_ended_error.html", return system.render_template("open_ended_error.html",
{'errors': feedback}) {'errors': feedback})
feedback_template = render_to_string("open_ended_feedback.html", { feedback_template = system.render_template("open_ended_feedback.html", {
'grader_type': response_items['grader_type'], 'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback, 'feedback': feedback,
...@@ -399,7 +402,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -399,7 +402,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return feedback_template return feedback_template
def _parse_score_msg(self, score_msg, join_feedback=True): def _parse_score_msg(self, score_msg, system, join_feedback=True):
""" """
Grader reply is a JSON-dump of the following dict Grader reply is a JSON-dump of the following dict
{ 'correct': True/False, { 'correct': True/False,
...@@ -451,7 +454,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -451,7 +454,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'rubric_scores_complete' : score_result['rubric_scores_complete'][i], 'rubric_scores_complete' : score_result['rubric_scores_complete'][i],
'rubric_xml' : score_result['rubric_xml'][i], 'rubric_xml' : score_result['rubric_xml'][i],
} }
feedback_items.append(self._format_feedback(new_score_result)) feedback_items.append(self._format_feedback(new_score_result, system))
if join_feedback: if join_feedback:
feedback = "".join(feedback_items) feedback = "".join(feedback_items)
else: else:
...@@ -459,7 +462,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -459,7 +462,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = int(median(score_result['score'])) score = int(median(score_result['score']))
else: else:
#This is for instructor and ML grading #This is for instructor and ML grading
feedback = self._format_feedback(score_result) feedback = self._format_feedback(score_result, system)
score = score_result['score'] score = score_result['score']
self.submission_id = score_result['submission_id'] self.submission_id = score_result['submission_id']
...@@ -467,7 +470,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -467,7 +470,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return {'valid': True, 'score': score, 'feedback': feedback} return {'valid': True, 'score': score, 'feedback': feedback}
def latest_post_assessment(self, short_feedback=False, join_feedback=True): def latest_post_assessment(self, system, short_feedback=False, join_feedback=True):
""" """
Gets the latest feedback, parses, and returns Gets the latest feedback, parses, and returns
@param short_feedback: If the long feedback is wanted or not @param short_feedback: If the long feedback is wanted or not
...@@ -476,7 +479,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -476,7 +479,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not self.history: if not self.history:
return "" return ""
feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback) feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), system, join_feedback=join_feedback)
if not short_feedback: if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else '' return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']: if feedback_dict['valid']:
...@@ -484,14 +487,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -484,14 +487,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
json.loads(self.history[-1].get('post_assessment', ""))) json.loads(self.history[-1].get('post_assessment', "")))
return short_feedback if feedback_dict['valid'] else '' return short_feedback if feedback_dict['valid'] else ''
def format_feedback_with_evaluation(self, feedback): def format_feedback_with_evaluation(self, system, feedback):
""" """
Renders a given html feedback into an evaluation template Renders a given html feedback into an evaluation template
@param feedback: HTML feedback @param feedback: HTML feedback
@return: Rendered html @return: Rendered html
""" """
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50} context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
html = render_to_string('open_ended_evaluation.html', context) html = system.render_template('open_ended_evaluation.html', context)
return html return html
def handle_ajax(self, dispatch, get, system): def handle_ajax(self, dispatch, get, system):
...@@ -583,7 +586,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -583,7 +586,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if self.state != self.INITIAL: if self.state != self.INITIAL:
latest = self.latest_answer() latest = self.latest_answer()
previous_answer = latest if latest is not None else self.initial_display previous_answer = latest if latest is not None else self.initial_display
post_assessment = self.latest_post_assessment() post_assessment = self.latest_post_assessment(system)
score = self.latest_score() score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect' correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
else: else:
......
...@@ -35,7 +35,7 @@ MAX_ATTEMPTS = 1 ...@@ -35,7 +35,7 @@ MAX_ATTEMPTS = 1
# Overriden by max_score specified in xml. # Overriden by max_score specified in xml.
MAX_SCORE = 1 MAX_SCORE = 1
class OpenEndedChild(): class OpenEndedChild(object):
""" """
States: States:
...@@ -123,7 +123,7 @@ class OpenEndedChild(): ...@@ -123,7 +123,7 @@ class OpenEndedChild():
return None return None
return self.history[-1].get('score') return self.history[-1].get('score')
def latest_post_assessment(self): def latest_post_assessment(self, system):
"""None if not available""" """None if not available"""
if not self.history: if not self.history:
return "" return ""
......
...@@ -122,8 +122,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -122,8 +122,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
if self.state == self.INITIAL: if self.state == self.INITIAL:
return '' return ''
rubric_renderer = CombinedOpenEndedRubric(True) rubric_renderer = CombinedOpenEndedRubric(system, True)
success, rubric_html = rubric_renderer.render_rubric(self.rubric) success, rubric_html = rubric_renderer.render_rubric(self.rubric)
# we'll render it # we'll render it
...@@ -149,7 +148,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -149,7 +148,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
if self.state == self.DONE: if self.state == self.DONE:
# display the previous hint # display the previous hint
latest = self.latest_post_assessment() latest = self.latest_post_assessment(system)
hint = latest if latest is not None else '' hint = latest if latest is not None else ''
else: else:
hint = '' hint = ''
......
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):
""" """
......
...@@ -13,6 +13,8 @@ from util.json_request import expect_json ...@@ -13,6 +13,8 @@ from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric
from lxml import etree from lxml import etree
from mitxmako.shortcuts import render_to_string
from xmodule.x_module import ModuleSystem
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -29,6 +31,7 @@ class GradingService(object): ...@@ -29,6 +31,7 @@ class GradingService(object):
self.url = config['url'] self.url = config['url']
self.login_url = self.url + '/login/' self.login_url = self.url + '/login/'
self.session = requests.session() self.session = requests.session()
self.system = ModuleSystem(None, None, None, render_to_string, None)
def _login(self): def _login(self):
""" """
...@@ -109,7 +112,7 @@ class GradingService(object): ...@@ -109,7 +112,7 @@ class GradingService(object):
response_json = json.loads(response) response_json = json.loads(response)
if response_json.has_key('rubric'): if response_json.has_key('rubric'):
rubric = response_json['rubric'] rubric = response_json['rubric']
rubric_renderer = CombinedOpenEndedRubric(False) rubric_renderer = CombinedOpenEndedRubric(self.system, False)
success, rubric_html = rubric_renderer.render_rubric(rubric) success, rubric_html = rubric_renderer.render_rubric(rubric)
if not success: if not success:
error_message = "Could not render rubric: {0}".format(rubric) error_message = "Could not render rubric: {0}".format(rubric)
......
...@@ -426,6 +426,14 @@ ...@@ -426,6 +426,14 @@
font-size: 1.2rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
} }
strong {
font-weight: 700;
a {
font-weight: 700;
}
}
} }
.actions { .actions {
......
...@@ -232,8 +232,9 @@ $red: rgb(178, 6, 16); ...@@ -232,8 +232,9 @@ $red: rgb(178, 6, 16);
} }
input, textarea { input, textarea {
height: 100%;
width: 100%; width: 100%;
padding: $baseline ($baseline*.75); padding: ($baseline/2);
&.long { &.long {
width: 100%; width: 100%;
......
...@@ -243,22 +243,15 @@ ...@@ -243,22 +243,15 @@
% endif % endif
% if registration.is_rejected: % if registration.is_rejected:
<div class="message message-status is-shown exam-schedule"> <div class="message message-status is-shown exam-schedule">
<p class="message-copy">Your <p class="message-copy"><strong>Your registration for the Pearson exam has been rejected. Please <a href="${testcenter_register_target}" id="exam_register_link">see your registration status details</a></strong>. Otherwise <a class="contact-link" href="mailto:exam-help@edx.org?subject=Pearson VUE Exam - ${get_course_about_section(course, 'university')} ${course.number}">contact edX at exam-help@edx.org</a> for further help.</p>
<a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a>
has been rejected. Please check the information you provided, and try to correct any demographic errors. Otherwise
contact edX for further help.</p>
<a href="mailto:exam-help@edx.org?subject=Pearson VUE Exam - ${get_course_about_section(course, 'university')} ${course.number}" class="button contact-button">Contact exam-help@edx.org</a>
</div> </div>
% endif % endif
% if not registration.is_accepted and not registration.is_rejected: % if not registration.is_accepted and not registration.is_rejected:
<div class="message message-status is-shown"> <div class="message message-status is-shown">
<p class="message-copy">Your <p class="message-copy"><strong>Your <a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a> is pending</strong>. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.</p>
<a href="${testcenter_register_target}" id="exam_register_link">registration for the Pearson exam</a>
is pending. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.</p>
</div> </div>
% endif % endif
% endif % endif
% endif % endif
<% <%
......
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
% if registration.registration_is_rejected: % if registration.registration_is_rejected:
<section class="status message message-flash registration-rejected message-action is-shown"> <section class="status message message-flash registration-rejected message-action is-shown">
<h3 class="message-title">Your registration for the Pearson exam has been rejected</h3> <h3 class="message-title">Your registration for the Pearson exam has been rejected</h3>
<p class="message-copy">Please see your registration status details for more information.</p> <p class="message-copy">Please see your <strong>registration status</strong> details for more information.</p>
</section> </section>
% endif % endif
...@@ -246,25 +246,25 @@ ...@@ -246,25 +246,25 @@
<li class="field-group phoneinfo"> <li class="field-group phoneinfo">
<div class="field required" id="field-phone"> <div class="field required" id="field-phone">
<label for="phone">Phone Number</label> <label for="phone">Phone Number</label>
<input id="phone" type="tel" name="phone" value="${testcenteruser.phone}" placeholder="e.g. 1-55-555-5555" /> <input id="phone" type="tel" name="phone" value="${testcenteruser.phone}" />
</div> </div>
<div class="field" id="field-extension"> <div class="field" id="field-extension">
<label for="extension">Extension</label> <label for="extension">Extension</label>
<input id="extension" class="short" type="tel" name="extension" value="${testcenteruser.extension}" placeholder="e.g. 555" /> <input id="extension" class="short" type="tel" name="extension" value="${testcenteruser.extension}" />
</div> </div>
<div class="field required" id="field-phone_country_code"> <div class="field required" id="field-phone_country_code">
<label for="phone_country_code">Phone Country Code</label> <label for="phone_country_code">Phone Country Code</label>
<input id="phone_country_code" class="short" type="text" name="phone_country_code" value="${testcenteruser.phone_country_code}" placeholder="e.g. 1, 44, 976" /> <input id="phone_country_code" class="short" type="text" name="phone_country_code" value="${testcenteruser.phone_country_code}" />
</div> </div>
</li> </li>
<li class="field-group faxinfo"> <li class="field-group faxinfo">
<div class="field" id="field-fax"> <div class="field" id="field-fax">
<label for="fax">Fax Number</label> <label for="fax">Fax Number</label>
<input id="fax" type="tel" class="short" name="fax" value="${testcenteruser.fax}" placeholder="e.g. 1-55-555-5555" /> <input id="fax" type="tel" class="short" name="fax" value="${testcenteruser.fax}" />
</div> </div>
<div class="field" id="field-fax_country_code"> <div class="field" id="field-fax_country_code">
<label for="fax_country_code">Fax Country Code</label> <label for="fax_country_code">Fax Country Code</label>
<input id="fax_country_code" class="short" type="text" name="fax_country_code" value="${testcenteruser.fax_country_code}" placeholder="e.g. 1, 44, 976" /> <input id="fax_country_code" class="short" type="text" name="fax_country_code" value="${testcenteruser.fax_country_code}" />
</div> </div>
</li> </li>
<li class="field" id="field-company_name"> <li class="field" id="field-company_name">
...@@ -474,7 +474,7 @@ ...@@ -474,7 +474,7 @@
<div class="details details-contact"> <div class="details details-contact">
<h4>Questions</h4> <h4>Questions</h4>
<p>If you have a specific question pertaining to your registration, you may contact <a class="contact-link" href="${exam_help_href}">exam-help@edx.org</a>.</p> <p>If you have a specific question pertaining to your registration, you may <a class="contact-link" href="${exam_help_href}">contact edX at exam-help@edx.org</a>.</p>
</div> </div>
</aside> </aside>
</section> </section>
...@@ -51,8 +51,7 @@ pygraphviz==1.1 ...@@ -51,8 +51,7 @@ pygraphviz==1.1
pil==1.1.7 pil==1.1.7
nltk==2.0.4 nltk==2.0.4
dogstatsd-python==0.2.1 dogstatsd-python==0.2.1
# Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs. MySQL-python==1.2.4c1
# MySQL-python
sphinx==1.1.3 sphinx==1.1.3
Shapely==1.2.16 Shapely==1.2.16
ipython==0.13.1 ipython==0.13.1
......
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