course_details.py 7.07 KB
Newer Older
1 2 3 4 5
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
import json
from json.encoder import JSONEncoder
6
import time
Don Mitchell committed
7
from contentstore.utils import get_modulestore
8
from util.converters import jsdate_to_time, time_to_date
Don Mitchell committed
9
from cms.djangoapps.models.settings import course_grading
10
from cms.djangoapps.contentstore.utils import update_item
11
import re
12
import logging
13

14

15
class CourseDetails(object):
Don Mitchell committed
16 17
    def __init__(self, location):
        self.course_location = location    # a Location obj
18 19 20 21
        self.start_date = None  # 'start'
        self.end_date = None    # 'end'
        self.enrollment_start = None
        self.enrollment_end = None
Don Mitchell committed
22 23 24 25 26 27 28 29 30 31
        self.syllabus = None    # a pdf file asset
        self.overview = ""      # html to render as the overview
        self.intro_video = None    # a video pointer
        self.effort = None      # int hours/week

    @classmethod
    def fetch(cls, course_location):
        """
        Fetch the course details for the given course from persistence and return a CourseDetails model.
        """
32 33 34
        if not isinstance(course_location, Location):
            course_location = Location(course_location)
            
Don Mitchell committed
35 36
        course = cls(course_location)
        
Don Mitchell committed
37
        descriptor = get_modulestore(course_location).get_item(course_location)
38 39 40 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
        
        temploc = course_location._replace(category='about', name='syllabus')
        try:
Don Mitchell committed
46
            course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
47 48 49
        except ItemNotFoundError:
            pass

50
        temploc = temploc._replace(name='overview')
51
        try:
Don Mitchell committed
52
            course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
53 54 55
        except ItemNotFoundError:
            pass
        
56
        temploc = temploc._replace(name='effort')
57
        try:
Don Mitchell committed
58
            course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
59 60 61
        except ItemNotFoundError:
            pass
        
62
        temploc = temploc._replace(name='video')
63
        try:
64 65
            raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
            course.intro_video = CourseDetails.parse_video_tag(raw_video) 
66 67
        except ItemNotFoundError:
            pass
Don Mitchell committed
68 69 70
        
        return course
        
71
    @classmethod
72
    def update_from_json(cls, jsondict):
73 74 75 76 77 78
        """
        Decode the json into CourseDetails and save any changed attrs to the db
        """
        ## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore        
        course_location = jsondict['course_location']
        ## Will probably want to cache the inflight courses because every blur generates an update
Don Mitchell committed
79
        descriptor = get_modulestore(course_location).get_item(course_location)
80 81
        
        dirty = False
82

83 84 85 86 87
        if 'start_date' in jsondict:
            converted = jsdate_to_time(jsondict['start_date'])
        else:
            converted = None
        if converted != descriptor.start:
88
            dirty = True
89
            descriptor.start = converted
90
            
91 92 93 94 95 96
        if 'end_date' in jsondict:
            converted = jsdate_to_time(jsondict['end_date'])
        else:
            converted = None

        if converted != descriptor.end:
97
            dirty = True
98
            descriptor.end = converted
99
            
100 101 102 103 104 105
        if 'enrollment_start' in jsondict:
            converted = jsdate_to_time(jsondict['enrollment_start'])
        else:
            converted = None

        if converted != descriptor.enrollment_start:
106
            dirty = True
107
            descriptor.enrollment_start = converted
108
            
109 110 111 112 113 114
        if 'enrollment_end' in jsondict:
            converted = jsdate_to_time(jsondict['enrollment_end'])
        else:
            converted = None

        if converted != descriptor.enrollment_end:
115
            dirty = True
116
            descriptor.enrollment_end = converted
117 118
            
        if dirty:
Don Mitchell committed
119
            get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
120 121 122
            
        # 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.
123
        temploc = Location(course_location)._replace(category='about', name='syllabus')
124
        update_item(temploc, jsondict['syllabus'])
125

126
        temploc = temploc._replace(name='overview')
127
        update_item(temploc, jsondict['overview'])
128
        
129
        temploc = temploc._replace(name='effort')
130
        update_item(temploc, jsondict['effort'])
131
        
132
        temploc = temploc._replace(name='video')
133 134
        recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
        update_item(temploc, recomposed_video_tag)
135 136 137 138 139
        
                    
        # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
        # it persisted correctly
        return CourseDetails.fetch(course_location)
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
    
    @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
         
        keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
        if keystring_matcher is None:
            keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video)
        
        if keystring_matcher:
            return keystring_matcher.group(0)
        else:
158
            logging.warn("ignoring the content because it doesn't not conform to expected pattern: " + raw_video)
159 160 161 162 163 164
            return None
    
    @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
165 166 167 168
        result = None
        if video_key:
            result = '<iframe width="560" height="315" src="http://www.youtube.com/embed/' + \
                video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
169 170 171
        return result

    
Don Mitchell committed
172 173 174

# TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder):
175
    def default(self, obj):
Don Mitchell committed
176
        if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel):
177 178 179
            return obj.__dict__
        elif isinstance(obj, Location):
            return obj.dict()
180 181
        elif isinstance(obj, time.struct_time):
            return time_to_date(obj)
182
        else:
183
            return JSONEncoder.default(self, obj)