diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index dfac1ac9c63d6038d111f20dc14001393f813122..7e0019205e79aa3275c2f8defbc0940acc6db863 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,7 +1,9 @@ +from fs.errors import ResourceNotFoundError import time import dateutil.parser import logging +from xmodule.graders import load_grading_policy from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule @@ -14,6 +16,9 @@ class CourseDescriptor(SequenceDescriptor): def __init__(self, system, definition=None, **kwargs): super(CourseDescriptor, self).__init__(system, definition, **kwargs) + + self._grader = None + self._grade_cutoffs = None try: self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") @@ -27,7 +32,34 @@ class CourseDescriptor(SequenceDescriptor): def has_started(self): return time.gmtime() > self.start - + + @property + def grader(self): + self.__load_grading_policy() + return self._grader + + @property + def grade_cutoffs(self): + self.__load_grading_policy() + return self._grade_cutoffs + + + def __load_grading_policy(self): + if not self._grader or not self._grade_cutoffs: + policy_string = "" + + try: + with self.system.resources_fs.open("grading_policy.json") as grading_policy_file: + policy_string = grading_policy_file.read() + except (IOError, ResourceNotFoundError): + log.warning("Unable to load course settings file from grading_policy.json in course " + self.id) + + grading_policy = load_grading_policy(policy_string) + + self._grader = grading_policy['GRADER'] + self._grade_cutoffs = grading_policy['GRADE_CUTOFFS'] + + @staticmethod def id_to_location(course_id): '''Convert the given course_id (org/course/name) to a location object. diff --git a/common/lib/xmodule/xmodule/graders.py b/common/lib/xmodule/xmodule/graders.py index 4473c8f1b9bfc22e81aaaa9b047fe8346dc6d6f7..fca862aa9f7b3c4a22acb6be3eb3365474563b31 100644 --- a/common/lib/xmodule/xmodule/graders.py +++ b/common/lib/xmodule/xmodule/graders.py @@ -1,4 +1,5 @@ import abc +import json import logging from collections import namedtuple @@ -9,6 +10,69 @@ log = logging.getLogger("mitx.courseware") # Section either indicates the name of the problem or the name of the section Score = namedtuple("Score", "earned possible graded section") +def load_grading_policy(course_policy_string): + """ + This loads a grading policy from a string (usually read from a file), + which can be a JSON object or an empty string. + + The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is + missing, it reverts to the default. + """ + + default_policy_string = """ + { + "GRADER" : [ + { + "type" : "Homework", + "min_count" : 12, + "drop_count" : 2, + "short_label" : "HW", + "weight" : 0.15 + }, + { + "type" : "Lab", + "min_count" : 12, + "drop_count" : 2, + "category" : "Labs", + "weight" : 0.15 + }, + { + "type" : "Midterm", + "name" : "Midterm Exam", + "short_label" : "Midterm", + "weight" : 0.3 + }, + { + "type" : "Final", + "name" : "Final Exam", + "short_label" : "Final", + "weight" : 0.4 + } + ], + "GRADE_CUTOFFS" : { + "A" : 0.87, + "B" : 0.7, + "C" : 0.6 + } + } + """ + + # Load the global settings as a dictionary + grading_policy = json.loads(default_policy_string) + + # Load the course policies as a dictionary + course_policy = {} + if course_policy_string: + course_policy = json.loads(course_policy_string) + + # Override any global settings with the course settings + grading_policy.update(course_policy) + + # Here is where we should parse any configurations, so that we can fail early + grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER']) + + return grading_policy + def aggregate_scores(scores, section_name="summary"): """ diff --git a/lms/djangoapps/courseware/course_settings.py b/lms/djangoapps/courseware/course_settings.py deleted file mode 100644 index 5b2348bee64a9dcad7fe7e3022042f72bdfaa69c..0000000000000000000000000000000000000000 --- a/lms/djangoapps/courseware/course_settings.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -Course settings module. All settings in the global_settings are -first applied, and then any settings in the settings.DATA_DIR/course_settings.json -are applied. A setting must be in ALL_CAPS. - -Settings are used by calling - -from courseware.course_settings import course_settings - -Note that courseware.course_settings.course_settings is not a module -- it's an object. So -importing individual settings is not possible: - -from courseware.course_settings.course_settings import GRADER # This won't work. - -""" -import json -import logging - -from django.conf import settings - -from xmodule import graders - -log = logging.getLogger("mitx.courseware") - -global_settings_json = """ -{ - "GRADER" : [ - { - "type" : "Homework", - "min_count" : 12, - "drop_count" : 2, - "short_label" : "HW", - "weight" : 0.15 - }, - { - "type" : "Lab", - "min_count" : 12, - "drop_count" : 2, - "category" : "Labs", - "weight" : 0.15 - }, - { - "type" : "Midterm", - "name" : "Midterm Exam", - "short_label" : "Midterm", - "weight" : 0.3 - }, - { - "type" : "Final", - "name" : "Final Exam", - "short_label" : "Final", - "weight" : 0.4 - } - ], - "GRADE_CUTOFFS" : { - "A" : 0.87, - "B" : 0.7, - "C" : 0.6 - } -} -""" - - -class Settings(object): - def __init__(self): - - # Load the global settings as a dictionary - global_settings = json.loads(global_settings_json) - - # Load the course settings as a dictionary - course_settings = {} - try: - # TODO: this doesn't work with multicourse - with open(settings.DATA_DIR + "/course_settings.json") as course_settings_file: - course_settings_string = course_settings_file.read() - course_settings = json.loads(course_settings_string) - except IOError: - log.warning("Unable to load course settings file from " + str(settings.DATA_DIR) + "/course_settings.json") - - # Override any global settings with the course settings - global_settings.update(course_settings) - - # Now, set the properties from the course settings on ourselves - for setting in global_settings: - setting_value = global_settings[setting] - setattr(self, setting, setting_value) - - # Here is where we should parse any configurations, so that we can fail early - self.GRADER = graders.grader_from_conf(self.GRADER) - -course_settings = Settings() diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 66aff08dca1f667596437b9367cce2b696994006..5a817e3d6ccd3b06993a4e6a0b1b5a3655683d73 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -3,7 +3,6 @@ import logging from django.conf import settings -from courseware.course_settings import course_settings from xmodule import graders from xmodule.graders import Score from models import StudentModule @@ -11,7 +10,7 @@ from models import StudentModule _log = logging.getLogger("mitx.courseware") -def grade_sheet(student, course, student_module_cache): +def grade_sheet(student, course, grader, student_module_cache): """ This pulls a summary of all problems in the course. It returns a dictionary with two datastructures: @@ -78,7 +77,6 @@ def grade_sheet(student, course, student_module_cache): 'chapter': c.metadata.get('display_name'), 'sections': sections}) - grader = course_settings.GRADER grade_summary = grader.grade(totaled_scores) return {'courseware_summary': chapters, diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 00fde8a84c3ac9dab3362cbce6aa0788a2a9fdfe..7281ab01ad85d62046f3cc6cd63dbe6e0498cdc4 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -19,7 +19,6 @@ from django.views.decorators.cache import cache_control from module_render import toc_for_course, get_module, get_section from models import StudentModuleCache from student.models import UserProfile -from multicourse import multicourse_settings from xmodule.modulestore import Location from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.django import modulestore @@ -110,8 +109,8 @@ def profile(request, course_id, student_id=None): user_info = UserProfile.objects.get(user=student) student_module_cache = StudentModuleCache(request.user, course) - course, _, _, _ = get_module(request.user, request, course.location, student_module_cache) - + course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache) + context = {'name': user_info.name, 'username': student.username, 'location': user_info.location, @@ -121,7 +120,7 @@ def profile(request, course_id, student_id=None): 'format_url_params': format_url_params, 'csrf': csrf(request)['csrf_token'] } - context.update(grades.grade_sheet(student, course, student_module_cache)) + context.update(grades.grade_sheet(student, course_module, course.grader, student_module_cache)) return render_to_response('profile.html', context) @@ -184,9 +183,6 @@ def index(request, course_id, chapter=None, section=None, chapter = clean(chapter) section = clean(section) - if settings.ENABLE_MULTICOURSE: - settings.MODULESTORE['default']['OPTIONS']['data_dir'] = settings.DATA_DIR + multicourse_settings.get_course_xmlpath(course) - context = { 'csrf': csrf(request)['csrf_token'], 'accordion': render_accordion(request, course, chapter, section), diff --git a/lms/templates/profile.html b/lms/templates/profile.html index 1e3bde59697f83cfceb54a4d3455550b12288b05..6cda85fb037fa6368bb23e6c87d6e932914b96d0 100644 --- a/lms/templates/profile.html +++ b/lms/templates/profile.html @@ -4,6 +4,7 @@ <%block name="headextra"> <%static:css group='course'/> </%block> + <%namespace name="profile_graphs" file="profile_graphs.js"/> <%block name="title"><title>Profile - edX 6.002x</title></%block> @@ -110,9 +111,9 @@ $(function() { </%block> -<%include file="navigation.html" args="active_page='profile'" /> +<%include file="course_navigation.html" args="active_page='profile'" /> -<section class="main-content"> +<section class="container"> <div class="profile-wrapper"> <section class="course-info"> @@ -126,8 +127,7 @@ $(function() { %for chapter in courseware_summary: %if not chapter['chapter'] == "hidden": <li> - <h2><a href="${reverse('courseware_chapter', args=format_url_params([chapter['course'], chapter['chapter']])) }"> - ${ chapter['chapter'] }</a></h2> + <h2>${ chapter['chapter'] }</h2> <ol class="sections"> %for section in chapter['sections']: @@ -138,7 +138,7 @@ $(function() { percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else "" %> - <h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }"> + <h3><a href="${reverse('courseware_section', kwargs={'course_id' : course.id, 'chapter' : chapter['chapter'], 'section' : section['section']})}"> ${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3> ${section['format']} %if 'due' in section and section['due']!="":