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 = '
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,'
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 - -##