diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 90f7aefb79..ff61f5cdc0 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -1,5 +1,6 @@ import json import hashlib +import logging from lxml import etree from mako.template import Template @@ -11,14 +12,14 @@ try: # This lets us do __name__ == ='__main__' except: settings = None - - ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. TODO: Shift everything from xml.dom.minidom to XPath (or XQuery) ''' +log = logging.getLogger("mitx.courseware") + def fasthash(string): m = hashlib.new("md4") m.update(string) @@ -83,11 +84,40 @@ def id_tag(course): if elem.get('id'): pass elif elem.get(default_ids[elem.tag]): - new_id = elem.get(default_ids[elem.tag]) # Convert to alphanumeric - new_id = "".join([a for a in new_id if a.isalnum()]) + new_id = elem.get(default_ids[elem.tag]) + new_id = "".join([a for a in new_id if a.isalnum()]) # Convert to alphanumeric + # Without this, a conflict may occur between an hmtl or youtube id + new_id = default_ids[elem.tag] + new_id elem.set('id', new_id) else: - elem.set('id', fasthash(etree.tostring(elem))) + elem.set('id', fasthash(etree.tostring(elem))) + +def propogate_downward_tag(element, attribute_name, parent_attribute = None): + ''' This call is to pass down an attribute to all children. If an element + has this attribute, it will be "inherited" by all of its children. If a + child (A) already has that attribute, A will keep the same attribute and + all of A's children will inherit A's attribute. This is a recursive call.''' + + if (parent_attribute == None): #This is the entry call. Select all due elements + all_attributed_elements = element.xpath("//*[@" + attribute_name +"]") + for attributed_element in all_attributed_elements: + attribute_value = attributed_element.get(attribute_name) + for child_element in attributed_element: + propogate_downward_tag(child_element, attribute_name, attribute_value) + else: + '''The hack below is because we would get _ContentOnlyELements from the + iterator that can't have due dates set. We can't find API for it. If we + ever have an element which subclasses BaseElement, we will not tag it''' + if not element.get(attribute_name) and type(element) == etree._Element: + element.set(attribute_name, parent_attribute) + + for child_element in element: + propogate_downward_tag(child_element, attribute_name, parent_attribute) + else: + #This element would have already been found by Xpath, so we return + #for now and trust that this element will get its turn to propogate + #to its children later. + return template_lookup = TemplateLookup(directories = [settings.DATA_DIR], module_directory = settings.MAKO_MODULE_DIR) @@ -101,6 +131,8 @@ def course_file(user): tree = etree.XML(data_template.render(**options)) id_tag(tree) + propogate_downward_tag(tree, "due") + propogate_downward_tag(tree, "graded") return tree def module_xml(coursefile, module, id_tag, module_id): diff --git a/courseware/models.py b/courseware/models.py index 5110f1d24f..f713da3195 100644 --- a/courseware/models.py +++ b/courseware/models.py @@ -24,7 +24,7 @@ class StudentModule(models.Model): module_id = models.CharField(max_length=255, db_index=True) # Filename for homeworks, etc. student = models.ForeignKey(User, db_index=True) class Meta: - unique_together = (('student', 'module_id', 'module_type'),) + unique_together = (('student', 'module_id'),) ## Internal state of the object state = models.TextField(null=True, blank=True) diff --git a/courseware/module_render.py b/courseware/module_render.py index 5dfc5e1b3b..93becb27fa 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -1,5 +1,6 @@ import StringIO import json +import logging import os import sys import sys @@ -32,6 +33,8 @@ import courseware.modules.seq_module import courseware.modules.vertical_module import courseware.modules.video_module +log = logging.getLogger("mitx.courseware") + ## TODO: Add registration mechanism modx_modules={'problem':courseware.modules.capa_module.LoncapaModule, 'video':courseware.modules.video_module.VideoModule, @@ -59,11 +62,10 @@ def make_track_function(request): def modx_dispatch(request, module=None, dispatch=None, id=None): ''' Generic view for extensions. ''' # Grab the student information for the module from the database - s = StudentModule.objects.filter(module_type=module, - student=request.user, + s = StudentModule.objects.filter(student=request.user, module_id=id) if len(s) == 0: - print "ls404", module, request.user, id + log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id)) raise Http404 s=s[0] diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index b6c7f1748c..380117d9a0 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -46,13 +46,13 @@ class LoncapaModule(XModule): def get_html(self): return render_to_string('problem_ajax.html', - {'id':self.filename, + {'id':self.item_id, 'ajax_url':self.ajax_url, }) def get_init_js(self): return render_to_string('problem.js', - {'id':self.filename, + {'id':self.item_id, 'ajax_url':self.ajax_url, }) @@ -100,7 +100,7 @@ class LoncapaModule(XModule): html=render_to_string('problem.html', {'problem' : content, - 'id' : self.filename, + 'id' : self.item_id, 'check_button' : check_button, 'reset_button' : reset_button, 'save_button' : save_button, diff --git a/courseware/views.py b/courseware/views.py index ec514efd98..e0857d8789 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -43,6 +43,12 @@ def profile(request): chapters = dom.xpath('//course[@name=$course]/chapter', course=course) responses=StudentModule.objects.filter(student=request.user) + response_by_id = {} + for response in responses: + response_by_id[response.module_id] = response + + + total_scores = {} for c in chapters: chname=c.get('name') @@ -53,31 +59,124 @@ def profile(request): scores=[] if len(problems)>0: for p in problems: - id = p.get('filename') + id = p.get('id') correct = 0 - for response in responses: - if response.module_id == id: - if response.grade!=None: - correct=response.grade - else: - correct=0 + if id in response_by_id: + response = response_by_id[id] + if response.grade!=None: + correct=response.grade + total=courseware.modules.capa_module.LoncapaModule(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? - scores.append((int(correct),total)) + scores.append((int(correct),total, ( True if s.get('graded') == "True" else False ) )) + + + section_total = (sum([score[0] for score in scores]), + sum([score[1] for score in scores])) + + graded_total = (sum([score[0] for score in scores if score[2]]), + sum([score[1] for score in scores if score[2]])) + + #Add the graded total to total_scores + format = s.get('format') if s.get('format') else "" + if format and graded_total[1] > 0: + format_scores = total_scores[ format ] if format in total_scores else [] + format_scores.append( graded_total ) + total_scores[ format ] = format_scores + score={'course':course, 'section':s.get("name"), 'chapter':c.get("name"), 'scores':scores, + 'section_total' : section_total, + 'format' : format, } hw.append(score) - + + + + def totalWithDrops(scores, drop_count): + sorted_scores = sorted( enumerate(scores), key=lambda x: -x[1]['percentage'] ) #Note that this key will sort the list descending + dropped_indices = [score[0] for score in sorted_scores[-drop_count:]] # A list of the indices of the dropped scores + aggregate_score = 0 + for index, score in enumerate(scores): + if index not in dropped_indices: + aggregate_score += score['percentage'] + + aggregate_score /= len(scores) - drop_count + + return aggregate_score, dropped_indices + + #Figure the homework scores + homework_scores = total_scores['Homework'] if 'Homework' in total_scores else [] + homework_percentages = [] + for i in range(12): + if i < len(homework_scores): + percentage = homework_scores[i][0] / float(homework_scores[i][1]) + summary = "{:.0%} ({}/{})".format( percentage, homework_scores[i][0], homework_scores[i][1] ) + else: + percentage = 0 + summary = "0% (?/?)" + summary = "Homework {} - {}".format(i + 1, summary) + + homework_percentages.append( {'percentage': percentage, 'summary': summary} ) + homework_total, homework_dropped_indices = totalWithDrops(homework_percentages, 2) + + #Figure the lab scores + lab_scores = total_scores['Lab'] if 'Lab' in total_scores else [] + lab_percentages = [] + for i in range(12): + if i < len(lab_scores): + percentage = lab_scores[i][0] / float(lab_scores[i][1]) + summary = "{:.0%} ({}/{})".format( percentage, lab_scores[i][0], lab_scores[i][1] ) + else: + percentage = 0 + summary = "0% (?/?)" + summary = "Lab {} - {}".format(i + 1, summary) + lab_percentages.append( {'percentage': percentage, 'summary': summary} ) + lab_total, lab_dropped_indices = totalWithDrops(lab_percentages, 2) + + midterm_score = (120, 150) + midterm_percentage = midterm_score[0] / float(midterm_score[1]) + + final_score = (200, 300) + final_percentage = final_score[0] / float(final_score[1]) + + grade_summary = [ + { + 'category': 'Homework', + 'subscores' : homework_percentages, + 'dropped_indices' : homework_dropped_indices, + 'totalscore' : {'score' : homework_total, 'summary' : "Homework Average - {:.0%}".format(homework_total)}, + 'weight' : 0.15, + }, + { + 'category': 'Labs', + 'subscores' : lab_percentages, + 'dropped_indices' : lab_dropped_indices, + 'totalscore' : {'score' : lab_total, 'summary' : "Lab Average - {:.0%}".format(lab_total)}, + 'weight' : 0.15, + }, + { + 'category': 'Midterm', + 'totalscore' : {'score' : midterm_percentage, 'summary' : "Midterm - {:.0%} ({}/{})".format(midterm_percentage, midterm_score[0], midterm_score[1])}, + 'weight' : 0.30, + }, + { + 'category': 'Final', + 'totalscore' : {'score' : final_percentage, 'summary' : "Final - {:.0%} ({}/{})".format(final_percentage, final_score[0], final_score[1])}, + 'weight' : 0.40, + } + ] + + user_info=UserProfile.objects.get(user=request.user) - context={'name':user_info.name, 'username':request.user.username, 'location':user_info.location, 'language':user_info.language, 'email':request.user.email, - 'homeworks':hw, + 'homeworks':hw, + 'grade_summary' : grade_summary, 'csrf':csrf(request)['csrf_token'] } return render_to_response('profile.html', context)