Commit 926fdd01 by ichuang

Merge pull request #173 from MITx/cpennington/pull-155-cleanup

This cleans up remaining issues w/ https://github.com/MITx/mitx/pull/155
parents 41be962d c468d47a
......@@ -59,16 +59,3 @@ def save_item(request):
export_to_github(course, "CMS Edit")
return HttpResponse(json.dumps({}))
def temp_force_export(request):
org = 'mit.edu'
course = '6002xs12'
name = '6.002_Spring_2012'
course = modulestore().get_item(['i4x', org, course, 'course', name])
fs = OSFS('../data-export-test')
xml = course.export_to_xml(fs)
with fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
return HttpResponse('Done')
......@@ -16,8 +16,8 @@ MODULESTORE = {
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'mongo_base',
'collection': 'key_store',
'db': 'xmodule',
'collection': 'modulestore',
}
}
}
......
......@@ -10,7 +10,6 @@ urlpatterns = ('',
url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$', 'contentstore.views.course_index', name='course_index'),
url(r'^temp_force_export$', 'contentstore.views.temp_force_export'),
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
)
......
......@@ -74,11 +74,11 @@ class LoncapaProblem(object):
Arguments:
- problem_text : xml defining the problem
- id : string used as the identifier for this problem; often a filename (no spaces)
- state : student state (represented as a dict)
- seed : random number generator seed (int)
- system : I4xSystem instance which provides OS, rendering, and user context
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- state (dict): student state
- seed (int): random number generator seed (int)
- system (I4xSystme): I4xSystem instance which provides OS, rendering, and user context
'''
......@@ -126,7 +126,7 @@ class LoncapaProblem(object):
self.done = False
def __unicode__(self):
return u"LoncapaProblem ({0})".format(self.problem_text)
return u"LoncapaProblem ({0})".format(self.problem_id)
def get_state(self):
''' Stored per-user session data neeeded to:
......@@ -265,7 +265,7 @@ class LoncapaProblem(object):
parent = inc.getparent() # insert new XML into tree in place of inlcude
parent.insert(parent.index(inc),incxml)
parent.remove(inc)
log.debug('Included %s into %s' % (file, self.id))
log.debug('Included %s into %s' % (file, self.problem_id))
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
'''
......
......@@ -18,21 +18,21 @@ setup(
entry_points={
'xmodule.v1': [
"abtest = xmodule.abtest_module:ABTestDescriptor",
"book = xmodule.translation_module:TranslateCustomTagDescriptor",
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"chapter = xmodule.seq_module:SequenceDescriptor",
"course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.translation_module:TranslateCustomTagDescriptor",
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.translation_module:TranslateCustomTagDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.vertical_module:VerticalDescriptor",
"section = xmodule.translation_module:SemanticSectionDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.translation_module:TranslateCustomTagDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.translation_module:TranslateCustomTagDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor",
]
}
......
......@@ -29,13 +29,6 @@ def group_from_value(groups, v):
class ABTestModule(XModule):
"""
Implements an A/B test with an aribtrary number of competing groups
Format:
<abtest>
<group name="a" portion=".1"><contenta/></group>
<group name="b" portion=".2"><contentb/></group>
<default><contentdefault/></default>
</abtest>
"""
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
......@@ -60,15 +53,53 @@ class ABTestModule(XModule):
in self.definition['data']['group_content'][self.group]]
# TODO (cpennington): Use Groups should be a first class object, rather than being
# managed by ABTests
class ABTestDescriptor(RawDescriptor, XmlDescriptor):
module_class = ABTestModule
def __init__(self, system, definition=None, **kwargs):
"""
definition is a dictionary with the following layout:
{'data': {
'experiment': 'the name of the experiment',
'group_portions': {
'group_a': 0.1,
'group_b': 0.2
},
'group_contents': {
'group_a': [
'url://for/content/module/1',
'url://for/content/module/2',
],
'group_b': [
'url://for/content/module/3',
],
DEFAULT: [
'url://for/default/content/1'
]
}
},
'children': [
'url://for/content/module/1',
'url://for/content/module/2',
'url://for/content/module/3',
'url://for/default/content/1',
]}
"""
kwargs['shared_state_key'] = definition['data']['experiment']
RawDescriptor.__init__(self, system, definition, **kwargs)
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
XML Format:
<abtest experiment="experiment_name">
<group name="a" portion=".1"><contenta/></group>
<group name="b" portion=".2"><contentb/></group>
<default><contentdefault/></default>
</abtest>
"""
experiment = xml_object.get('experiment')
if experiment is None:
......
......@@ -65,10 +65,8 @@ class ComplexEncoder(json.JSONEncoder):
class CapaModule(XModule):
''' Interface between capa_problem and x_module. Originally a hack
meant to be refactored out, but it seems to be serving a useful
prupose now. We can e.g .destroy and create the capa_problem on a
reset.
'''
An XModule implementing LonCapa format problems, implemented by way of capa.capa_problem.LoncapaProblem
'''
icon_class = 'problem'
......@@ -80,11 +78,6 @@ class CapaModule(XModule):
dom2 = etree.fromstring(definition['data'])
self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'),
default="closed")
# TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed")
self.explain_available = only_one(dom2.xpath('/problem/@explain_available'))
display_due_date_string = self.metadata.get('due', None)
if display_due_date_string is not None:
self.display_due_date = dateutil.parser.parse(display_due_date_string)
......@@ -246,16 +239,6 @@ class CapaModule(XModule):
if self.max_attempts is None and self.rerandomize != "always":
save_button = False
# Check if explanation is available, and if so, give a link
explain = ""
if self.lcp.done and self.explain_available == 'attempted':
explain = self.explanation
if self.closed() and self.explain_available == 'closed':
explain = self.explanation
if len(explain) == 0:
explain = False
context = {'problem': content,
'id': self.id,
'check_button': check_button,
......@@ -265,7 +248,6 @@ class CapaModule(XModule):
'ajax_url': self.system.ajax_url,
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'explain': explain,
'progress': self.get_progress(),
}
......
......@@ -19,9 +19,13 @@ URL_RE = re.compile("""
(/(?P<revision>[^/]+))?
""", re.VERBOSE)
# TODO (cpennington): We should decide whether we want to expand the
# list of valid characters in a location
INVALID_CHARS = re.compile(r"[^\w.-]")
_LocationBase = namedtuple('LocationBase', 'tag org course category name revision')
class Location(_LocationBase):
'''
Encodes a location.
......
......@@ -82,9 +82,9 @@ class XMLModuleStore(ModuleStore):
course = course_dir
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, modulestore):
def __init__(self, xmlstore):
"""
modulestore: the XMLModuleStore to store the loaded modules in
xmlstore: the XMLModuleStore to store the loaded modules in
"""
self.unnamed_modules = 0
self.used_slugs = set()
......@@ -110,18 +110,18 @@ class XMLModuleStore(ModuleStore):
# log.debug('-> slug=%s' % slug)
xml_data.set('slug', slug)
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class)
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, xmlstore.default_class)
log.debug('==> importing module location %s' % repr(module.location))
modulestore.modules[module.location] = module
xmlstore.modules[module.location] = module
if modulestore.eager:
if xmlstore.eager:
module.get_children()
return module
system_kwargs = dict(
render_template=lambda: '',
load_item=modulestore.get_item,
resources_fs=OSFS(modulestore.data_dir / course_dir),
load_item=xmlstore.get_item,
resources_fs=OSFS(xmlstore.data_dir / course_dir),
process_xml=process_xml
)
MakoDescriptorSystem.__init__(self, **system_kwargs)
......
......@@ -96,7 +96,9 @@ class XModule(object):
kwargs: Optional arguments. Subclasses should always accept kwargs and pass them
to the parent class constructor.
Current known uses of kwargs:
metadata: A dictionary containing data that specifies information that is particular
metadata: SCAFFOLDING - This dictionary will be split into several different types of metadata
in the future (course policy, modification history, etc).
A dictionary containing data that specifies information that is particular
to a problem in the context of a course
'''
self.system = system
......
......@@ -3,9 +3,14 @@ from xmodule.x_module import XModuleDescriptor
from lxml import etree
import copy
import logging
from collections import namedtuple
log = logging.getLogger(__name__)
# TODO (cpennington): This was implemented in an attempt to improve performance,
# but the actual improvement wasn't measured (and it was implemented late at night).
# We should check if it hurts, and whether there's a better way of doing lazy loading
class LazyLoadingDict(MutableMapping):
"""
A dictionary object that lazily loads it's contents from a provided
......@@ -58,6 +63,18 @@ class LazyLoadingDict(MutableMapping):
self._loaded = True
_AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
class AttrMap(_AttrMapBase):
"""
A class that specifies a metadata_key, a function to transform an xml attribute to be placed in that key,
and to transform that key value
"""
def __new__(_cls, metadata_key, to_metadata=lambda x: x, from_metadata=lambda x: x):
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
class XmlDescriptor(XModuleDescriptor):
"""
Mixin class for standardized parsing of from xml
......@@ -71,6 +88,13 @@ class XmlDescriptor(XModuleDescriptor):
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
'due', 'graded', 'name', 'slug')
# A dictionary mapping xml attribute names to functions of the value
# that return the metadata key and value
xml_attribute_map = {
'graded': AttrMap('graded', lambda val: val == 'true', lambda val: str(val).lower()),
'name': AttrMap('display_name'),
}
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
......@@ -116,16 +140,11 @@ class XmlDescriptor(XModuleDescriptor):
def metadata_loader():
metadata = {}
for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'):
from_xml = xml_object.get(attr)
if from_xml is not None:
metadata[attr] = from_xml
if xml_object.get('graded') is not None:
metadata['graded'] = xml_object.get('graded') == 'true'
if xml_object.get('name') is not None:
metadata['display_name'] = xml_object.get('name')
for attr in cls.metadata_attributes:
val = xml_object.get(attr)
if val is not None:
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
return metadata
......@@ -171,6 +190,8 @@ class XmlDescriptor(XModuleDescriptor):
The returned XML should be able to be parsed back into an identical XModuleDescriptor
using the from_xml method with the same system, org, and course
resource_fs is a pyfilesystem office (from the fs package)
"""
xml_object = self.definition_to_xml(resource_fs)
self.__class__.clean_metadata_from_xml(xml_object)
......@@ -191,15 +212,15 @@ class XmlDescriptor(XModuleDescriptor):
xml_object.set('slug', self.name)
xml_object.tag = self.category
for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'):
if attr in self.metadata and attr not in self._inherited_metadata:
xml_object.set(attr, self.metadata[attr])
for attr in self.metadata_attributes:
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
metadata_key = attr_map.metadata_key
if 'graded' in self.metadata and 'graded' not in self._inherited_metadata:
xml_object.set('graded', str(self.metadata['graded']).lower())
if metadata_key not in self.metadata or metadata_key in self._inherited_metadata:
continue
if 'display_name' in self.metadata:
xml_object.set('name', self.metadata['display_name'])
val = attr_map.from_metadata(self.metadata[metadata_key])
xml_object.set(attr, val)
return etree.tostring(xml_object, pretty_print=True)
......
......@@ -72,6 +72,10 @@ class StudentModuleCache(object):
'''
Find any StudentModule objects that are needed by any child modules of the
supplied descriptor. Avoids making multiple queries to the database
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
'''
if user.is_authenticated():
module_ids = self._get_module_state_keys(descriptor, depth)
......@@ -92,7 +96,11 @@ class StudentModuleCache(object):
def _get_module_state_keys(self, descriptor, depth):
'''
Get a list of the state_keys needed for StudentModules
required for this chunk of module xml
required for this module descriptor
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
'''
keys = [descriptor.location.url()]
......
......@@ -41,7 +41,7 @@ class I4xSystem(object):
module instance object.
render_template - a function that takes (template_file, context), and returns
rendered html.
user - The user to base the seed off of for this request
user - The user to base the random number generator seed off of for this request
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR.
replace_urls - TEMPORARY - A function like static_replace.replace_urls
......@@ -176,20 +176,20 @@ def get_section(course_module, chapter, section):
def get_module(user, request, location, student_module_cache, position=None):
''' Get an instance of the xmodule class corresponding to module_xml,
''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
exists.
Arguments:
- user : current django User
- request : current django HTTPrequest
- module_xml : lxml etree of xml subtree for the requested module
- location : A Location-like object identifying the module to load
- student_module_cache : a StudentModuleCache
- position : extra information from URL for user-specified
position within module
Returns:
- a tuple (xmodule instance, instance_module, shared_module, module type).
- a tuple (xmodule instance, instance_module, shared_module, module category).
instance_module is a StudentModule specific to this module for this student
shared_module is a StudentModule specific to all modules with the same 'shared_state_key' attribute, or None if the module doesn't elect to share state
'''
......
......@@ -30,9 +30,6 @@
% if answer_available:
<input class="show" type="button" value="Show Answer">
% endif
% if explain :
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
% endif
% if attempts_allowed :
<section class="submission_feedback">
You have used ${ attempts_used } of ${ attempts_allowed } submissions
......
......@@ -42,21 +42,6 @@ end
task :default => [:test, :pep8, :pylint]
directory REPORT_DIR
directory LMS_REPORT_DIR
desc "Run pep8 on all libraries"
task :pep8 => REPORT_DIR do
sh("pep8 --ignore=E501 lms/djangoapps common/lib/* | tee #{REPORT_DIR}/pep8.report")
end
desc "Run pylint on all libraries"
task :pylint => REPORT_DIR do
Dir["lms/djangoapps/*", "common/lib/*"].each do |app|
ENV['PYTHONPATH'] = File.dirname(app)
app = File.basename(app)
sh("pylint --rcfile=.pylintrc -f parseable #{app} | tee #{REPORT_DIR}/#{app}.pylint.report")
end
end
default_options = {
:lms => '8000',
......@@ -68,18 +53,39 @@ task :predjango do
sh('pip install -e common/lib/xmodule')
end
[:lms, :cms, :common].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s)
directory report_dir
desc "Run pep8 on all #{system} code"
task "pep8_#{system}" => report_dir do
sh("pep8 --ignore=E501 #{system}/djangoapps #{system}/lib | tee #{report_dir}/pep8.report")
end
task :pep8 => "pep8_#{system}"
desc "Run pylint on all #{system} code"
task "pylint_#{system}" => report_dir do
Dir["#{system}/djangoapps/*", "#{system}/lib/*"].each do |app|
ENV['PYTHONPATH'] = File.dirname(app)
app = File.basename(app)
sh("pylint --rcfile=.pylintrc -f parseable #{app} | tee #{report_dir}/#{app}.pylint.report")
end
end
task :pylint => "pylint_#{system}"
end
[:lms, :cms].each do |system|
task_name = "test_#{system}"
report_dir = File.join(REPORT_DIR, task_name)
report_dir = File.join(REPORT_DIR, system.to_s)
directory report_dir
# Per System tasks
desc "Run all django tests on our djangoapps for the #{system}"
task task_name => [report_dir, :predjango] do
task "test_#{system}" => [report_dir, :predjango] do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover")
sh(django_admin(system, :test, 'test', *Dir["#{system}/djangoapps/*"].each))
end
task :test => task_name
task :test => "test_#{system}"
desc <<-desc
Start the #{system} locally with the specified environment (defaults to dev).
......@@ -89,7 +95,8 @@ end
args.with_defaults(:env => 'dev', :options => default_options[system])
sh(django_admin(system, args.env, 'runserver', args.options))
end
# Per environment tasks
Dir["#{system}/envs/*.py"].each do |env_file|
env = File.basename(env_file).gsub(/\.py/, '')
desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment