Commit 260dc84f by Christina Roberts

Merge pull request #1854 from MITx/fix/cdodge/violation-fixes

violation fixes
parents c0b32caa 3ca2bf46
......@@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None):
if (len(new_html_parsed) == 1):
content = new_html_parsed[0].tail
else:
content = "\n".join([html.tostring(ele)
for ele in new_html_parsed[1:]])
content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
return {"id": passed_id,
"date": update['date'],
......
from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from django.http import Http404
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
......
import logging
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
......@@ -9,7 +8,7 @@ import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"}
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
def get_modulestore(location):
......@@ -87,8 +86,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
if settings.LMS_BASE is not None:
if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE)
else:
lms_base = settings.LMS_BASE
......@@ -193,6 +191,7 @@ class CoursePageNames:
CourseOutline = "course_index"
Checklists = "checklists"
def add_open_ended_panel_tab(course):
"""
Used to add the open ended panel tab to a course if it does not exist.
......@@ -209,6 +208,7 @@ def add_open_ended_panel_tab(course):
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
"""
Used to remove the open ended panel tab from a course if it exists.
......@@ -221,6 +221,6 @@ def remove_open_ended_panel_tab(course):
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct!=OPEN_ENDED_PANEL]
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
changed = True
return changed, course_tabs
......@@ -14,9 +14,6 @@ from tempfile import mkdtemp
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
......@@ -244,8 +241,7 @@ def edit_subsection(request, location):
(field.name, field.read_from(item))
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and
field.scope == Scope.settings
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
)
can_view_live = False
......@@ -623,7 +619,6 @@ def delete_item(request):
store = get_modulestore(item_loc)
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
# if item.location.revision=None, then delete both draft and published version
# if caller wants to only delete the draft than the caller should put item.location.revision='draft'
......@@ -665,7 +660,7 @@ def save_item(request):
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location));
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
......@@ -800,7 +795,7 @@ def upload_asset(request, org, course, coursename):
# Does the course actually exist?!? Get anything from it to prove its existance
try:
item = modulestore().get_item(location)
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
......@@ -844,14 +839,12 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
'''
This view will return all CMS users who are editors for the specified course
'''
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
......@@ -878,14 +871,14 @@ def create_json_response(errmsg=None):
return resp
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
......@@ -911,14 +904,15 @@ def add_user(request, location):
return create_json_response()
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
......@@ -999,7 +993,6 @@ def reorder_static_tabs(request):
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
......@@ -1011,7 +1004,6 @@ def reorder_static_tabs(request):
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
......@@ -1184,7 +1176,7 @@ def course_config_graders_page(request, org, course, name):
return render_to_response('settings_graders.html', {
'context_course': course_module,
'course_location' : location,
'course_location': location,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
......@@ -1203,8 +1195,8 @@ def course_config_advanced_page(request, org, course, name):
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location' : location,
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
'course_location': location,
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
})
......@@ -1225,7 +1217,8 @@ def course_settings_updates(request, org, course, name, section):
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else: return
else:
return
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
......@@ -1320,6 +1313,7 @@ def course_advanced_updates(request, org, course, name):
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
......@@ -1433,7 +1427,6 @@ def asset_index(request, org, course, name):
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
asset_display = []
for asset in assets:
id = asset['_id']
......@@ -1586,8 +1579,10 @@ def import_course(request, org, course, name):
shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False, static_content_store=contentstore(),
target_location_namespace=Location(location), draft_store=modulestore())
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
......
......@@ -174,7 +174,6 @@ class CourseDetails(object):
return result
# TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj):
......
......@@ -45,8 +45,7 @@ class CourseGradingModel(object):
# return empty model
else:
return {
"id": index,
return {"id": index,
"type": "",
"min_count": 0,
"drop_count": 0,
......@@ -95,7 +94,6 @@ class CourseGradingModel(object):
return CourseGradingModel.fetch(course_location)
@staticmethod
def update_grader_from_json(course_location, grader):
"""
......@@ -137,7 +135,6 @@ class CourseGradingModel(object):
return cutoffs
@staticmethod
def update_grace_period_from_json(course_location, graceperiodjson):
"""
......@@ -210,8 +207,7 @@ class CourseGradingModel(object):
location = Location(location)
descriptor = get_modulestore(location).get_item(location)
return {
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
}
......@@ -231,7 +227,6 @@ class CourseGradingModel(object):
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format
......@@ -262,8 +257,7 @@ class CourseGradingModel(object):
@staticmethod
def parse_grader(json_grader):
# manual to clear out kruft
result = {
"type": json_grader["type"],
result = {"type": json_grader["type"],
"min_count": int(json_grader.get('min_count', 0)),
"drop_count": int(json_grader.get('drop_count', 0)),
"short_label": json_grader.get('short_label', None),
......
......@@ -6,6 +6,7 @@ from xblock.core import Scope
from xmodule.course_module import CourseDescriptor
import copy
class CourseMetadata(object):
'''
For CRUD operations on metadata fields which do not have specific editors
......@@ -13,8 +14,13 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the
editable metadata.
'''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
'end',
'enrollment_start',
'enrollment_end',
'tabs',
'graceperiod',
'checklists']
@classmethod
def fetch(cls, course_location):
......
......@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Object, BlockScope
from xblock.core import String, Scope, Object
DEFAULT = "_DEFAULT_GROUP"
......
import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.contentstore.content import StaticContent
from xblock.core import Scope, String
log = logging.getLogger(__name__)
......@@ -25,7 +24,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable'
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
......
......@@ -219,5 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
stores_state = True
has_score = True
always_recalculate_grades=True
always_recalculate_grades = True
template_dir_name = "combinedopenended"
......@@ -10,7 +10,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
from xblock.core import String, Scope, List
from xblock.core import Scope, List
from xmodule.modulestore.exceptions import ItemNotFoundError
......@@ -60,7 +60,6 @@ class ConditionalModule(ConditionalFields, XModule):
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/conditional/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
]}
js_module_name = "Conditional"
......@@ -82,8 +81,7 @@ class ConditionalModule(ConditionalFields, XModule):
xml_value = self.descriptor.xml_attributes.get(xml_attr)
if xml_value:
return xml_value, attr_name
raise Exception('Error in conditional module: unknown condition "%s"'
% xml_attr)
raise Exception('Error in conditional module: unknown condition "%s"' % xml_attr)
def is_condition_satisfied(self):
self.required_modules = [self.system.get_module(descriptor) for
......@@ -163,7 +161,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
stores_state = True
has_score = False
@staticmethod
def parse_sources(xml_element, system, return_descriptor=False):
"""Parse xml_element 'sources' attr and:
......
......@@ -9,6 +9,7 @@ import StringIO
from xmodule.modulestore import Location
from .django import contentstore
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
......@@ -61,6 +62,7 @@ class StaticContent(object):
return {'tag': location.tag, 'org': location.org, 'course': location.course,
'category': location.category, 'name': location.name,
'revision': location.revision}
@staticmethod
def get_location_from_path(path):
# remove leading / character if it is there one
......@@ -79,8 +81,6 @@ class StaticContent(object):
return StaticContent.get_url_path_from_location(loc)
class ContentStore(object):
'''
Abstraction for all ContentStore providers (e.g. MongoDB)
......
from __future__ import absolute_import
from importlib import import_module
from os import environ
from django.conf import settings
......
......@@ -6,7 +6,6 @@ from gridfs.errors import NoFile
from xmodule.modulestore.mongo import location_to_query, Location
from xmodule.contentstore.content import XASSET_LOCATION_TAG
import sys
import logging
from .content import StaticContent, ContentStore
......@@ -26,7 +25,6 @@ class MongoContentStore(ContentStore):
self.fs = gridfs.GridFS(_db)
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
def save(self, content):
id = content.get_id()
......@@ -34,7 +32,8 @@ class MongoContentStore(ContentStore):
self.delete(id)
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
displayname=content.name, thumbnail_location=content.thumbnail_location,
import_path=content.import_path) as fp:
fp.write(content.data)
......@@ -49,7 +48,8 @@ class MongoContentStore(ContentStore):
try:
with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
fp.uploadDate, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
except NoFile:
raise NotFoundError()
......
......@@ -211,7 +211,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
template_dir_name = 'course'
def __init__(self, *args, **kwargs):
super(CourseDescriptor, self).__init__(*args, **kwargs)
......@@ -421,7 +420,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
policy['GRADE_CUTOFFS'] = value
self.grading_policy = policy
@property
def lowest_passing_grade(self):
return min(self._grading_policy['GRADE_CUTOFFS'].values())
......@@ -460,7 +458,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
else:
return self.cohort_config.get("auto_cohort_groups", [])
@property
def top_level_discussion_topic_ids(self):
"""
......@@ -469,7 +466,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
topics = self.discussion_topics
return [d["id"] for d in topics.values()]
@property
def cohorted_discussions(self):
"""
......@@ -483,8 +479,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return set(config.get("cohorted_discussions", []))
@property
def is_newish(self):
"""
......@@ -585,7 +579,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
yield module_descriptor
for c in self.get_children():
sections = []
for s in c.get_children():
if s.lms.graded:
xmoduledescriptors = list(yield_descriptor_descendents(s))
......@@ -603,7 +596,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return {'graded_sections': graded_sections,
'all_descriptors': all_descriptors, }
@staticmethod
def make_id(org, course, url_name):
return '/'.join([org, course, url_name])
......
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
......@@ -21,7 +20,6 @@ class DiscussionModule(DiscussionFields, XModule):
}
js_module_name = "InlineDiscussion"
def get_html(self):
context = {
'discussion_id': self.discussion_id,
......
class InvalidDefinitionError(Exception):
pass
class NotFoundError(Exception):
pass
class ProcessingError(Exception):
'''
An error occurred while processing a request to the XModule.
......
......@@ -51,6 +51,8 @@ class Date(ModelType):
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType):
def from_json(self, time_str):
"""
......
......@@ -149,7 +149,6 @@ class FolditModule(FolditFields, XModule):
return 1
class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
"""
Module for adding Foldit problems to courses
......
......@@ -6,7 +6,6 @@ Passes settings.MODULESTORE as kwargs to MongoModuleStore
from __future__ import absolute_import
from importlib import import_module
from os import environ
from django.conf import settings
......
......@@ -12,6 +12,7 @@ INHERITABLE_METADATA = (
'giturl' # for git edit link
)
def compute_inherited_metadata(descriptor):
"""Given a descriptor, traverse all of its descendants and do metadata
inheritance. Should be called on a CourseDescriptor after importing a
......
......@@ -7,7 +7,6 @@ from collections import namedtuple
from fs.osfs import OSFS
from itertools import repeat
from path import path
from datetime import datetime
from operator import attrgetter
from uuid import uuid4
......@@ -31,11 +30,13 @@ log = logging.getLogger(__name__)
# there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change
def get_course_id_no_run(location):
'''
'''
return "/".join([location.org, location.course])
class MongoKeyValueStore(KeyValueStore):
"""
A KeyValueStore that maps keyed data access to one of the 3 data areas
......@@ -130,8 +131,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
render_template: a function for rendering templates, as per
MakoDescriptorSystem
"""
super(CachingDescriptorSystem, self).__init__(
self.load_item, resources_fs, error_tracker, render_template)
super(CachingDescriptorSystem, self).__init__(self.load_item, resources_fs,
error_tracker, render_template)
self.modulestore = modulestore
self.module_data = module_data
self.default_class = default_class
......@@ -140,7 +141,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.course_id = None
self.cached_metadata = cached_metadata
def load_item(self, location):
"""
Return an XModule instance for the specified location
......@@ -468,7 +468,7 @@ class MongoModuleStore(ModuleStoreBase):
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
# bother with the metadata inheritance
return [self._load_item(item, data_cache,
apply_cached_metadata=(item['location']['category']!='course' or depth !=0)) for item in items]
apply_cached_metadata=(item['location']['category'] != 'course' or depth != 0)) for item in items]
def get_courses(self):
'''
......@@ -710,10 +710,9 @@ class MongoModuleStore(ModuleStoreBase):
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
self.update_metadata(course.location, own_metadata(course))
self.collection.remove({'_id': Location(location).dict()},
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
# from overriding our default value set in the init method.
safe=self.collection.safe)
self.collection.remove({'_id': Location(location).dict()}, safe=self.collection.safe)
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
......
......@@ -3,7 +3,7 @@ from itertools import repeat
from xmodule.course_module import CourseDescriptor
from .exceptions import (ItemNotFoundError, NoPathToItem)
from . import ModuleStore, Location
from . import Location
def path_to_location(modulestore, course_id, location):
......
import logging
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
......@@ -94,7 +93,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
return True
def delete_course(modulestore, contentstore, source_location, commit = False):
def delete_course(modulestore, contentstore, source_location, commit=False):
# first check to see if the modulestore is Mongo backed
if not isinstance(modulestore, MongoModuleStore):
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
......
......@@ -169,7 +169,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# Didn't load properly. Fall back on loading as an error
# descriptor. This should never error due to formatting.
msg = "Error loading from xml. " + str(err)[:200]
log.warning(msg)
# Normally, we don't want lots of exception traces in our logs from common
......@@ -376,10 +375,10 @@ class XMLModuleStore(ModuleStoreBase):
if course is None:
msg = ("No 'course' attribute set for course in {dir}."
" Using default '{default}'".format(
dir=course_dir,
" Using default '{default}'".format(dir=course_dir,
default=course_dir
))
)
)
log.warning(msg)
tracker(msg)
course = course_dir
......@@ -445,7 +444,6 @@ class XMLModuleStore(ModuleStoreBase):
log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor
def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name):
self._load_extra_content(system, course_descriptor, category, base_dir, course_dir)
......@@ -453,7 +451,6 @@ class XMLModuleStore(ModuleStoreBase):
if os.path.isdir(base_dir / url_name):
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
def _load_extra_content(self, system, course_descriptor, category, path, course_dir):
for filepath in glob.glob(path / '*'):
......@@ -480,7 +477,6 @@ class XMLModuleStore(ModuleStoreBase):
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
system.error_tracker("ERROR: " + str(e))
def get_instance(self, course_id, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at
......@@ -542,7 +538,6 @@ class XMLModuleStore(ModuleStoreBase):
return items
def get_courses(self, depth=0):
"""
Returns a list of course descriptors. If there were errors on loading,
......@@ -567,7 +562,6 @@ class XMLModuleStore(ModuleStoreBase):
"""
raise NotImplementedError("XMLModuleStores are read-only")
def update_children(self, location, children):
"""
Set the children for the item specified by the location to
......@@ -578,7 +572,6 @@ class XMLModuleStore(ModuleStoreBase):
"""
raise NotImplementedError("XMLModuleStores are read-only")
def update_metadata(self, location, metadata):
"""
Set the metadata for the item specified by the location to
......
......@@ -28,6 +28,7 @@ def lazyproperty(fn):
"""
attr_name = '_lazy_' + fn.__name__
@property
def _lazyprop(self):
if not hasattr(self, attr_name):
......
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