212 lines
7.9 KiB
Python
212 lines
7.9 KiB
Python
"""
|
|
Course settings module. The settings are based of django.conf. All settings in
|
|
courseware.global_course_settings are first applied, and then any settings
|
|
in the settings.DATA_DIR/course_settings.py are applied. A setting must be
|
|
in ALL_CAPS.
|
|
|
|
Settings are used by calling
|
|
|
|
from courseware import course_settings
|
|
|
|
Note that courseware.course_settings is not a module -- it's an object. So
|
|
importing individual settings is not possible:
|
|
|
|
from courseware.course_settings import GRADER # This won't work.
|
|
|
|
"""
|
|
|
|
from lxml import etree
|
|
import random
|
|
import imp
|
|
import logging
|
|
import sys
|
|
import types
|
|
|
|
from django.conf import settings
|
|
|
|
from courseware import global_course_settings
|
|
from courseware import graders
|
|
from courseware.graders import Score
|
|
from models import StudentModule
|
|
import courseware.content_parser as content_parser
|
|
import courseware.modules
|
|
|
|
_log = logging.getLogger("mitx.courseware")
|
|
|
|
class Settings(object):
|
|
def __init__(self):
|
|
# update this dict from global settings (but only for ALL_CAPS settings)
|
|
for setting in dir(global_course_settings):
|
|
if setting == setting.upper():
|
|
setattr(self, setting, getattr(global_course_settings, setting))
|
|
|
|
|
|
data_dir = settings.DATA_DIR
|
|
|
|
fp = None
|
|
try:
|
|
fp, pathname, description = imp.find_module("course_settings", [data_dir])
|
|
mod = imp.load_module("course_settings", fp, pathname, description)
|
|
except Exception as e:
|
|
_log.exception("Unable to import course settings file from " + data_dir + ". Error: " + str(e))
|
|
mod = types.ModuleType('course_settings')
|
|
finally:
|
|
if fp:
|
|
fp.close()
|
|
|
|
for setting in dir(mod):
|
|
if setting == setting.upper():
|
|
setting_value = getattr(mod, 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()
|
|
|
|
|
|
|
|
|
|
def grade_sheet(student):
|
|
"""
|
|
This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
|
|
|
|
- courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters,
|
|
each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded
|
|
problems, and is good for displaying a course summary with due dates, etc.
|
|
|
|
- grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
|
|
"""
|
|
dom=content_parser.course_file(student)
|
|
course = dom.xpath('//course/@name')[0]
|
|
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
|
|
|
|
responses=StudentModule.objects.filter(student=student)
|
|
response_by_id = {}
|
|
for response in responses:
|
|
response_by_id[response.module_id] = response
|
|
|
|
|
|
totaled_scores = {}
|
|
chapters=[]
|
|
for c in xmlChapters:
|
|
sections = []
|
|
chname=c.get('name')
|
|
|
|
|
|
for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section',
|
|
course=course, chname=chname):
|
|
problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem',
|
|
course=course, chname=chname, section=s.get('name'))
|
|
|
|
graded = True if s.get('graded') == "true" else False
|
|
scores=[]
|
|
if len(problems)>0:
|
|
for p in problems:
|
|
(correct,total) = get_score(student, p, response_by_id)
|
|
|
|
if settings.GENERATE_PROFILE_SCORES:
|
|
if total > 1:
|
|
correct = random.randrange( max(total-2, 1) , total + 1 )
|
|
else:
|
|
correct = total
|
|
|
|
if not total > 0:
|
|
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
|
|
graded = False
|
|
scores.append( Score(correct,total, graded, p.get("name")) )
|
|
|
|
section_total, graded_total = aggregate_scores(scores, s.get("name"))
|
|
#Add the graded total to totaled_scores
|
|
format = s.get('format', "")
|
|
subtitle = s.get('subtitle', format)
|
|
if format and graded_total[1] > 0:
|
|
format_scores = totaled_scores.get(format, [])
|
|
format_scores.append( graded_total )
|
|
totaled_scores[ format ] = format_scores
|
|
|
|
section_score={'section':s.get("name"),
|
|
'scores':scores,
|
|
'section_total' : section_total,
|
|
'format' : format,
|
|
'subtitle' : subtitle,
|
|
'due' : s.get("due") or "",
|
|
'graded' : graded,
|
|
}
|
|
sections.append(section_score)
|
|
|
|
chapters.append({'course':course,
|
|
'chapter' : c.get("name"),
|
|
'sections' : sections,})
|
|
|
|
|
|
grader = course_settings.GRADER
|
|
grade_summary = grader.grade(totaled_scores)
|
|
|
|
return {'courseware_summary' : chapters,
|
|
'grade_summary' : grade_summary}
|
|
|
|
def aggregate_scores(scores, section_name = "summary"):
|
|
total_correct_graded = sum(score.earned for score in scores if score.graded)
|
|
total_possible_graded = sum(score.possible for score in scores if score.graded)
|
|
|
|
total_correct = sum(score.earned for score in scores)
|
|
total_possible = sum(score.possible for score in scores)
|
|
|
|
#regardless of whether or not it is graded
|
|
all_total = Score(total_correct,
|
|
total_possible,
|
|
False,
|
|
section_name)
|
|
#selecting only graded things
|
|
graded_total = Score(total_correct_graded,
|
|
total_possible_graded,
|
|
True,
|
|
section_name)
|
|
|
|
return all_total, graded_total
|
|
|
|
|
|
def get_score(user, problem, cache):
|
|
## HACK: assumes max score is fixed per problem
|
|
id = problem.get('id')
|
|
correct = 0.0
|
|
|
|
# If the ID is not in the cache, add the item
|
|
if id not in cache:
|
|
module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__?
|
|
module_id = id,
|
|
student = user,
|
|
state = None,
|
|
grade = 0,
|
|
max_grade = None,
|
|
done = 'i')
|
|
cache[id] = module
|
|
|
|
# Grab the # correct from cache
|
|
if id in cache:
|
|
response = cache[id]
|
|
if response.grade!=None:
|
|
correct=float(response.grade)
|
|
|
|
# Grab max grade from cache, or if it doesn't exist, compute and save to DB
|
|
if id in cache and response.max_grade != None:
|
|
total = response.max_grade
|
|
else:
|
|
## HACK 1: We shouldn't specifically reference capa_module
|
|
## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system
|
|
from module_render import I4xSystem
|
|
system = I4xSystem(None, None, None)
|
|
total=float(courseware.modules.capa_module.Module(system, etree.tostring(problem), "id").max_score())
|
|
response.max_grade = total
|
|
response.save()
|
|
|
|
#Now we re-weight the problem, if specified
|
|
weight = problem.get("weight", None)
|
|
if weight:
|
|
weight = float(weight)
|
|
correct = correct * weight / total
|
|
total = weight
|
|
|
|
return (correct, total)
|