diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 72ae3821cc..e3680018e3 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,7 +1,7 @@ import json import shutil from django.test.client import Client -from override_settings import override_settings +from django.test.utils import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path @@ -10,6 +10,7 @@ import json from fs.osfs import OSFS import copy from mock import Mock +from json import dumps, loads from student.models import Registration from django.contrib.auth.models import User @@ -26,10 +27,12 @@ from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.templates import update_templates from xmodule.capa_module import CapaDescriptor from xmodule.course_module import CourseDescriptor from xmodule.seq_module import SequenceDescriptor +from xmodule.modulestore.exceptions import ItemNotFoundError TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') @@ -207,6 +210,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # check for custom_tags self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template') + # check for graiding_policy.json + fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') + self.assertTrue(fs.exists('grading_policy.json')) + + # compare what's on disk compared to what we have in our course + with fs.open('grading_policy.json','r') as grading_policy: + on_disk = loads(grading_policy.read()) + course = ms.get_item(location) + self.assertEqual(on_disk, course.definition['data']['grading_policy']) # remove old course delete_course(ms, cs, location) @@ -399,3 +411,32 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertIn('markdown', context, "markdown is missing from context") self.assertIn('markdown', problem.metadata, "markdown is missing from metadata") self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") + + +class TemplateTestCase(ModuleStoreTestCase): + + def test_template_cleanup(self): + ms = modulestore('direct') + + # insert a bogus template in the store + bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus') + source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Empty') + + ms.clone_item(source_template_location, bogus_template_location) + + verify_create = ms.get_item(bogus_template_location) + self.assertIsNotNone(verify_create) + + # now run cleanup + update_templates() + + # now try to find dangling template, it should not be in DB any longer + asserted = False + try: + verify_create = ms.get_item(bogus_template_location) + except ItemNotFoundError: + asserted = True + + self.assertTrue(asserted) + + diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 84e79b9670..925b2431b9 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -249,7 +249,7 @@ class CourseGradingTest(CourseTestCase): altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") - test_grader.grace_period = {'hours' : '4'} + test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0} altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 9af5b09276..166982e35f 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,7 +1,6 @@ import json import shutil from django.test.client import Client -from override_settings import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path @@ -86,7 +85,6 @@ class ContentStoreTestCase(ModuleStoreTestCase): # Now make sure that the user is now actually activated self.assertTrue(user(email).is_active) - class AuthTestCase(ContentStoreTestCase): """Check that various permissions-related things work""" diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 4e3510463f..be028b2836 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -2,7 +2,6 @@ import json import copy from time import time from django.test import TestCase -from override_settings import override_settings from django.conf import settings from student.models import Registration diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index f4c6fd3d7c..3d0b8f78af 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -155,7 +155,8 @@ class CourseGradingModel(object): if 'grace_period' in graceperiodjson: graceperiodjson = graceperiodjson['grace_period'] - grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()]) + # lms requires these to be in a fixed order + grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson) descriptor = get_modulestore(course_location).get_item(course_location) descriptor.metadata['graceperiod'] = grace_rep @@ -234,10 +235,10 @@ class CourseGradingModel(object): @staticmethod def convert_set_grace_period(descriptor): - # 5 hours 59 minutes 59 seconds => converted to iso format + # 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59} rawgrace = descriptor.metadata.get('graceperiod', None) if rawgrace: - parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} + parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} return parsedgrace else: return None diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 8cbae177a8..7d92ab69ad 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -58,6 +58,9 @@ $(document).ready(function() { drop: onSectionReordered, greedy: true }); + + // stop clicks on drag bars from doing their thing w/o stopping drag + $('.drag-handle').click(function(e) {e.preventDefault(); }); }); @@ -202,13 +205,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) { children = _.without(children, ui.draggable.data('id')); } // add to this parent (figure out where) - for (var i = 0; i < _els.length; i++) { - if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) { + for (var i = 0, bump = 0; i < _els.length; i++) { + if (ui.draggable.is(_els[i])) { + bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c + // it's not in that list + } + else if (ui.offset.top < $(_els[i]).offset().top) { // insert at i in children and _els ui.draggable.insertBefore($(_els[i])); // TODO figure out correct way to have it remove the style: top:n; setting (and similar line below) ui.draggable.attr("style", "position:relative;"); - children.splice(i, 0, ui.draggable.data('id')); + children.splice(i + bump, 0, ui.draggable.data('id')); break; } } diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js index 826b385dff..f4c7df41a6 100644 --- a/cms/static/js/views/settings/main_settings_view.js +++ b/cms/static/js/views/settings/main_settings_view.js @@ -227,7 +227,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ time = 0; } var newVal = new Date(date.getTime() + time * 1000); - if (cacheModel.get(fieldName).getTime() !== newVal.getTime()) { + if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) { cacheModel.save(fieldName, newVal, { error: CMS.ServerError}); } } diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index 0fbf863fee..b3ad928b39 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -2,7 +2,7 @@ import django.test from django.contrib.auth.models import User from django.conf import settings -from override_settings import override_settings +from django.test.utils import override_settings from course_groups.models import CourseUserGroup from course_groups.cohorts import (get_cohort, get_course_cohorts, diff --git a/common/djangoapps/status/tests.py b/common/djangoapps/status/tests.py index 98a36f433a..1695663ac5 100644 --- a/common/djangoapps/status/tests.py +++ b/common/djangoapps/status/tests.py @@ -1,7 +1,7 @@ from django.conf import settings from django.test import TestCase import os -from override_settings import override_settings +from django.test.utils import override_settings from tempfile import NamedTemporaryFile from status import get_site_status_msg diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py index 900371a0dd..ec2d29ecfa 100644 --- a/common/djangoapps/util/converters.py +++ b/common/djangoapps/util/converters.py @@ -18,10 +18,13 @@ def jsdate_to_time(field): """ if field is None: return field - elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z - d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable + elif isinstance(field, basestring): + # ISO format but ignores time zone assuming it's Z. + d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable return d.utctimetuple() - elif isinstance(field, int) or isinstance(field, float): + elif isinstance(field, (int, long, float)): return time.gmtime(field / 1000) elif isinstance(field, time.struct_time): return field + else: + raise ValueError("Couldn't convert %r to time" % field) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 4ad25def8a..a1a4e6b65e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -633,9 +633,13 @@ class MultipleChoiceResponse(LoncapaResponse): # define correct choices (after calling secondary setup) xml = self.xml cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id')) - self.correct_choices = [contextualize_text(choice.get('name'), - self.context) for choice in cxml if - contextualize_text(choice.get('correct'), self.context) == "true"] + + # contextualize correct attribute and then select ones for which + # correct = "true" + self.correct_choices = [ + contextualize_text(choice.get('name'), self.context) + for choice in cxml + if contextualize_text(choice.get('correct'), self.context) == "true"] def mc_setup_response(self): ''' diff --git a/common/lib/capa/capa/templates/codeinput.html b/common/lib/capa/capa/templates/codeinput.html index 5c2ff2aca5..eb8cad0d70 100644 --- a/common/lib/capa/capa/templates/codeinput.html +++ b/common/lib/capa/capa/templates/codeinput.html @@ -50,6 +50,7 @@ }, smartIndent: false }); + $("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))}); }); diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index a4ad548ae8..2c69c449ba 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -446,7 +446,7 @@ class CourseDescriptor(SequenceDescriptor): # utility function to get datetime objects for dates used to # compute the is_new flag and the sorting_score def to_datetime(timestamp): - return datetime.fromtimestamp(time.mktime(timestamp)) + return datetime(*timestamp[:6]) def get_date(field): timetuple = self._try_parse_time(field) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py index bdbd5a6133..509a2c7db9 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py @@ -2,6 +2,7 @@ import logging from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from fs.osfs import OSFS +from json import dumps def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir): @@ -27,6 +28,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d # export the course updates export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html') + # export the grading policy + policies_dir = export_fs.makeopendir('policies') + course_run_policy_dir = policies_dir.makeopendir(course.location.name) + with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: + grading_policy.write(dumps(course.definition['data']['grading_policy'])) + def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''): query_loc = Location('i4x', course_location.org, course_location.course, category_type, None) diff --git a/common/lib/xmodule/xmodule/templates.py b/common/lib/xmodule/xmodule/templates.py index ce37df929f..eaf821155e 100644 --- a/common/lib/xmodule/xmodule/templates.py +++ b/common/lib/xmodule/xmodule/templates.py @@ -56,6 +56,10 @@ def update_templates(): available from the installed plugins """ + # cdodge: build up a list of all existing templates. This will be used to determine which + # templates have been removed from disk - and thus we need to remove from the DB + templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None]) + for category, templates in all_templates().items(): for template in templates: if 'display_name' not in template.metadata: @@ -85,3 +89,12 @@ def update_templates(): modulestore('direct').update_item(template_location, template.data) modulestore('direct').update_children(template_location, template.children) modulestore('direct').update_metadata(template_location, template.metadata) + + # remove template from list of templates to delete + templates_to_delete = [t for t in templates_to_delete if t.location != template_location] + + # now remove all templates which appear to have removed from disk + if len(templates_to_delete) > 0: + logging.debug('deleting dangling templates = {0}'.format(templates_to_delete)) + for template in templates_to_delete: + modulestore('direct').delete_item(template.location) diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py index 23ed06eb59..9abb5d183f 100644 --- a/common/lib/xmodule/xmodule/timelimit_module.py +++ b/common/lib/xmodule/xmodule/timelimit_module.py @@ -86,8 +86,8 @@ class TimeLimitModule(XModule): modified_duration = self._get_accommodated_duration(duration) self.ending_at = self.beginning_at + modified_duration - def get_end_time_in_ms(self): - return int(self.ending_at * 1000) + def get_remaining_time_in_ms(self): + return int((self.ending_at - time()) * 1000) def get_instance_state(self): state = {} diff --git a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee index d31a402a99..8b47696c01 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee @@ -50,7 +50,7 @@ if Backbone? convertMath: -> element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] renderResponses: -> diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index a8e95c2565..6320c3d1e3 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -47,7 +47,7 @@ if Backbone? convertMath: -> element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] toggleVote: (event) -> diff --git a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee index e6c8064978..84e7357e1f 100644 --- a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee +++ b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee @@ -26,7 +26,7 @@ if Backbone? convertMath: -> body = @$el.find(".response-body") - body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html() + body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]] markAsStaff: -> diff --git a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee index 32683fe6f6..1f305ddf34 100644 --- a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee @@ -30,7 +30,7 @@ if Backbone? convertMath: -> element = @$(".response-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] markAsStaff: -> diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 99f138f0bc..cecc4f9cf9 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -1,5 +1,5 @@ from django.core.urlresolvers import reverse -from override_settings import override_settings +from django.test.utils import override_settings import xmodule.modulestore.django diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index efa5ad823e..fb6842d4a9 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -11,7 +11,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.conf import settings from django.core.urlresolvers import reverse -from override_settings import override_settings +from django.test.utils import override_settings import xmodule.modulestore.django from xmodule.modulestore.mongo import MongoModuleStore diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index e8b36ecd2a..fb351e1c01 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -177,10 +177,10 @@ def check_for_active_timelimit_module(request, course_id, course): raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location)) time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url') context['time_expired_redirect_url'] = time_expired_redirect_url - # Fetch the end time (in GMT) as stored in the module when it was started. - # This value should be UTC time as number of milliseconds since epoch. - end_date = timelimit_module.get_end_time_in_ms() - context['timer_expiration_datetime'] = end_date + # Fetch the remaining time relative to the end time as stored in the module when it was started. + # This value should be in milliseconds. + remaining_time = timelimit_module.get_remaining_time_in_ms() + context['timer_expiration_duration'] = remaining_time if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata: context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation'] return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location}) @@ -191,7 +191,7 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des ''' Updates the state of the provided timing module, starting it if it hasn't begun. Returns dict with timer-related values to enable display of time remaining. - Returns 'timer_expiration_datetime' in dict if timer is still active, and not if timer has expired. + Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired. ''' context = {} # determine where to go when the exam ends: @@ -215,10 +215,9 @@ def update_timelimit_module(user, course_id, student_module_cache, timelimit_des instance_module.save() # the exam has been started, either because the student is returning to the - # exam page, or because they have just visited it. Fetch the end time (in GMT) as stored - # in the module when it was started. - # This value should be UTC time as number of milliseconds since epoch. - context['timer_expiration_datetime'] = timelimit_module.get_end_time_in_ms() + # exam page, or because they have just visited it. Fetch the remaining time relative to the + # end time as stored in the module when it was started. + context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms() # also use the timed module to determine whether top-level navigation is visible: if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata: context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation'] @@ -325,7 +324,7 @@ def index(request, course_id, chapter=None, section=None, if section_module.category == 'timelimit': timer_context = update_timelimit_module(request.user, course_id, student_module_cache, section_descriptor, section_module) - if 'timer_expiration_datetime' in timer_context: + if 'timer_expiration_duration' in timer_context: context.update(timer_context) else: # if there is no expiration defined, then we know the timer has expired: diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py index ac059a1e3f..4b5fe2ba5a 100644 --- a/lms/djangoapps/django_comment_client/tests.py +++ b/lms/djangoapps/django_comment_client/tests.py @@ -6,7 +6,7 @@ from django.conf import settings from mock import Mock -from override_settings import override_settings +from django.test.utils import override_settings import xmodule.modulestore.django diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 1f1a80e2b4..877c730539 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -11,6 +11,8 @@ from django.http import HttpResponse from django.utils import simplejson from django_comment_client.models import Role from django_comment_client.permissions import check_permissions_by_view +from xmodule.modulestore.exceptions import NoPathToItem + from mitxmako import middleware import pystache_custom as pystache @@ -164,6 +166,7 @@ def initialize_discussion_info(course): # get all discussion models within this course_id all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id) + path_to_locations = {} for module in all_modules: skip_module = False for key in ('id', 'discussion_category', 'for'): @@ -171,6 +174,14 @@ def initialize_discussion_info(course): log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) skip_module = True + # cdodge: pre-compute the path_to_location. Note this can throw an exception for any + # dangling discussion modules + try: + path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location) + except NoPathToItem: + log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location)) + skip_module = True + if skip_module: continue @@ -235,6 +246,7 @@ def initialize_discussion_info(course): _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['category_map'] = category_map _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() + _DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations class JsonResponse(HttpResponse): @@ -390,11 +402,23 @@ def get_courseware_context(content, course): if id in id_map: location = id_map[id]["location"].url() title = id_map[id]["title"] - (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) - url = reverse('courseware_position', kwargs={"course_id": course_id, - "chapter": chapter, - "section": section, - "position": position}) + + # cdodge: did we pre-compute, if so, then let's use that rather than recomputing + if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']: + (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location] + else: + try: + (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location) + except NoPathToItem: + # Object is not in the graph any longer, let's just get path to the base of the course + # so that we can at least return something to the caller + (course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location) + + url = reverse('courseware_position', kwargs={"course_id":course_id, + "chapter":chapter, + "section":section, + "position":position}) + content_info = {"courseware_url": url, "courseware_title": title} return content_info diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 2610e57422..b775aa158a 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -15,7 +15,7 @@ import json from nose import SkipTest from mock import patch, Mock -from override_settings import override_settings +from django.test.utils import override_settings # Need access to internal func to put users in the right group from django.contrib.auth.models import Group diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 4d220d4baa..ec2fe5ab38 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -22,7 +22,7 @@ from mitxmako.shortcuts import render_to_string import logging log = logging.getLogger(__name__) -from override_settings import override_settings +from django.test.utils import override_settings from django.http import QueryDict diff --git a/lms/envs/common.py b/lms/envs/common.py index f3bf223451..eb8c9989f0 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -200,7 +200,6 @@ COURSE_TITLE = "Circuits and Electronics" ### Dark code. Should be enabled in local settings for devel. ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome) -QUICKEDIT = False WIKI_ENABLED = False diff --git a/lms/envs/dev_edx4edx.py b/lms/envs/dev_edx4edx.py index c138ed81ae..2ebd24e68b 100644 --- a/lms/envs/dev_edx4edx.py +++ b/lms/envs/dev_edx4edx.py @@ -34,7 +34,6 @@ EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx" DEBUG = True ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome) -QUICKEDIT = True MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT] diff --git a/lms/envs/edx4edx_aws.py b/lms/envs/edx4edx_aws.py index de377c0b57..b82048824f 100644 --- a/lms/envs/edx4edx_aws.py +++ b/lms/envs/edx4edx_aws.py @@ -6,7 +6,6 @@ COURSE_TITLE = "edx4edx: edX Author Course" EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx" ### Dark code. Should be enabled in local settings for devel. -QUICKEDIT = True ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome) ### PIPELINE_CSS_COMPRESSOR = None diff --git a/lms/lib/dogfood/README.md b/lms/lib/dogfood/README.md deleted file mode 100644 index c6a7113049..0000000000 --- a/lms/lib/dogfood/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a library for edx4edx, allowing users to practice writing problems. diff --git a/lms/lib/dogfood/__init__.py b/lms/lib/dogfood/__init__.py deleted file mode 100644 index d00d8ea793..0000000000 --- a/lms/lib/dogfood/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from check import * diff --git a/lms/lib/dogfood/check.py b/lms/lib/dogfood/check.py deleted file mode 100644 index 070d3f9262..0000000000 --- a/lms/lib/dogfood/check.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python - -from random import choice -import string -import traceback - -from django.conf import settings -import capa.capa_problem as lcp -from dogfood.views import update_problem - - -def GenID(length=8, chars=string.letters + string.digits): - return ''.join([choice(chars) for i in range(length)]) - -randomid = GenID() - - -def check_problem_code(ans, the_lcp, correct_answers, false_answers): - """ - ans = student's answer - the_lcp = LoncapaProblem instance - - returns dict {'ok':is_ok,'msg': message with iframe} - """ - pfn = "dog%s" % randomid - pfn += the_lcp.problem_id.replace('filename', '') # add problem ID to dogfood problem name - update_problem(pfn, ans, filestore=the_lcp.system.filestore) - msg = '
' - msg += '' % (settings.MITX_ROOT_URL, pfn) - msg += '
' - - endmsg = """

Note: if the code text box disappears after clicking on "Check", - please type something in the box to make it refresh properly. This is a - bug with Chrome; it does not happen with Firefox. It is being fixed. -

""" - - is_ok = True - if (not correct_answers) or (not false_answers): - ret = {'ok': is_ok, - 'msg': msg + endmsg, - } - return ret - - try: - # check correctness - fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn) - test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system) - - if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1') == 'correct'): - is_ok = False - if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1') == 'correct'): - is_ok = False - except Exception, err: - is_ok = False - msg += "

Error: %s

" % str(err).replace('<', '<') - msg += "

%s

" % traceback.format_exc().replace('<', '<') - - ret = {'ok': is_ok, - 'msg': msg + endmsg, - } - return ret diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py deleted file mode 100644 index 6df881df98..0000000000 --- a/lms/lib/dogfood/views.py +++ /dev/null @@ -1,325 +0,0 @@ -''' -dogfood.py - -For using mitx / edX / i4x in checking itself. - -df_capa_problem: accepts an XML file for a problem, and renders it. -''' -import logging -import datetime -import re -import os # FIXME - use OSFS instead - -from fs.osfs import OSFS - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.context_processors import csrf -from django.core.mail import send_mail -from django.http import Http404 -from django.http import HttpResponse -from django.shortcuts import redirect -from mitxmako.shortcuts import render_to_response, render_to_string - -import track.views -from lxml import etree - -from courseware.module_render import make_track_function, ModuleSystem, get_module -from courseware.models import StudentModule -from multicourse import multicourse_settings -from student.models import UserProfile -from util.cache import cache -from util.views import accepts - -import courseware.content_parser as content_parser -#import courseware.modules -import xmodule - -log = logging.getLogger("mitx.courseware") - -etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True)) - -DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings - - -def update_problem(pfn, pxml, coursename=None, overwrite=True, filestore=None): - ''' - update problem with filename pfn, and content (xml) pxml. - ''' - if not filestore: - if not coursename: coursename = DOGFOOD_COURSENAME - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - pfn2 = settings.DATA_DIR + xp + 'problems/%s.xml' % pfn - fp = open(pfn2, 'w') - else: - pfn2 = 'problems/%s.xml' % pfn - fp = filestore.open(pfn2, 'w') - log.debug('[dogfood.update_problem] pfn2=%s' % pfn2) - - if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False - pxmls = pxml if type(pxml) in [str, unicode] else etree.tostring(pxml, pretty_print=True) - fp.write(pxmls) - fp.close() - - -def df_capa_problem(request, id=None): - ''' - dogfood capa problem. - - Accepts XML for a problem, inserts it into the dogfood course.xml. - Returns rendered problem. - ''' - # "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY." - - if settings.DEBUG: - log.debug('[lib.dogfood.df_capa_problem] id=%s' % id) - - if not 'coursename' in request.session: - coursename = DOGFOOD_COURSENAME - else: - coursename = request.session['coursename'] - - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - - # Grab the XML corresponding to the request from course.xml - module = 'problem' - - try: - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - except Exception, err: - log.error("[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err) - xml = None - - # if problem of given ID does not exist, then create it - # do this only if course.xml has a section named "DogfoodProblems" - if not xml: - m = re.match('filename([A-Za-z0-9_]+)$', id) # extract problem filename from ID given - if not m: - raise Exception, '[lib.dogfood.df_capa_problem] Illegal problem id %s' % id - pfn = m.group(1) - log.debug('[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn) - - # add problem to course.xml - fn = settings.DATA_DIR + xp + 'course.xml' - xml = etree.parse(fn) - seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure! - if seq == None: - raise Exception, "[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!" - newprob = etree.Element('problem') - newprob.set('type', 'lecture') - newprob.set('showanswer', 'attempted') - newprob.set('rerandomize', 'never') - newprob.set('title', pfn) - newprob.set('filename', pfn) - newprob.set('name', pfn) - seq.append(newprob) - fp = open(fn, 'w') - fp.write(etree.tostring(xml, pretty_print=True)) # write new XML - fp.close() - - # now create new problem file - # update_problem(pfn,'\n\nThis is a new problem\n\n\n',coursename,overwrite=False) - - # reset cache entry - user = request.user - groups = content_parser.user_groups(user) - options = {'dev_content': settings.DEV_CONTENT, - 'groups': groups} - filename = xp + 'course.xml' - cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups)) - log.debug('[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key) - #cache.delete(cache_key) - tree = content_parser.course_xml_process(xml) # add ID tags - cache.set(cache_key, etree.tostring(tree), 60) - # settings.DEFAULT_GROUPS.append('dev') # force content_parser.course_file to not use cache - - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - if not xml: - log.debug("[lib.dogfood.df_capa_problem] problem xml not found!") - - # add problem ID to list so that is_staff check can be bypassed - request.session['dogfood_id'] = id - - # hand over to quickedit to do the rest - return quickedit(request, id=id, qetemplate='dogfood.html', coursename=coursename) - - -def quickedit(request, id=None, qetemplate='quickedit.html', coursename=None): - ''' - quick-edit capa problem. - - Maybe this should be moved into capa/views.py - Or this should take a "module" argument, and the quickedit moved into capa_module. - - id is passed in from url resolution - qetemplate is used by dogfood.views.dj_capa_problem, to override normal template - ''' - print "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY." - print "In deployed use, this will only edit on one server" - print "We need a setting to disable for production where there is" - print "a load balanacer" - - if not request.user.is_staff: - if not ('dogfood_id' in request.session and request.session['dogfood_id'] == id): - return redirect('/') - - if id == 'course.xml': - return quickedit_git_reload(request) - - # get coursename if stored - if not coursename: - coursename = multicourse_settings.get_coursename_from_request(request) - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - - def get_lcp(coursename, id): - # Grab the XML corresponding to the request from course.xml - # create empty student state for this problem, if not previously existing - s = StudentModule.objects.filter(student=request.user, - module_id=id) - student_module_cache = list(s) if s is not None else [] - #if len(s) == 0 or s is None: - # smod=StudentModule(student=request.user, - # module_type = 'problem', - # module_id=id, - # state=instance.get_state()) - # smod.save() - # student_module_cache = [smod] - module = 'problem' - module_xml = etree.XML(content_parser.module_xml(request.user, module, 'id', id, coursename)) - module_id = module_xml.get('id') - log.debug("module_id = %s" % module_id) - (instance, smod, module_type) = get_module(request.user, request, module_xml, student_module_cache, position=None) - log.debug('[dogfood.views] instance=%s' % instance) - lcp = instance.lcp - log.debug('[dogfood.views] lcp=%s' % lcp) - pxml = lcp.tree - pxmls = etree.tostring(pxml, pretty_print=True) - return instance, pxmls - - def old_get_lcp(coursename, id): - # Grab the XML corresponding to the request from course.xml - module = 'problem' - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - - ajax_url = settings.MITX_ROOT_URL + '/modx/' + id + '/' - - # Create the module (instance of capa_module.Module) - system = ModuleSystem(track_function=make_track_function(request), - render_function=None, - render_template=render_to_string, - ajax_url=ajax_url, - filestore=OSFS(settings.DATA_DIR + xp), - ) - instance = xmodule.get_module_class(module)(system, - xml, - id, - state=None) - log.info('ajax_url = ' + instance.ajax_url) - - # create empty student state for this problem, if not previously existing - s = StudentModule.objects.filter(student=request.user, - module_state_key=id) - if len(s) == 0 or s is None: - smod = StudentModule(student=request.user, - module_type='problem', - module_state_key=id, - state=instance.get_instance_state()) - smod.save() - - lcp = instance.lcp - pxml = lcp.tree - pxmls = etree.tostring(pxml, pretty_print=True) - - return instance, pxmls - - instance, pxmls = get_lcp(coursename, id) - - # if there was a POST, then process it - msg = '' - if 'qesubmit' in request.POST: - action = request.POST['qesubmit'] - if "Revert" in action: - msg = "Reverted to original" - elif action == 'Change Problem': - key = 'quickedit_%s' % id - if not key in request.POST: - msg = "oops, missing code key=%s" % key - else: - newcode = request.POST[key] - - # see if code changed - if str(newcode) == str(pxmls) or '\n' + str(newcode) == str(pxmls): - msg = "No changes" - else: - # check new code - isok = False - try: - newxml = etree.fromstring(newcode) - isok = True - except Exception, err: - msg = "Failed to change problem: XML error \"%s\"" % err - - if isok: - filename = instance.lcp.fileobject.name - fp = open(filename, 'w') # TODO - replace with filestore call? - fp.write(newcode) - fp.close() - msg = "Problem changed! (%s)" % filename - instance, pxmls = get_lcp(coursename, id) - - lcp = instance.lcp - - # get the rendered problem HTML - phtml = instance.get_html() - # phtml = instance.get_problem_html() - - context = {'id': id, - 'msg': msg, - 'lcp': lcp, - 'filename': lcp.fileobject.name, - 'pxmls': pxmls, - 'phtml': phtml, - "destroy_js": '', - 'init_js': '', - 'csrf': csrf(request)['csrf_token'], - } - - result = render_to_response(qetemplate, context) - return result - - -def quickedit_git_reload(request): - ''' - reload course.xml and all courseware files for this course, from the git repo. - assumes the git repo has already been setup. - staff only. - ''' - if not request.user.is_staff: - return redirect('/') - - # get coursename if stored - coursename = multicourse_settings.get_coursename_from_request(request) - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - - msg = "" - if 'cancel' in request.POST: - return redirect("/courseware") - - if 'gitupdate' in request.POST: - import os # FIXME - put at top? - #cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/','')) - cmd = "cd ../data%s; ./GITRELOAD '%s'" % (xp, xp.replace('/', '')) - msg += '

cmd: %s

' % cmd - ret = os.popen(cmd).read() - msg += '

%s

' % ret.replace('<', '<') - msg += "

git update done!

" - - context = {'id': id, - 'msg': msg, - 'coursename': coursename, - 'csrf': csrf(request)['csrf_token'], - } - - result = render_to_response("gitupdate.html", context) - return result diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index 4e893d2455..81b497d4f9 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -67,7 +67,7 @@ section.course-index { } .chapter { - width: 100%; + width: 100% !important; @include box-sizing(border-box); padding: 11px 14px; @include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0)); @@ -99,6 +99,8 @@ section.course-index { @include border-radius(0); margin: 0; padding: 9px 0 9px 9px; + overflow: auto; + width: 100%; li { border-bottom: 0; diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 72a4b2cae1..fcbc83d815 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -60,14 +60,12 @@ }); -% if timer_expiration_datetime: +% if timer_expiration_duration: - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - - -<%include file="mathjax_include.html" /> - - - - - - -
- -## ----------------------------------------------------------------------------- -## information - -##
-##

Rendition of your problem code

-##
- -## ----------------------------------------------------------------------------- -## rendered problem display - - - - - - - -
-
- ${phtml} -
-
- - - - - -## - - - -## image input: for clicking on images (see imageinput.html) - - - - - <%block name="js_extra"/> - - - diff --git a/lms/templates/gitupdate.html b/lms/templates/gitupdate.html deleted file mode 100644 index a0cedabeae..0000000000 --- a/lms/templates/gitupdate.html +++ /dev/null @@ -1,32 +0,0 @@ - - -edX gitupdate - - - -
-

edX gitupdate

-
- -

Coursename: ${coursename}

- -% if msg: - - ${msg} - -% else: -

-Do you REALLY want to overwrite all the course.xml + problems + html -files with version from the main git repository? -

- -
- -## - -
-% endif - -

Return to site

- - diff --git a/lms/templates/quickedit.html b/lms/templates/quickedit.html deleted file mode 100644 index bc8e74eb65..0000000000 --- a/lms/templates/quickedit.html +++ /dev/null @@ -1,180 +0,0 @@ -<%namespace name='static' file='static_content.html'/> - - -## ----------------------------------------------------------------------------- -## Template for courseware.views.quickedit -## -## Used for quick-edit link present when viewing capa-format assesment problems. -## ----------------------------------------------------------------------------- - - -## -## - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:css group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: -## -% endif - - - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - -## - - - -<%block name="headextra"/> - - - <%include file="mathjax_include.html" /> - - - - - - - -## ----------------------------------------------------------------------------- -## information and i4x PSL code - -
-

QuickEdit

-
- - -
- -
- - - -
- -${msg|n} - -## ----------------------------------------------------------------------------- -## rendered problem display - - - -
- - - - - - - -
-
-
- ${phtml} -
-
-
- - - - - -## - - - - - - - - <%block name="js_extra"/> - - - diff --git a/lms/urls.py b/lms/urls.py index b25c4d259e..a203d468e7 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -320,10 +320,6 @@ if settings.COURSEWARE_ENABLED: 'courseware.views.static_tab', name="static_tab"), ) -if settings.QUICKEDIT: - urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'dogfood.views.quickedit'),) - urlpatterns += (url(r'^dogfood/(?P[^/]*)$', 'dogfood.views.df_capa_problem'),) - if settings.ENABLE_JASMINE: urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) diff --git a/requirements.txt b/requirements.txt index 0faf2e3ba5..7bfaa11bc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,6 @@ django_nose nosexcover==1.0.7 rednose==0.3.3 GitPython==0.3.2.RC1 -django-override-settings==1.2 mock==0.8.0 PyYAML==3.10 South==0.7.6