course_info_model.py 6.67 KB
Newer Older
1
import re
2
import logging
3 4
from lxml import html, etree
from django.http import HttpResponseBadRequest
5
import django.utils
6 7
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
8

9 10 11
# # 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__)
Calen Pennington committed
12 13


14
def get_course_updates(location, provided_id):
15 16
    """
    Retrieve the relevant course_info updates and unpack into the model which the client expects:
17
    [{id : index, date : string, content : html string}]
18 19 20 21
    """
    try:
        course_updates = modulestore('direct').get_item(location)
    except ItemNotFoundError:
22 23
        modulestore('direct').create_and_save_xmodule(location)
        course_updates = modulestore('direct').get_item(location)
24 25 26

    # current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
    location_base = course_updates.location.url()
Calen Pennington committed
27

28 29
    # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
    try:
30 31 32 33 34
        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("<ol><li>" + escaped + "</li></ol>")
Calen Pennington committed
35

36 37
    # Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
    course_upd_collection = []
38
    provided_id = get_idx(provided_id) if provided_id is not None else None
39
    if course_html_parsed.tag == 'ol':
40 41
        # 0 is the newest
        for idx, update in enumerate(course_html_parsed):
42 43 44
            if len(update) > 0:
                content = _course_info_content(update)
                # make the id on the client be 1..len w/ 1 being the oldest and len being the newest
45 46 47 48 49 50 51 52 53 54
                computed_id = len(course_html_parsed) - idx
                payload = {
                    "id": computed_id,
                    "date": update.findtext("h2"),
                    "content": content
                }
                if provided_id is None:
                    course_upd_collection.append(payload)
                elif provided_id == computed_id:
                    return payload
Calen Pennington committed
55

56 57
    return course_upd_collection

Calen Pennington committed
58

59 60 61 62
def update_course_updates(location, update, passed_id=None):
    """
    Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if
    it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index
Calen Pennington committed
63
    into the html structure.
64 65 66 67
    """
    try:
        course_updates = modulestore('direct').get_item(location)
    except ItemNotFoundError:
68 69
        modulestore('direct').create_and_save_xmodule(location)
        course_updates = modulestore('direct').get_item(location)
Calen Pennington committed
70

71 72
    # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
    try:
73 74 75 76 77
        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("<ol><li>" + escaped + "</li></ol>")
78

79 80 81 82 83 84 85 86 87 88 89 90
    # if there's no ol, create it
    if course_html_parsed.tag != 'ol':
        # surround whatever's there w/ an ol
        if course_html_parsed.tag != 'li':
            # but first wrap in an li
            li = etree.Element('li')
            li.append(course_html_parsed)
            course_html_parsed = li
        ol = etree.Element('ol')
        ol.append(course_html_parsed)
        course_html_parsed = ol

91
    # No try/catch b/c failure generates an error back to client
92
    new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
Calen Pennington committed
93

94 95 96 97 98 99 100 101 102 103 104 105 106
    # ??? Should this use the id in the json or in the url or does it matter?
    if passed_id is not None:
        idx = get_idx(passed_id)
        # idx is count from end of list
        course_html_parsed[-idx] = new_html_parsed
    else:
        course_html_parsed.insert(0, new_html_parsed)
        idx = len(course_html_parsed)

    # update db record
    course_updates.data = html.tostring(course_html_parsed)
    modulestore('direct').update_item(location, course_updates.data)

107 108 109 110 111
    return {
        "id": idx,
        "date": update['date'],
        "content": _course_info_content(new_html_parsed),
    }
112 113 114 115 116 117 118 119 120 121 122 123 124


def _course_info_content(html_parsed):
    """
    Constructs the HTML for the course info update, not including the header.
    """
    if len(html_parsed) == 1:
        # could enforce that update[0].tag == 'h2'
        content = html_parsed[0].tail
    else:
        content = html_parsed[0].tail if html_parsed[0].tail is not None else ""
        content += "\n".join([html.tostring(ele) for ele in html_parsed[1:]])
    return content
125

126

127
# pylint: disable=unused-argument
128 129 130 131 132 133
def delete_course_update(location, update, passed_id):
    """
    Delete the given course_info update from the db.
    Returns the resulting course_updates b/c their ids change.
    """
    if not passed_id:
134
        return HttpResponseBadRequest()
Calen Pennington committed
135

136 137 138
    try:
        course_updates = modulestore('direct').get_item(location)
    except ItemNotFoundError:
139
        return HttpResponseBadRequest()
Calen Pennington committed
140

141 142 143
    # 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:
144 145 146 147 148
        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("<ol><li>" + escaped + "</li></ol>")
Calen Pennington committed
149

150 151
    if course_html_parsed.tag == 'ol':
        # ??? Should this use the id in the json or in the url or does it matter?
152 153 154
        idx = get_idx(passed_id)
        # idx is count from end of list
        element_to_delete = course_html_parsed[-idx]
155
        if element_to_delete is not None:
156
            course_html_parsed.remove(element_to_delete)
157 158

        # update db record
159
        course_updates.data = html.tostring(course_html_parsed)
160
        store = modulestore('direct')
161
        store.update_item(location, course_updates.data)
Calen Pennington committed
162

163
    return get_course_updates(location, None)
Calen Pennington committed
164 165


166 167 168 169
def get_idx(passed_id):
    """
    From the url w/ idx appended, get the idx.
    """
170
    idx_matcher = re.search(r'.*?/?(\d+)$', passed_id)
171
    if idx_matcher:
Calen Pennington committed
172
        return int(idx_matcher.group(1))
173 174
    else:
        return None