From bd2dd9408d2402e09b51f9f53faa304eb351cef7 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Wed, 20 Mar 2013 17:49:03 -0400 Subject: [PATCH] Unit tests and minor fixes to course_info_updates. Main fix was returning the content in same form as it was saved rather than parroting it back. --- .../contentstore/course_info_model.py | 52 +++++--- .../contentstore/tests/test_course_updates.py | 116 ++++++++++++++++-- cms/djangoapps/contentstore/views.py | 40 +++--- 3 files changed, 163 insertions(+), 45 deletions(-) diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 8c8aed549d..589db4ac56 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -1,13 +1,15 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from lxml import html, etree +from lxml import html import re from django.http import HttpResponseBadRequest import logging +import django.utils -## TODO store as array of { date, content } and override course_info_module.definition_from_xml -## This should be in a class which inherits from XmlDescriptor +# # TODO store as array of { date, content } and override course_info_module.definition_from_xml +# # This should be in a class which inherits from XmlDescriptor +log = logging.getLogger(__name__) def get_course_updates(location): @@ -26,9 +28,11 @@ def get_course_updates(location): # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: - course_html_parsed = etree.fromstring(course_updates.data) - except etree.XMLSyntaxError: - course_html_parsed = etree.fromstring("
    ") + course_html_parsed = html.fromstring(course_updates.data) + except: + log.error("Cannot parse: " + course_updates.data) + escaped = django.utils.html.escape(course_updates.data) + course_html_parsed = html.fromstring("
    1. " + escaped + "
    ") # Confirm that root is
      , iterate over
    1. , pull out

      subs and then rest of val course_upd_collection = [] @@ -64,9 +68,11 @@ def update_course_updates(location, update, passed_id=None): # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: - course_html_parsed = etree.fromstring(course_updates.data) - except etree.XMLSyntaxError: - course_html_parsed = etree.fromstring("
        ") + course_html_parsed = html.fromstring(course_updates.data) + except: + log.error("Cannot parse: " + course_updates.data) + escaped = django.utils.html.escape(course_updates.data) + course_html_parsed = html.fromstring("
        1. " + escaped + "
        ") # No try/catch b/c failure generates an error back to client new_html_parsed = html.fromstring('
      1. ' + update['date'] + '

        ' + update['content'] + '
      2. ') @@ -85,12 +91,19 @@ def update_course_updates(location, update, passed_id=None): passed_id = course_updates.location.url() + "/" + str(idx) # update db record - course_updates.data = etree.tostring(course_html_parsed) + course_updates.data = html.tostring(course_html_parsed) modulestore('direct').update_item(location, course_updates.data) - return {"id" : passed_id, - "date" : update['date'], - "content" :update['content']} + 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:]]) + + return {"id": passed_id, + "date": update['date'], + "content": content} + def delete_course_update(location, update, passed_id): """ @@ -108,9 +121,11 @@ def delete_course_update(location, update, passed_id): # TODO use delete_blank_text parser throughout and cache as a static var in a class # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: - course_html_parsed = etree.fromstring(course_updates.data) - except etree.XMLSyntaxError: - course_html_parsed = etree.fromstring("
          ") + course_html_parsed = html.fromstring(course_updates.data) + except: + log.error("Cannot parse: " + course_updates.data) + escaped = django.utils.html.escape(course_updates.data) + course_html_parsed = html.fromstring("
          1. " + escaped + "
          ") if course_html_parsed.tag == 'ol': # ??? Should this use the id in the json or in the url or does it matter? @@ -121,7 +136,7 @@ def delete_course_update(location, update, passed_id): course_html_parsed.remove(element_to_delete) # update db record - course_updates.data = etree.tostring(course_html_parsed) + course_updates.data = html.tostring(course_html_parsed) store = modulestore('direct') store.update_item(location, course_updates.data) @@ -132,7 +147,6 @@ def get_idx(passed_id): """ From the url w/ idx appended, get the idx. """ - # TODO compile this regex into a class static and reuse for each call - idx_matcher = re.search(r'.*/(\d+)$', passed_id) + idx_matcher = re.search(r'.*?/?(\d+)$', passed_id) if idx_matcher: return int(idx_matcher.group(1)) diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py index 6a3a1e21f7..29f387b3e6 100644 --- a/cms/djangoapps/contentstore/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/tests/test_course_updates.py @@ -1,31 +1,127 @@ from contentstore.tests.test_course_settings import CourseTestCase from django.core.urlresolvers import reverse import json +from webob.exc import HTTPServerError +from django.http import HttpResponseBadRequest class CourseUpdateTest(CourseTestCase): def test_course_update(self): # first get the update to force the creation - url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'name': self.course_location.name}) + url = reverse('course_info', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'name': self.course_location.name}) self.client.get(url) - content = '' + init_content = '' payload = {'content': content, 'date': 'January 8, 2013'} - url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'provided_id': ''}) + url = reverse('course_info_json', kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) resp = self.client.post(url, json.dumps(payload), "application/json") payload = json.loads(resp.content) - self.assertHTMLEqual(content, payload['content'], "single iframe") + self.assertHTMLEqual(payload['content'], content) - url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, - 'provided_id': payload['id']}) - content += '
          div

          p

          ' + first_update_url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': payload['id']}) + content += '
          div

          p

          ' payload['content'] = content + resp = self.client.post(first_update_url, json.dumps(payload), + "application/json") + + self.assertHTMLEqual(content, json.loads(resp.content)['content'], + "iframe w/ div") + + # now put in an evil update + content = '
            ' + payload = {'content': content, + 'date': 'January 11, 2013'} + url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) + resp = self.client.post(url, json.dumps(payload), "application/json") - self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div") + payload = json.loads(resp.content) + + self.assertHTMLEqual(content, payload['content'], "self closing ol") + + url = reverse('course_info_json', + kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) + resp = self.client.get(url) + payload = json.loads(resp.content) + self.assertTrue(len(payload) == 2) + + # now try to update a non-existent update + url = reverse('course_info_json', kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': '9'}) + content = 'blah blah' + payload = {'content': content, + 'date': 'January 21, 2013'} + self.assertContains( + self.client.post(url, json.dumps(payload), "application/json"), + 'Failed to save', status_code=400) + + # update w/ malformed html + content = 'error' + payload = {'content': content, + 'date': 'January 11, 2013'} + url = reverse('course_info_json', kwargs={'org': self.course_location.org, + 'course': self.course_location.course, + 'provided_id': ''}) + + resp = self.client.post(url, json.dumps(payload), "application/json") + + payload = json.loads(resp.content) + + self.assertContains( + self.client.post(url, json.dumps(payload), "application/json"), + '