courses.py 10.4 KB
Newer Older
1
from collections import defaultdict
2 3
from fs.errors import ResourceNotFoundError
import logging
4
import inspect
5
import re
6

7
from path import path
8
from django.http import Http404
9
from django.conf import settings
10
from .module_render import get_module
11
from xmodule.course_module import CourseDescriptor
Chris Dodge committed
12
from xmodule.modulestore import Location, XML_MODULESTORE_TYPE
13
from xmodule.modulestore.django import modulestore
14
from xmodule.contentstore.content import StaticContent
15
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
Calen Pennington committed
16
from courseware.model_data import FieldDataCache
17
from static_replace import replace_static_urls
18
from courseware.access import has_access
19
import branding
20

21
log = logging.getLogger(__name__)
22

Calen Pennington committed
23

24 25 26 27 28 29 30 31 32 33 34 35 36 37
def get_request_for_thread():
    """Walk up the stack, return the nearest first argument named "request"."""
    frame = None
    try:
        for f in inspect.stack()[1:]:
            frame = f[0]
            code = frame.f_code
            if code.co_varnames[:1] == ("request",):
                return frame.f_locals["request"]
            elif code.co_varnames[:2] == ("self", "request",):
                return frame.f_locals["request"]
    finally:
        del frame

38

39
def get_course_by_id(course_id, depth=0):
40
    """
41
    Given a course id, return the corresponding course descriptor.
42

43
    If course_id is not valid, raises a 404.
44
    depth: The number of levels of children for the modulestore to cache. None means infinite depth
45
    """
46 47
    try:
        course_loc = CourseDescriptor.id_to_location(course_id)
48
        return modulestore().get_instance(course_id, course_loc, depth=depth)
49 50
    except (KeyError, ItemNotFoundError):
        raise Http404("Course not found.")
51 52
    except InvalidLocationError:
        raise Http404("Invalid location")
53

54
def get_course_with_access(user, course_id, action, depth=0):
55 56 57 58
    """
    Given a course_id, look up the corresponding course descriptor,
    check that the user has the access to perform the specified action
    on the course, and return the descriptor.
59

60
    Raises a 404 if the course_id is invalid, or the user doesn't have access.
61 62

    depth: The number of levels of children for the modulestore to cache. None means infinite depth
63
    """
64
    course = get_course_by_id(course_id, depth=depth)
65 66 67 68
    if not has_access(user, course, action):
        # Deliberately return a non-specific error message to avoid
        # leaking info about access control settings
        raise Http404("Course not found.")
69
    return course
70

71

72 73 74 75 76 77 78 79 80 81
def get_opt_course_with_access(user, course_id, action):
    """
    Same as get_course_with_access, except that if course_id is None,
    return None without performing any access checks.
    """
    if course_id is None:
        return None
    return get_course_with_access(user, course_id, action)


82
def course_image_url(course):
83 84
    """Try to look up the image url for the course.  If it's not found,
    log an error and return the dead link"""
Calen Pennington committed
85 86
    if course.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
        return '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
87
    else:
88
        loc = course.location._replace(tag='c4x', category='asset', name=course.course_image)
Chris Dodge committed
89 90
        _path = StaticContent.get_url_path_from_location(loc)
        return _path
91 92


93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
def find_file(fs, dirs, filename):
    """
    Looks for a filename in a list of dirs on a filesystem, in the specified order.

    fs: an OSFS filesystem
    dirs: a list of path objects
    filename: a string

    Returns d / filename if found in dir d, else raises ResourceNotFoundError.
    """
    for d in dirs:
        filepath = path(d) / filename
        if fs.exists(filepath):
            return filepath
    raise ResourceNotFoundError("Could not find {0}".format(filename))

109

110 111
def get_course_about_section(course, section_key):
    """
Victor Shnayder committed
112 113 114
    This returns the snippet of html to be rendered on the course about page,
    given the key for the section.

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
    Valid keys:
    - overview
    - title
    - university
    - number
    - short_description
    - description
    - key_dates (includes start, end, exams, etc)
    - video
    - course_staff_short
    - course_staff_extended
    - requirements
    - syllabus
    - textbook
    - faq
    - more_info
131
    - ocw_links
132 133
    """

Victor Shnayder committed
134 135 136
    # Many of these are stored as html files instead of some semantic
    # markup. This can change without effecting this interface when we find a
    # good format for defining so many snippets of text/html.
137 138

# TODO: Remove number, instructors from this list
Victor Shnayder committed
139 140 141 142
    if section_key in ['short_description', 'description', 'key_dates', 'video',
                       'course_staff_short', 'course_staff_extended',
                       'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
                       'number', 'instructors', 'overview',
143
                       'effort', 'end_date', 'prerequisites', 'ocw_links']:
144

145
        try:
146 147 148

            request = get_request_for_thread()

149
            loc = course.location._replace(category='about', name=section_key)
150 151

            # Use an empty cache
Calen Pennington committed
152
            field_data_cache = FieldDataCache([], course.id, request.user)
153 154 155 156
            about_module = get_module(
                request.user,
                request,
                loc,
Calen Pennington committed
157
                field_data_cache,
158 159
                course.id,
                not_found_ok=True,
160
                wrap_xmodule_display=False,
Calen Pennington committed
161
                static_asset_path=course.static_asset_path
162
            )
163 164 165

            html = ''

166
            if about_module is not None:
167
                html = about_module.runtime.render(about_module, None, 'student_view').content
168 169

            return html
170 171

        except ItemNotFoundError:
Victor Shnayder committed
172 173
            log.warning("Missing about section {key} in course {url}".format(
                key=section_key, url=course.location.url()))
174 175
            return None
    elif section_key == "title":
176
        return course.display_name_with_default
177
    elif section_key == "university":
Chris Dodge committed
178
        return course.display_org_with_default
179
    elif section_key == "number":
Chris Dodge committed
180
        return course.display_number_with_default
181 182 183

    raise KeyError("Invalid about key " + str(section_key))

184

185

186
def get_course_info_section(request, course, section_key):
187
    """
Victor Shnayder committed
188 189 190
    This returns the snippet of html to be rendered on the course info page,
    given the key for the section.

191 192 193 194 195 196 197
    Valid keys:
    - handouts
    - guest_handouts
    - updates
    - guest_updates
    """

198

199
    loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
200 201

    # Use an empty cache
Calen Pennington committed
202
    field_data_cache = FieldDataCache([], course.id, request.user)
203 204 205 206
    info_module = get_module(
        request.user,
        request,
        loc,
Calen Pennington committed
207
        field_data_cache,
208
        course.id,
209
        wrap_xmodule_display=False,
Calen Pennington committed
210
        static_asset_path=course.static_asset_path
211 212
    )

213
    html = ''
214

215
    if info_module is not None:
216
        html = info_module.runtime.render(info_module, None, 'student_view').content
217

218
    return html
219

220

221 222
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
#       arjun will address this by the end of October if no one does so prior to
223
#       then.
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
def get_course_syllabus_section(course, section_key):
    """
    This returns the snippet of html to be rendered on the syllabus page,
    given the key for the section.

    Valid keys:
    - syllabus
    - guest_syllabus
    """

    # Many of these are stored as html files instead of some semantic
    # markup. This can change without effecting this interface when we find a
    # good format for defining so many snippets of text/html.

    if section_key in ['syllabus', 'guest_syllabus']:
        try:
240 241 242 243 244
            fs = course.system.resources_fs
            # first look for a run-specific version
            dirs = [path("syllabus") / course.url_name, path("syllabus")]
            filepath = find_file(fs, dirs, section_key + ".html")
            with fs.open(filepath) as htmlFile:
245 246
                return replace_static_urls(
                    htmlFile.read().decode('utf-8'),
247
                    getattr(course, 'data_dir', None),
248
                    course_id=course.location.course_id,
Calen Pennington committed
249
                    static_asset_path=course.static_asset_path,
250
                )
251 252 253 254 255 256 257
        except ResourceNotFoundError:
            log.exception("Missing syllabus section {key} in course {url}".format(
                key=section_key, url=course.location.url()))
            return "! Syllabus missing !"

    raise KeyError("Invalid about key " + str(section_key))

258 259

def get_courses_by_university(user, domain=None):
260 261 262 263 264 265
    '''
    Returns dict of lists of courses available, keyed by course.org (ie university).
    Courses are sorted by course.number.
    '''
    # TODO: Clean up how 'error' is done.
    # filter out any courses that errored.
266
    visible_courses = get_courses(user, domain)
267

268
    universities = defaultdict(list)
269
    for course in visible_courses:
270
        universities[course.org].append(course)
271

272
    return universities
273 274 275 276 277 278 279 280 281


def get_courses(user, domain=None):
    '''
    Returns a list of courses available, sorted by course.number
    '''
    courses = branding.get_visible_courses(domain)
    courses = [c for c in courses if has_access(user, c, 'see_exists')]

Calen Pennington committed
282
    courses = sorted(courses, key=lambda course: course.number)
283 284 285 286 287 288 289 290 291 292 293 294 295 296

    return courses


def sort_by_announcement(courses):
    """
    Sorts a list of courses by their announcement date. If the date is
    not available, sort them by their start date.
    """

    # Sort courses by how far are they from they start day
    key = lambda course: course.sorting_score
    courses = sorted(courses, key=key)

297
    return courses
298

299

300 301 302 303 304 305
def get_cms_course_link_by_id(course_id):
    """
    Returns a proto-relative link to course_index for editing the course in cms, assuming that the course is actually
    cms-backed. If course_id is improperly formatted, just return the root of the cms
    """
    format_str = r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<name>[^/]+)$'
306 307 308
    host = "//{}/".format(settings.CMS_BASE)  # protocol-relative
    m_obj = re.match(format_str, course_id)
    if m_obj:
309
        return "{host}{org}/{course}/course/{name}".format(host=host,
310 311 312
                                                           org=m_obj.group('org'),
                                                           course=m_obj.group('course'),
                                                           name=m_obj.group('name'))
313
    return host