course_details.py 8.29 KB
Newer Older
Don Mitchell committed
1 2 3 4 5 6
import re
import logging
import datetime
import json
from json.encoder import JSONEncoder

7 8
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
9
from contentstore.utils import get_modulestore, course_image_url
10
from models.settings import course_grading
11
from xmodule.fields import Date
Don Mitchell committed
12
from xmodule.modulestore.django import loc_mapper
13

14

15
class CourseDetails(object):
Don Mitchell committed
16 17 18 19 20
    def __init__(self, org, course_id, run):
        # still need these for now b/c the client's screen shows these 3 fields
        self.org = org
        self.course_id = course_id
        self.run = run
21
        self.start_date = None  # 'start'
22
        self.end_date = None  # 'end'
23 24
        self.enrollment_start = None
        self.enrollment_end = None
25
        self.syllabus = None  # a pdf file asset
26
        self.short_description = ""
27 28 29
        self.overview = ""  # html to render as the overview
        self.intro_video = None  # a video pointer
        self.effort = None  # int hours/week
30 31
        self.course_image_name = ""
        self.course_image_asset_path = ""  # URL of the course image
Don Mitchell committed
32 33

    @classmethod
34
    def fetch(cls, course_locator):
Don Mitchell committed
35 36 37
        """
        Fetch the course details for the given course from persistence and return a CourseDetails model.
        """
38
        course_old_location = loc_mapper().translate_locator_to_location(course_locator)
Don Mitchell committed
39 40
        descriptor = get_modulestore(course_old_location).get_item(course_old_location)
        course = cls(course_old_location.org, course_old_location.course, course_old_location.name)
Calen Pennington committed
41

42 43 44 45
        course.start_date = descriptor.start
        course.end_date = descriptor.end
        course.enrollment_start = descriptor.enrollment_start
        course.enrollment_end = descriptor.enrollment_end
46 47
        course.course_image_name = descriptor.course_image
        course.course_image_asset_path = course_image_url(descriptor)
Calen Pennington committed
48

Don Mitchell committed
49
        temploc = course_old_location.replace(category='about', name='syllabus')
50
        try:
51
            course.syllabus = get_modulestore(temploc).get_item(temploc).data
52 53 54
        except ItemNotFoundError:
            pass

55 56 57 58 59 60
        temploc = course_old_location.replace(category='about', name='short_description')
        try:
            course.short_description = get_modulestore(temploc).get_item(temploc).data
        except ItemNotFoundError:
            pass

Don Mitchell committed
61
        temploc = temploc.replace(name='overview')
62
        try:
63
            course.overview = get_modulestore(temploc).get_item(temploc).data
64 65
        except ItemNotFoundError:
            pass
Calen Pennington committed
66

Don Mitchell committed
67
        temploc = temploc.replace(name='effort')
68
        try:
69
            course.effort = get_modulestore(temploc).get_item(temploc).data
70 71
        except ItemNotFoundError:
            pass
Calen Pennington committed
72

Don Mitchell committed
73
        temploc = temploc.replace(name='video')
74
        try:
75
            raw_video = get_modulestore(temploc).get_item(temploc).data
Calen Pennington committed
76
            course.intro_video = CourseDetails.parse_video_tag(raw_video)
77 78
        except ItemNotFoundError:
            pass
Calen Pennington committed
79

Don Mitchell committed
80
        return course
Calen Pennington committed
81

82
    @classmethod
83
    def update_about_item(cls, course_old_location, about_key, data, course, user):
84 85 86 87 88 89 90 91 92 93 94 95 96 97
        """
        Update the about item with the new data blob. If data is None, then
        delete the about item.
        """
        temploc = Location(course_old_location).replace(category='about', name=about_key)
        store = get_modulestore(temploc)
        if data is None:
            store.delete_item(temploc)
        else:
            try:
                about_item = store.get_item(temploc)
            except ItemNotFoundError:
                about_item = store.create_xmodule(temploc, system=course.runtime)
            about_item.data = data
98
            store.update_item(about_item, user.id)
99 100

    @classmethod
101
    def update_from_json(cls, course_locator, jsondict, user):
102 103 104
        """
        Decode the json into CourseDetails and save any changed attrs to the db
        """
105
        course_old_location = loc_mapper().translate_locator_to_location(course_locator)
Don Mitchell committed
106
        descriptor = get_modulestore(course_old_location).get_item(course_old_location)
Calen Pennington committed
107

108
        dirty = False
109

110 111 112 113 114 115
        # In the descriptor's setter, the date is converted to JSON using Date's to_json method.
        # Calling to_json on something that is already JSON doesn't work. Since reaching directly
        # into the model is nasty, convert the JSON Date to a Python date, which is what the
        # setter expects as input.
        date = Date()

116
        if 'start_date' in jsondict:
117
            converted = date.from_json(jsondict['start_date'])
118 119 120
        else:
            converted = None
        if converted != descriptor.start:
121
            dirty = True
122
            descriptor.start = converted
Calen Pennington committed
123

124
        if 'end_date' in jsondict:
125
            converted = date.from_json(jsondict['end_date'])
126 127 128 129
        else:
            converted = None

        if converted != descriptor.end:
130
            dirty = True
131
            descriptor.end = converted
Calen Pennington committed
132

133
        if 'enrollment_start' in jsondict:
134
            converted = date.from_json(jsondict['enrollment_start'])
135 136 137 138
        else:
            converted = None

        if converted != descriptor.enrollment_start:
139
            dirty = True
140
            descriptor.enrollment_start = converted
Calen Pennington committed
141

142
        if 'enrollment_end' in jsondict:
143
            converted = date.from_json(jsondict['enrollment_end'])
144 145 146 147
        else:
            converted = None

        if converted != descriptor.enrollment_end:
148
            dirty = True
149
            descriptor.enrollment_end = converted
Calen Pennington committed
150

151 152 153 154
        if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image:
            descriptor.course_image = jsondict['course_image_name']
            dirty = True

155
        if dirty:
156
            get_modulestore(course_old_location).update_item(descriptor, user.id)
Calen Pennington committed
157

158 159
        # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
        # to make faster, could compare against db or could have client send over a list of which fields changed.
160
        for about_type in ['syllabus', 'overview', 'effort', 'short_description']:
161
            cls.update_about_item(course_old_location, about_type, jsondict[about_type], descriptor, user)
162

163
        recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
164
        cls.update_about_item(course_old_location, 'video', recomposed_video_tag, descriptor, user)
Calen Pennington committed
165

Don Mitchell committed
166
        # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm
167
        # it persisted correctly
168
        return CourseDetails.fetch(course_locator)
Calen Pennington committed
169

170 171 172 173 174 175 176 177 178
    @staticmethod
    def parse_video_tag(raw_video):
        """
        Because the client really only wants the author to specify the youtube key, that's all we send to and get from the client.
        The problem is that the db stores the html markup as well (which, of course, makes any sitewide changes to how we do videos
        next to impossible.)
        """
        if not raw_video:
            return None
Calen Pennington committed
179

180
        keystring_matcher = re.search(r'(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
181
        if keystring_matcher is None:
182
            keystring_matcher = re.search(r'<?=\d+:[a-zA-Z0-9_-]+', raw_video)
Calen Pennington committed
183

184 185 186
        if keystring_matcher:
            return keystring_matcher.group(0)
        else:
187
            logging.warn("ignoring the content because it doesn't not conform to expected pattern: " + raw_video)
188
            return None
Calen Pennington committed
189

190 191 192 193
    @staticmethod
    def recompose_video_tag(video_key):
        # TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing
        # the right thing
194 195
        result = None
        if video_key:
196
            result = '<iframe width="560" height="315" src="//www.youtube.com/embed/' + \
197
                video_key + '?rel=0" frameborder="0" allowfullscreen=""></iframe>'
198 199
        return result

Calen Pennington committed
200

Don Mitchell committed
201
# TODO move to a more general util?
Don Mitchell committed
202
class CourseSettingsEncoder(json.JSONEncoder):
Don Mitchell committed
203 204 205
    """
    Serialize CourseDetails, CourseGradingModel, datetime, and old Locations
    """
206
    def default(self, obj):
Don Mitchell committed
207
        if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)):
208 209 210
            return obj.__dict__
        elif isinstance(obj, Location):
            return obj.dict()
211
        elif isinstance(obj, datetime.datetime):
212
            return Date().to_json(obj)
213
        else:
214
            return JSONEncoder.default(self, obj)