course_details.py 8.02 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
from opaque_keys.edx.locations import Location
8
from xmodule.modulestore.exceptions import ItemNotFoundError
9
from contentstore.utils import course_image_url
10
from models.settings import course_grading
11
from xmodule.fields import Date
12
from xmodule.modulestore.django import modulestore
13

14
class CourseDetails(object):
Don Mitchell committed
15 16 17 18 19
    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
20
        self.start_date = None  # 'start'
21
        self.end_date = None  # 'end'
22 23
        self.enrollment_start = None
        self.enrollment_end = None
24
        self.syllabus = None  # a pdf file asset
25
        self.short_description = ""
26 27 28
        self.overview = ""  # html to render as the overview
        self.intro_video = None  # a video pointer
        self.effort = None  # int hours/week
29 30
        self.course_image_name = ""
        self.course_image_asset_path = ""  # URL of the course image
Don Mitchell committed
31 32

    @classmethod
33
    def fetch(cls, course_key):
Don Mitchell committed
34 35 36
        """
        Fetch the course details for the given course from persistence and return a CourseDetails model.
        """
37
        descriptor = modulestore().get_course(course_key)
38 39 40 41 42 43 44 45 46 47
        course_details = cls(course_key.org, course_key.course, course_key.run)

        course_details.start_date = descriptor.start
        course_details.end_date = descriptor.end
        course_details.enrollment_start = descriptor.enrollment_start
        course_details.enrollment_end = descriptor.enrollment_end
        course_details.course_image_name = descriptor.course_image
        course_details.course_image_asset_path = course_image_url(descriptor)

        temploc = course_key.make_usage_key('about', 'syllabus')
48
        try:
49
            course_details.syllabus = modulestore().get_item(temploc).data
50 51 52
        except ItemNotFoundError:
            pass

53
        temploc = course_key.make_usage_key('about', 'short_description')
54
        try:
55
            course_details.short_description = modulestore().get_item(temploc).data
56 57 58
        except ItemNotFoundError:
            pass

59
        temploc = course_key.make_usage_key('about', 'overview')
60
        try:
61
            course_details.overview = modulestore().get_item(temploc).data
62 63
        except ItemNotFoundError:
            pass
Calen Pennington committed
64

65
        temploc = course_key.make_usage_key('about', 'effort')
66
        try:
67
            course_details.effort = modulestore().get_item(temploc).data
68 69
        except ItemNotFoundError:
            pass
Calen Pennington committed
70

71
        temploc = course_key.make_usage_key('about', 'video')
72
        try:
73
            raw_video = modulestore().get_item(temploc).data
74
            course_details.intro_video = CourseDetails.parse_video_tag(raw_video)
75 76
        except ItemNotFoundError:
            pass
Calen Pennington committed
77

78
        return course_details
Calen Pennington committed
79

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

    @classmethod
99
    def update_from_json(cls, course_key, jsondict, user):
100 101 102
        """
        Decode the json into CourseDetails and save any changed attrs to the db
        """
103
        module_store = modulestore()
104
        descriptor = module_store.get_course(course_key)
Calen Pennington committed
105

106
        dirty = False
107

108 109 110 111 112 113
        # 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()

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

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

        if converted != descriptor.end:
128
            dirty = True
129
            descriptor.end = converted
Calen Pennington committed
130

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

        if converted != descriptor.enrollment_start:
137
            dirty = True
138
            descriptor.enrollment_start = converted
Calen Pennington committed
139

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

        if converted != descriptor.enrollment_end:
146
            dirty = True
147
            descriptor.enrollment_end = converted
Calen Pennington committed
148

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

153
        if dirty:
154
            module_store.update_item(descriptor, user.id)
Calen Pennington committed
155

156 157
        # 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.
158
        for about_type in ['syllabus', 'overview', 'effort', 'short_description']:
159
            cls.update_about_item(course_key, about_type, jsondict[about_type], descriptor, user)
160

161
        recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
162
        cls.update_about_item(course_key, 'video', recomposed_video_tag, descriptor, user)
Calen Pennington committed
163

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

168 169 170 171 172 173 174 175 176
    @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
177

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

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

188 189 190 191
    @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
192 193
        result = None
        if video_key:
194
            result = '<iframe width="560" height="315" src="//www.youtube.com/embed/' + \
195
                video_key + '?rel=0" frameborder="0" allowfullscreen=""></iframe>'
196 197
        return result

Calen Pennington committed
198

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