diff --git a/djangoapps/courseware/capa/capa_problem.py b/djangoapps/courseware/capa/capa_problem.py index f5739fd8b0..0f9b5dacab 100644 --- a/djangoapps/courseware/capa/capa_problem.py +++ b/djangoapps/courseware/capa/capa_problem.py @@ -73,20 +73,22 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul # removed in MC ## These should be transformed -#html_special_response = {"textline":textline.render, -# "schematic":schematic.render, -# "textbox":textbox.render, -# "solution":solution.render, +#html_special_response = {"textline":inputtypes.textline.render, +# "schematic":inputtypes.schematic.render, +# "textbox":inputtypes.textbox.render, +# "formulainput":inputtypes.jstextline.render, +# "solution":inputtypes.solution.render, # } class LoncapaProblem(object): - def __init__(self, fileobject, id, state=None, seed=None): + def __init__(self, fileobject, id, state=None, seed=None, system=None): ## Initialize class variables from state self.seed = None self.student_answers = dict() self.correct_map = dict() self.done = False self.problem_id = id + self.system = system if seed != None: self.seed = seed @@ -117,7 +119,7 @@ class LoncapaProblem(object): self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers) self.context = self.extract_context(self.tree, seed=self.seed) for response in self.tree.xpath('//'+"|//".join(response_types)): - responder = response_types[response.tag](response, self.context) + responder = response_types[response.tag](response, self.context, self.system) responder.preprocess_response() def get_state(self): @@ -163,7 +165,7 @@ class LoncapaProblem(object): self.correct_map = dict() problems_simple = self.extract_problems(self.tree) for response in problems_simple: - grader = response_types[response.tag](response, self.context) + grader = response_types[response.tag](response, self.context, self.system) results = grader.grade(answers) # call the responsetype instance to do the actual grading self.correct_map.update(results) return self.correct_map @@ -177,7 +179,7 @@ class LoncapaProblem(object): answer_map = dict() problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries for response in problems_simple: - responder = response_types[response.tag](response, self.context) # instance of numericalresponse, customresponse,... + responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,... results = responder.get_answers() answer_map.update(results) # dict of (id,correct_answer) diff --git a/djangoapps/courseware/capa/inputtypes.py b/djangoapps/courseware/capa/inputtypes.py index 0388b35d0b..ae8809c066 100644 --- a/djangoapps/courseware/capa/inputtypes.py +++ b/djangoapps/courseware/capa/inputtypes.py @@ -101,6 +101,11 @@ def textline(element, value, state, msg=""): #----------------------------------------------------------------------------- def js_textline(element, value, status, msg=''): + ''' + Plan: We will inspect element to figure out type + ''' + # TODO: Make a wrapper for + # TODO: Make an AJAX loop to confirm equation is okay in real-time as user types ## TODO: Code should follow PEP8 (4 spaces per indentation level) ''' textline is used for simple one-line inputs, like formularesponse and symbolicresponse. @@ -147,7 +152,7 @@ def schematic(element, value, status, msg=''): 'id':eid, 'value':value, 'initial_value':initial_value, - 'state':state, + 'state':status, 'width':width, 'height':height, 'parts':parts, diff --git a/djangoapps/courseware/capa/responsetypes.py b/djangoapps/courseware/capa/responsetypes.py index d9b18428fe..190f52e7b6 100644 --- a/djangoapps/courseware/capa/responsetypes.py +++ b/djangoapps/courseware/capa/responsetypes.py @@ -71,17 +71,17 @@ class MultipleChoiceResponse(GenericResponse): - `a+b`
- a+b^2
- a+b+c - a+b+d + `a+b`
+ a+b^2
+ a+b+c + a+b+d
TODO: handle direction and randomize ''' - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) @@ -155,7 +155,7 @@ class OptionResponse(GenericResponse): TODO: handle direction and randomize ''' - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.answer_fields = xml.findall('optioninput') if settings.DEBUG: @@ -179,7 +179,7 @@ class OptionResponse(GenericResponse): #----------------------------------------------------------------------------- class NumericalResponse(GenericResponse): - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.correct_answer = contextualize_text(xml.get('answer'), context) try: @@ -257,7 +257,7 @@ def sympy_check2(): ''' - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml ## CRITICAL TODO: Should cover all entrytypes ## NOTE: xpath will look at root of XML tree, not just @@ -412,7 +412,7 @@ class ExternalResponse(GenericResponse): Typically used by coding problems. """ - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id', id=xml.get('id')) @@ -472,7 +472,7 @@ class StudentInputError(Exception): #----------------------------------------------------------------------------- class FormulaResponse(GenericResponse): - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.correct_answer = contextualize_text(xml.get('answer'), context) self.samples = contextualize_text(xml.get('samples'), context) @@ -553,7 +553,7 @@ class FormulaResponse(GenericResponse): #----------------------------------------------------------------------------- class SchematicResponse(GenericResponse): - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id', id=xml.get('id')) @@ -562,7 +562,7 @@ class SchematicResponse(GenericResponse): id=xml.get('id'))[0] answer_src = answer.get('src') if answer_src != None: - self.code = open(settings.DATA_DIR+'src/'+answer_src).read() + self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used else: self.code = answer.text @@ -599,7 +599,7 @@ class ImageResponse(GenericResponse): """ - def __init__(self, xml, context): + def __init__(self, xml, context, system=None): self.xml = xml self.context = context self.ielements = xml.findall('imageinput') diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py index 0e317f7004..210cddb0b2 100644 --- a/djangoapps/courseware/module_render.py +++ b/djangoapps/courseware/module_render.py @@ -15,7 +15,6 @@ from mitxmako.shortcuts import render_to_string from models import StudentModule -import track.views import courseware.modules @@ -56,6 +55,8 @@ def make_track_function(request): tracking function to them. This generates a closure for each request that gives a clean interface on both sides. ''' + import track.views + def f(event_type, event): return track.views.server_track(request, event_type, event, page='x_module') return f diff --git a/djangoapps/courseware/modules/__init__.py b/djangoapps/courseware/modules/__init__.py index b20ce3778c..4fc5fb67cb 100644 --- a/djangoapps/courseware/modules/__init__.py +++ b/djangoapps/courseware/modules/__init__.py @@ -1,8 +1,6 @@ import os import os.path -from django.conf import settings - import capa_module import html_module import schematic_module diff --git a/djangoapps/courseware/modules/capa_module.py b/djangoapps/courseware/modules/capa_module.py index 0b75faa491..7d42cfb250 100644 --- a/djangoapps/courseware/modules/capa_module.py +++ b/djangoapps/courseware/modules/capa_module.py @@ -142,7 +142,8 @@ class Module(XModule): dom2 = etree.fromstring(xml) - self.explanation=content_parser.item(dom2.xpath('/problem/@explain'), default="closed") + self.explanation="problems/"+content_parser.item(dom2.xpath('/problem/@explain'), default="closed") + # TODO: Should be converted to: self.explanation=content_parser.item(dom2.xpath('/problem/@explain'), default="closed") self.explain_available=content_parser.item(dom2.xpath('/problem/@explain_available')) display_due_date_string=content_parser.item(dom2.xpath('/problem/@due')) @@ -188,7 +189,8 @@ class Module(XModule): if state!=None and 'attempts' in state: self.attempts=state['attempts'] - self.filename="problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml" + # TODO: Should be: self.filename=content_parser.item(dom2.xpath('/problem/@filename')) + self.filename= "problems/"+content_parser.item(dom2.xpath('/problem/@filename'))+".xml" self.name=content_parser.item(dom2.xpath('/problem/@name')) self.weight=content_parser.item(dom2.xpath('/problem/@weight')) if self.rerandomize == 'never': @@ -200,7 +202,7 @@ class Module(XModule): except Exception,err: print '[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename) raise Exception,err - self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed) + self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system) def handle_ajax(self, dispatch, get): ''' @@ -307,11 +309,11 @@ class Module(XModule): lcp_id = self.lcp.problem_id correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: - self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state) + self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return json.dumps({'success':inst.message}) except: - self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state) + self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() raise Exception,"error in capa_module" return json.dumps({'success':'Unknown Error'}) @@ -388,7 +390,7 @@ class Module(XModule): self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. self.lcp.seed=None - self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state()) + self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system) event_info['new_state']=self.lcp.get_state() self.tracker('reset_problem', event_info) diff --git a/djangoapps/courseware/modules/template_module.py b/djangoapps/courseware/modules/template_module.py index 41556eb9d4..a8899f985c 100644 --- a/djangoapps/courseware/modules/template_module.py +++ b/djangoapps/courseware/modules/template_module.py @@ -1,8 +1,6 @@ import json import os -## TODO: Abstract out from Django -from django.conf import settings from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule @@ -14,8 +12,14 @@ class Module(XModule): @classmethod def get_xml_tags(c): - ## TODO: Abstract out from filesystem - tags = os.listdir(settings.DATA_DIR+'/custom_tags') + ## TODO: Abstract out from filesystem and Django + ## HACK: For now, this lets us import without abstracting out + try: + from django.conf import settings + tags = os.listdir(settings.DATA_DIR+'/custom_tags') + except: + print "Could not open tags directory." + tags = [] return tags def get_html(self): diff --git a/djangoapps/courseware/modules/x_module.py b/djangoapps/courseware/modules/x_module.py index 9e70a16891..0898ac4f8f 100644 --- a/djangoapps/courseware/modules/x_module.py +++ b/djangoapps/courseware/modules/x_module.py @@ -7,6 +7,8 @@ class XModule(object): ''' Implements a generic learning module. Initialized on access with __init__, first time with state=None, and then with state + + See the HTML module for a simple example ''' id_attribute='id' # An attribute guaranteed to be unique @@ -16,18 +18,31 @@ class XModule(object): return [] def get_completion(self): + ''' This is mostly unimplemented. + It gives a progress indication -- e.g. 30 minutes of 1.5 hours watched. 3 of 5 problems done, etc. ''' return courseware.progress.completion() def get_state(self): + ''' State of the object, as stored in the database + ''' return "" def get_score(self): + ''' Score the student received on the problem. + ''' return None def max_score(self): + ''' Maximum score. Two notes: + * This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another + * In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code + should get fixed), and (b) break some analytics we plan to put in place. + ''' return None def get_html(self): + ''' HTML, as shown in the browser. This is the only method that must be implemented + ''' return "Unimplemented" def get_init_js(self): @@ -38,6 +53,9 @@ class XModule(object): return "" def get_destroy_js(self): + ''' JavaScript called to destroy the problem (e.g. when a user switches to a different tab). + We make an attempt, but not a promise, to call this when the user closes the web page. + ''' return "" def handle_ajax(self, dispatch, get): diff --git a/djangoapps/courseware/tests.py b/djangoapps/courseware/tests.py index 682927efb7..fabb79e4e7 100644 --- a/djangoapps/courseware/tests.py +++ b/djangoapps/courseware/tests.py @@ -10,6 +10,24 @@ import courseware.graders as graders from courseware.graders import Score, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader from courseware.grades import aggregate_scores +class I4xSystem(object): + ''' + This is an abstraction such that x_modules can function independent + of the courseware (e.g. import into other types of courseware, LMS, + or if we want to have a sandbox server for user-contributed content) + ''' + def __init__(self): + self.ajax_url = '/' + self.track_function = lambda x: None + self.render_function = lambda x: {} # Probably incorrect + self.exception404 = Exception + def __repr__(self): + return repr(self.__dict__) + def __str__(self): + return str(self.__dict__) + +i4xs = I4xSystem + class ModelsTest(unittest.TestCase): def setUp(self): pass @@ -69,7 +87,7 @@ class ModelsTest(unittest.TestCase): class MultiChoiceTest(unittest.TestCase): def test_MC_grade(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1') + test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) correct_answers = {'1_2_1':'choice_foil3'} self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') false_answers = {'1_2_1':'choice_foil2'} @@ -77,7 +95,7 @@ class MultiChoiceTest(unittest.TestCase): def test_MC_bare_grades(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1') + test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) correct_answers = {'1_2_1':'choice_2'} self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') false_answers = {'1_2_1':'choice_1'} @@ -85,7 +103,7 @@ class MultiChoiceTest(unittest.TestCase): def test_TF_grade(self): truefalse_file = os.getcwd()+"/djangoapps/courseware/test_files/truefalse.xml" - test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1') + test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1', system=i4xs) correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']} self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') false_answers = {'1_2_1':['choice_foil1']} @@ -100,7 +118,7 @@ class MultiChoiceTest(unittest.TestCase): class ImageResponseTest(unittest.TestCase): def test_ir_grade(self): imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml" - test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1') + test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1', system=i4xs) correct_answers = {'1_2_1':'(490,11)-(556,98)', '1_2_2':'(242,202)-(296,276)'} test_answers = {'1_2_1':'[500,20]', @@ -117,7 +135,7 @@ class OptionResponseTest(unittest.TestCase): ''' def test_or_grade(self): optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml" - test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1') + test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1', system=i4xs) correct_answers = {'1_2_1':'True', '1_2_2':'False'} test_answers = {'1_2_1':'True', diff --git a/sass/_info.scss b/sass/_info.scss index a4629c9e03..7fcdcc8d10 100644 --- a/sass/_info.scss +++ b/sass/_info.scss @@ -20,6 +20,7 @@ div.info-wrapper { border-bottom: 1px solid #e3e3e3; margin-bottom: lh(.5); padding-bottom: lh(.5); + list-style-type: disk; &:first-child { background: $cream; @@ -28,6 +29,11 @@ div.info-wrapper { padding: lh(.5); } + ol, ul { + margin: lh() 0 0 lh(); + list-style-type: circle; + } + h2 { float: left; margin: 0 flex-gutter() 0 0; diff --git a/sass/discussion/_answers.scss b/sass/discussion/_answers.scss index a65c147c0c..f0de650206 100644 --- a/sass/discussion/_answers.scss +++ b/sass/discussion/_answers.scss @@ -1,3 +1,5 @@ +// Styles for individual answers + div.answer-controls { @include box-sizing(border-box); display: inline-block; diff --git a/sass/discussion/_askbot-original.scss b/sass/discussion/_askbot-original.scss index 7c0e684f71..09db42ce4e 100644 --- a/sass/discussion/_askbot-original.scss +++ b/sass/discussion/_askbot-original.scss @@ -1,4 +1,4 @@ -// original styles +// original Askbot styles // body { // background: #fff; diff --git a/sass/discussion/_badges.scss b/sass/discussion/_badges.scss index 2b809327ed..d74dd93d13 100644 --- a/sass/discussion/_badges.scss +++ b/sass/discussion/_badges.scss @@ -1,3 +1,5 @@ +// Style for the user badge list (can be accessed by clicking "View all MIT badges" in the badge section of the Askbot user profile + div.badges-intro { margin: 20px 0; } diff --git a/sass/discussion/_discussion.scss b/sass/discussion/_discussion.scss index 12863766cc..b9022a43d8 100644 --- a/sass/discussion/_discussion.scss +++ b/sass/discussion/_discussion.scss @@ -1,4 +1,5 @@ -// Layout +// Generic layout styles for the discussion forums + body.askbot { section.main-content { diff --git a/sass/discussion/_form-wmd-toolbar.scss b/sass/discussion/_form-wmd-toolbar.scss index 0445fcf546..7d9b81c1ff 100644 --- a/sass/discussion/_form-wmd-toolbar.scss +++ b/sass/discussion/_form-wmd-toolbar.scss @@ -1,3 +1,5 @@ +// Styles for the WYSIWYG question/answer editor + .wmd-panel { } diff --git a/sass/discussion/_forms.scss b/sass/discussion/_forms.scss index ab14bc1137..3d484729b1 100644 --- a/sass/discussion/_forms.scss +++ b/sass/discussion/_forms.scss @@ -1,3 +1,5 @@ +// Styles for different forms in the system + form.answer-form { @include box-sizing(border-box); border-top: 1px solid #ddd; diff --git a/sass/discussion/_modals.scss b/sass/discussion/_modals.scss index a26fee54d0..5a7e6db1e5 100644 --- a/sass/discussion/_modals.scss +++ b/sass/discussion/_modals.scss @@ -1,3 +1,5 @@ +// Style for modal boxes that pop up to notify the user of various events + .vote-notification { background-color: darken($mit-red, 7%); @include border-radius(4px); diff --git a/sass/discussion/_profile.scss b/sass/discussion/_profile.scss index 13c5ae380c..42e6b772f8 100644 --- a/sass/discussion/_profile.scss +++ b/sass/discussion/_profile.scss @@ -1,3 +1,5 @@ +// Style for the user profile view + body.user-profile-page { section.questions { diff --git a/sass/discussion/_question-view.scss b/sass/discussion/_question-view.scss index 72408f0ff8..4b7765b2f9 100644 --- a/sass/discussion/_question-view.scss +++ b/sass/discussion/_question-view.scss @@ -1,3 +1,5 @@ +// Styles for the single question view + div.question-header { div.official-stamp { diff --git a/sass/discussion/_questions.scss b/sass/discussion/_questions.scss index 26bb270d2d..4f855cd092 100644 --- a/sass/discussion/_questions.scss +++ b/sass/discussion/_questions.scss @@ -1,3 +1,5 @@ +// Styles for the default question list view + div.question-list-header { display: block; margin-bottom: 0px; diff --git a/sass/discussion/_sidebar.scss b/sass/discussion/_sidebar.scss index ea07f6af11..5ff8ce2c55 100644 --- a/sass/discussion/_sidebar.scss +++ b/sass/discussion/_sidebar.scss @@ -1,3 +1,5 @@ +// Styles for the Askbot sidebar + div.discussion-wrapper aside { @extend .sidebar; border-left: 1px solid #d3d3d3; diff --git a/sass/discussion/_tags.scss b/sass/discussion/_tags.scss index 72c4e9ffc4..a8d4d0f034 100644 --- a/sass/discussion/_tags.scss +++ b/sass/discussion/_tags.scss @@ -1,3 +1,5 @@ +// Styles for the question tags + ul.tags { list-style: none; display: inline;