From f2309b3112703088aa3d5f98a0cceb5783daac65 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Jun 2012 15:18:49 -0400 Subject: [PATCH 1/5] Remove references to djangosettings from responsetypes.py --- common/lib/capa/checker.py | 1 + common/lib/capa/responsetypes.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/checker.py b/common/lib/capa/checker.py index 742d28766b..2139035d2a 100755 --- a/common/lib/capa/checker.py +++ b/common/lib/capa/checker.py @@ -23,6 +23,7 @@ log = logging.getLogger('capa.checker') class DemoSystem(object): def __init__(self): self.lookup = TemplateLookup(directories=[path(__file__).dirname() / 'templates']) + self.DEBUG = True def render_template(self, template_filename, dictionary, context=None): if context is None: diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index 955d2553a1..c5683bb0bf 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -277,14 +277,14 @@ def sympy_check2(): print "can't find cfn in context = ",context if not self.code: - if not answer: + if answer is None: # raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid self.code = '' else: answer_src = answer.get('src') if answer_src is not None: - self.code = open(settings.DATA_DIR+'src/'+answer_src).read() + self.code = self.system.filesystem.open('src/'+answer_src).read() else: self.code = answer.text @@ -329,8 +329,7 @@ def sympy_check2(): }) # pass self.system.debug to cfn - # if hasattr(self.system,'debug'): self.context['debug'] = self.system.debug - self.context['debug'] = settings.DEBUG + self.context['debug'] = self.system.DEBUG # exec the check function if type(self.code)==str: @@ -492,7 +491,7 @@ main() answer_src = answer.get('src') if answer_src is not None: - self.code = open(settings.DATA_DIR+'src/'+answer_src).read() + self.code = self.system.filesystem.open('src/'+answer_src).read() else: self.code = answer.text @@ -522,7 +521,7 @@ main() log.error(msg) raise Exception, msg - if settings.DEBUG: log.info('response = %s' % r.text) + if self.system.DEBUG: log.info('response = %s' % r.text) if (not r.text ) or (not r.text.strip()): raise Exception,'Error: no response from external server url=%s' % self.url @@ -551,7 +550,7 @@ main() rxml = self.do_external_request('get_score',extra_payload) except Exception, err: log.error('Error %s' % err) - if settings.DEBUG: + if self.system.DEBUG: correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) )) correct_map['msg_%s' % self.answer_ids[0]] = '%s' % str(err).replace('<','<') return correct_map @@ -581,7 +580,7 @@ main() exans = json.loads(rxml.find('expected').text) except Exception,err: log.error('Error %s' % err) - if settings.DEBUG: + if self.system.DEBUG: msg = '%s' % str(err).replace('<','<') exans = [''] * len(self.answer_ids) exans[0] = msg From f78be58141e01c9a93b9292962872f70882e8dc6 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Jun 2012 16:29:23 -0400 Subject: [PATCH 2/5] Cleaning up pep8 issues, including extraneous imports --- common/lib/capa/capa_problem.py | 250 ++++++++++++++++---------------- 1 file changed, 123 insertions(+), 127 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index 4b40df6653..b655270a9a 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -12,7 +12,6 @@ import logging import math import numpy import os -import os.path import random import re import scipy @@ -20,58 +19,58 @@ import struct from lxml import etree from lxml.etree import Element -from xml.sax.saxutils import escape, unescape +from xml.sax.saxutils import unescape from util import contextualize_text import inputtypes -from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse, SymbolicResponse +from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse import calc import eia log = logging.getLogger(__name__) -response_types = {'numericalresponse':NumericalResponse, - 'formularesponse':FormulaResponse, - 'customresponse':CustomResponse, - 'schematicresponse':SchematicResponse, - 'externalresponse':ExternalResponse, - 'multiplechoiceresponse':MultipleChoiceResponse, - 'truefalseresponse':TrueFalseResponse, - 'imageresponse':ImageResponse, - 'optionresponse':OptionResponse, - 'symbolicresponse':SymbolicResponse, +response_types = {'numericalresponse': NumericalResponse, + 'formularesponse': FormulaResponse, + 'customresponse': CustomResponse, + 'schematicresponse': SchematicResponse, + 'externalresponse': ExternalResponse, + 'multiplechoiceresponse': MultipleChoiceResponse, + 'truefalseresponse': TrueFalseResponse, + 'imageresponse': ImageResponse, + 'optionresponse': OptionResponse, + 'symbolicresponse': SymbolicResponse, } -entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput'] -solution_types = ['solution'] # extra things displayed after "show answers" is pressed -response_properties = ["responseparam", "answer"] # these get captured as student responses +entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput'] +solution_types = ['solution'] # extra things displayed after "show answers" is pressed +response_properties = ["responseparam", "answer"] # these get captured as student responses # How to convert from original XML to HTML # We should do this with xlst later -html_transforms = {'problem': {'tag':'div'}, - "numericalresponse": {'tag':'span'}, - "customresponse": {'tag':'span'}, - "externalresponse": {'tag':'span'}, - "schematicresponse": {'tag':'span'}, - "formularesponse": {'tag':'span'}, - "symbolicresponse": {'tag':'span'}, - "multiplechoiceresponse": {'tag':'span'}, - "text": {'tag':'span'}, - "math": {'tag':'span'}, +html_transforms = {'problem': {'tag': 'div'}, + "numericalresponse": {'tag': 'span'}, + "customresponse": {'tag': 'span'}, + "externalresponse": {'tag': 'span'}, + "schematicresponse": {'tag': 'span'}, + "formularesponse": {'tag': 'span'}, + "symbolicresponse": {'tag': 'span'}, + "multiplechoiceresponse": {'tag': 'span'}, + "text": {'tag': 'span'}, + "math": {'tag': 'span'}, } -global_context={'random':random, - 'numpy':numpy, - 'math':math, - 'scipy':scipy, - 'calc':calc, - 'eia':eia} +global_context = {'random': random, + 'numpy': numpy, + 'math': math, + 'scipy': scipy, + 'calc': calc, + 'eia': eia} # These should be removed from HTML output, including all subelements html_problem_semantics = ["responseparam", "answer", "script"] # These should be removed from HTML output, but keeping subelements -html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse",'symbolicresponse'] +html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text", "externalresponse", 'symbolicresponse'] # removed in MC ## These should be transformed @@ -82,6 +81,7 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul # "solution":inputtypes.solution.render, # } + class LoncapaProblem(object): def __init__(self, fileobject, id, state=None, seed=None, system=None): ## Initialize class variables from state @@ -107,22 +107,22 @@ class LoncapaProblem(object): # TODO: Does this deplete the Linux entropy pool? Is this fast enough? if not self.seed: - self.seed=struct.unpack('i', os.urandom(4))[0] + self.seed = struct.unpack('i', os.urandom(4))[0] ## Parse XML file - if getattr(system,'DEBUG',False): + if getattr(system, 'DEBUG', False): log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject) file_text = fileobject.read() - self.fileobject = fileobject # save it, so we can use for debugging information later + self.fileobject = fileobject # save it, so we can use for debugging information later # Convert startouttext and endouttext to proper # TODO: Do with XML operations - file_text = re.sub("startouttext\s*/","text",file_text) - file_text = re.sub("endouttext\s*/","/text",file_text) + file_text = re.sub("startouttext\s*/", "text", file_text) + file_text = re.sub("endouttext\s*/", "/text", file_text) self.tree = etree.XML(file_text) - self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers) + 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)): + for response in self.tree.xpath('//' + "|//".join(response_types)): responder = response_types[response.tag](response, self.context, self.system) responder.preprocess_response() @@ -130,34 +130,34 @@ class LoncapaProblem(object): return u"LoncapaProblem ({0})".format(self.fileobject) def get_state(self): - ''' Stored per-user session data neeeded to: + ''' Stored per-user session data neeeded to: 1) Recreate the problem 2) Populate any student answers. ''' - return {'seed':self.seed, - 'student_answers':self.student_answers, - 'correct_map':self.correct_map, - 'done':self.done} + return {'seed': self.seed, + 'student_answers': self.student_answers, + 'correct_map': self.correct_map, + 'done': self.done} def get_max_score(self): ''' TODO: multiple points for programming problems. ''' - sum = 0 - for et in entry_types: - sum = sum + self.tree.xpath('count(//'+et+')') + sum = 0 + for et in entry_types: + sum = sum + self.tree.xpath('count(//' + et + ')') return int(sum) def get_score(self): - correct=0 + correct = 0 for key in self.correct_map: if self.correct_map[key] == u'correct': correct += 1 - if (not self.student_answers) or len(self.student_answers)==0: - return {'score':0, - 'total':self.get_max_score()} + if (not self.student_answers) or len(self.student_answers) == 0: + return {'score': 0, + 'total': self.get_max_score()} else: - return {'score':correct, - 'total':self.get_max_score()} + return {'score': correct, + 'total': self.get_max_score()} def grade_answers(self, answers): ''' @@ -168,38 +168,36 @@ class LoncapaProblem(object): Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123 ''' self.student_answers = answers - context=self.extract_context(self.tree) self.correct_map = dict() problems_simple = self.extract_problems(self.tree) for response in problems_simple: grader = response_types[response.tag](response, self.context, self.system) - results = grader.get_score(answers) # call the responsetype instance to do the actual grading + results = grader.get_score(answers) # call the responsetype instance to do the actual grading self.correct_map.update(results) return self.correct_map def get_question_answers(self): """Returns a dict of answer_ids to answer values. If we can't generate - an answer (this sometimes happens in customresponses), that answer_id is - not included. Called by "show answers" button JSON request + an answer (this sometimes happens in customresponses), that answer_id is + not included. Called by "show answers" button JSON request (see capa_module) """ - context=self.extract_context(self.tree) answer_map = dict() - problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries + 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, self.system) # 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) + answer_map.update(results) # dict of (id,correct_answer) # example for the following: - for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)): - answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline + for entry in problems_simple.xpath("//" + "|//".join(response_properties + entry_types)): + answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline if answer: answer_map[entry.get('id')] = contextualize_text(answer, self.context) # include solutions from ... stanzas # Tentative merge; we should figure out how we want to handle hints and solutions - for entry in self.tree.xpath("//"+"|//".join(solution_types)): + for entry in self.tree.xpath("//" + "|//".join(solution_types)): answer = etree.tostring(entry) if answer: answer_map[entry.get('id')] = answer @@ -207,11 +205,10 @@ class LoncapaProblem(object): return answer_map def get_answer_ids(self): - """Return the IDs of all the responses -- these are the keys used for - the dicts returned by grade_answers and get_question_answers. (Though + """Return the IDs of all the responses -- these are the keys used for + the dicts returned by grade_answers and get_question_answers. (Though get_question_answers may only return a subset of these.""" answer_ids = [] - context=self.extract_context(self.tree) problems_simple = self.extract_problems(self.tree) for response in problems_simple: responder = response_types[response.tag](response, self.context) @@ -223,35 +220,35 @@ class LoncapaProblem(object): return answer_ids - # ======= Private ======== - - def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private + def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private ''' Extract content of from the problem.xml file, and exec it in the context of this problem. Provides ability to randomize problems, and also set variables for problem answer checking. - + Problem XML goes to Python execution context. Runs everything in script tags ''' random.seed(self.seed) - context = {'global_context':global_context} # save global context in here also - context.update(global_context) # initialize context to have stuff in global_context - context['__builtins__'] = globals()['__builtins__'] # put globals there also - context['the_lcp'] = self # pass instance of LoncapaProblem in + context = {'global_context': global_context} # save global context in here also + context.update(global_context) # initialize context to have stuff in global_context + context['__builtins__'] = globals()['__builtins__'] # put globals there also + context['the_lcp'] = self # pass instance of LoncapaProblem in #for script in tree.xpath('/problem/script'): for script in tree.findall('.//script'): stype = script.get('type') if stype: - if 'javascript' in stype: continue # skip javascript - if 'perl' in stype: continue # skip perl - # TODO: evaluate only python + if 'javascript' in stype: + continue # skip javascript + if 'perl' in stype: + continue # skip perl + # TODO: evaluate only python code = script.text XMLESC = {"'": "'", """: '"'} - code = unescape(code,XMLESC) + code = unescape(code, XMLESC) try: - exec code in context, context # use "context" for global context; thus defs in code are global within code + exec code in context, context # use "context" for global context; thus defs in code are global within code except Exception: log.exception("Error while execing code: " + code) return context @@ -265,11 +262,11 @@ class LoncapaProblem(object): if problemtree.tag in html_problem_semantics: return - problemid = problemtree.get('id') # my ID - + problemid = problemtree.get('id') # my ID + # used to be # if problemtree.tag in html_special_response: - + if problemtree.tag in inputtypes.get_input_xml_tags(): # status is currently the answer for the problem ID for the input element, # but it will turn into a dict containing both the answer and any associated message @@ -283,7 +280,7 @@ class LoncapaProblem(object): value = self.student_answers[problemid] #### This code is a hack. It was merged to help bring two branches - #### in sync, but should be replaced. msg should be passed in a + #### in sync, but should be replaced. msg should be passed in a #### response_type # prepare the response message, if it exists in correct_map if 'msg' in self.correct_map: @@ -296,112 +293,111 @@ class LoncapaProblem(object): # do the rendering # This should be broken out into a helper function # that handles all input objects - render_object = inputtypes.SimpleInput(system = self.system, - xml = problemtree, - state = {'value':value, - 'status': status, - 'id':problemtree.get('id'), - 'feedback':{'message':msg} - }, - use = 'capa_input') - return render_object.get_html() #function(problemtree, value, status, msg) # render the special response (textline, schematic,...) + render_object = inputtypes.SimpleInput(system=self.system, + xml=problemtree, + state={'value': value, + 'status': status, + 'id': problemtree.get('id'), + 'feedback': {'message': msg} + }, + use='capa_input') + return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...) - tree=Element(problemtree.tag) + tree = Element(problemtree.tag) for item in problemtree: subitems = self.extract_html(item) if subitems is not None: for subitem in subitems: tree.append(subitem) - for (key,value) in problemtree.items(): + for (key, value) in problemtree.items(): tree.set(key, value) - tree.text=problemtree.text - tree.tail=problemtree.tail + tree.text = problemtree.text + tree.tail = problemtree.tail if problemtree.tag in html_transforms: - tree.tag=html_transforms[problemtree.tag]['tag'] + tree.tag = html_transforms[problemtree.tag]['tag'] # Reset attributes. Otherwise, we get metadata in HTML - # (e.g. answers) + # (e.g. answers) # TODO: We should remove and not zero them. # I'm not sure how to do that quickly with lxml for k in tree.keys(): - tree.set(k,"") + tree.set(k, "") # TODO: Fix. This loses Element().tail #if problemtree.tag in html_skip: # return tree return [tree] - def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private + def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private ''' - Assign IDs to all the responses + Assign IDs to all the responses Assign sub-IDs to all entries (textline, schematic, etc.) Annoted correctness and value In-place transformation ''' response_id = 1 - for response in tree.xpath('//'+"|//".join(response_types)): - response_id_str=self.problem_id+"_"+str(response_id) - response.attrib['id']=response_id_str + for response in tree.xpath('//' + "|//".join(response_types)): + response_id_str = self.problem_id + "_" + str(response_id) + response.attrib['id'] = response_id_str if response_id not in correct_map: correct = 'unsubmitted' response.attrib['state'] = correct response_id = response_id + 1 answer_id = 1 - for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in (entry_types + solution_types)]), + for entry in tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), id=response_id_str): - # assign one answer_id for each entry_type or solution_type + # assign one answer_id for each entry_type or solution_type entry.attrib['response_id'] = str(response_id) entry.attrib['answer_id'] = str(answer_id) - entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id) - answer_id=answer_id+1 + entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) + answer_id = answer_id + 1 # ... may not be associated with any specific response; give IDs for those separately - # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). + # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). solution_id = 1 for solution in tree.findall('.//solution'): - solution.attrib['id'] = "%s_solution_%i"%(self.problem_id, solution_id) + solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id) solution_id += 1 def extract_problems(self, problem_tree): ''' Remove layout from the problem, and give a purified XML tree of just the problems ''' - problem_tree=copy.deepcopy(problem_tree) - tree=Element('problem') - for response in problem_tree.xpath("//"+"|//".join(response_types)): + problem_tree = copy.deepcopy(problem_tree) + tree = Element('problem') + for response in problem_tree.xpath("//" + "|//".join(response_types)): newresponse = copy.copy(response) - for e in newresponse: + for e in newresponse: newresponse.remove(e) # copy.copy is needed to make xpath work right. Otherwise, it starts at the root # of the tree. We should figure out if there's some work-around - for e in copy.copy(response).xpath("//"+"|//".join(response_properties+entry_types)): + for e in copy.copy(response).xpath("//" + "|//".join(response_properties + entry_types)): newresponse.append(e) - + tree.append(newresponse) return tree -if __name__=='__main__': - problem_id='simpleFormula' +if __name__ == '__main__': + problem_id = 'simpleFormula' filename = 'simpleFormula.xml' - problem_id='resistor' + problem_id = 'resistor' filename = 'resistor.xml' - lcp = LoncapaProblem(filename, problem_id) - + context = lcp.extract_context(lcp.tree) problem = lcp.extract_problems(lcp.tree) - print lcp.grade_problems({'resistor_2_1':'1.0','resistor_3_1':'2.0'}) + print lcp.grade_problems({'resistor_2_1': '1.0', 'resistor_3_1': '2.0'}) #print lcp.grade_problems({'simpleFormula_2_1':'3*x^3'}) #numericalresponse(problem, context) - + #print etree.tostring((lcp.tree)) print '============' print #print etree.tostring(lcp.extract_problems(lcp.tree)) print lcp.get_html() #print extract_context(tree) - + # def handle_fr(self, element): @@ -411,4 +407,4 @@ if __name__=='__main__': # "sample_range":dict(zip(variables, sranges)), # "samples_count": numsamples, # "id":id, - # self.questions[self.lid]=problem + # self.questions[self.lid]=problem From af8155641b42850d3d30ecdcf9293886ec54ed70 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Jun 2012 16:30:23 -0400 Subject: [PATCH 3/5] Move remaining capa templates from courseware into common module --- {lms => common/lib/capa}/templates/textbox.html | 0 {lms => common/lib/capa}/templates/textinput_dynamath.html | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {lms => common/lib/capa}/templates/textbox.html (100%) rename {lms => common/lib/capa}/templates/textinput_dynamath.html (100%) diff --git a/lms/templates/textbox.html b/common/lib/capa/templates/textbox.html similarity index 100% rename from lms/templates/textbox.html rename to common/lib/capa/templates/textbox.html diff --git a/lms/templates/textinput_dynamath.html b/common/lib/capa/templates/textinput_dynamath.html similarity index 100% rename from lms/templates/textinput_dynamath.html rename to common/lib/capa/templates/textinput_dynamath.html From 80eb769d0c214502ede4169450d88f87b223b125 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Jun 2012 16:43:59 -0400 Subject: [PATCH 4/5] Pull StudentInputError from the module it's actually defined in --- common/lib/xmodule/capa_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index ad3e88207b..6bd7cbebdc 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -11,7 +11,8 @@ from datetime import timedelta from lxml import etree from x_module import XModule, XModuleDescriptor -from capa.capa_problem import LoncapaProblem, StudentInputError +from capa.capa_problem import LoncapaProblem +from capa.responsetypes import StudentInputError log = logging.getLogger("mitx.courseware") #----------------------------------------------------------------------------- From c3e730a0a2a8ea39e2bc7a25784c61474108782a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 7 Jun 2012 16:44:57 -0400 Subject: [PATCH 5/5] Add the capa module templates to the list of mako templates --- lms/envs/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index e4c0604aa9..806c379747 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -70,7 +70,8 @@ MAKO_TEMPLATES = {} MAKO_TEMPLATES['course'] = [DATA_DIR] MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections'] MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags'] -MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', +MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', + COMMON_ROOT / 'lib' / 'capa' / 'templates', DATA_DIR / 'info', DATA_DIR / 'problems']