courses.py 11.1 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
def get_course(course_id, depth=0):
    """
    Given a course id, return the corresponding course descriptor.

    If course_id is not valid, raises a ValueError.  This is appropriate
    for internal use.

    depth: The number of levels of children for the modulestore to cache.
    None means infinite depth.  Default is to fetch no children.
    """
    try:
        course_loc = CourseDescriptor.id_to_location(course_id)
        return modulestore().get_instance(course_id, course_loc, depth=depth)
    except (KeyError, ItemNotFoundError):
        raise ValueError("Course not found: {}".format(course_id))
    except InvalidLocationError:
        raise ValueError("Invalid location: {}".format(course_id))


58
def get_course_by_id(course_id, depth=0):
59
    """
60
    Given a course id, return the corresponding course descriptor.
61

62
    If course_id is not valid, raises a 404.
63

64
    depth: The number of levels of children for the modulestore to cache. None means infinite depth
65
    """
66 67
    try:
        course_loc = CourseDescriptor.id_to_location(course_id)
68
        return modulestore().get_instance(course_id, course_loc, depth=depth)
69 70
    except (KeyError, ItemNotFoundError):
        raise Http404("Course not found.")
71 72
    except InvalidLocationError:
        raise Http404("Invalid location")
73

74

75
def get_course_with_access(user, course_id, action, depth=0):
76 77 78 79
    """
    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.
80

81
    Raises a 404 if the course_id is invalid, or the user doesn't have access.
82 83

    depth: The number of levels of children for the modulestore to cache. None means infinite depth
84
    """
85
    course = get_course_by_id(course_id, depth=depth)
86 87 88 89
    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.")
90
    return course
91

92

93 94 95 96 97 98 99 100 101 102
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)


103
def course_image_url(course):
104 105
    """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
106 107
    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"
108
    else:
Don Mitchell committed
109
        loc = course.location.replace(tag='c4x', category='asset', name=course.course_image)
Chris Dodge committed
110 111
        _path = StaticContent.get_url_path_from_location(loc)
        return _path
112 113


Don Mitchell committed
114
def find_file(filesystem, dirs, filename):
115 116 117
    """
    Looks for a filename in a list of dirs on a filesystem, in the specified order.

Don Mitchell committed
118
    filesystem: an OSFS filesystem
119 120 121 122 123
    dirs: a list of path objects
    filename: a string

    Returns d / filename if found in dir d, else raises ResourceNotFoundError.
    """
Don Mitchell committed
124 125 126
    for directory in dirs:
        filepath = path(directory) / filename
        if filesystem.exists(filepath):
127 128 129
            return filepath
    raise ResourceNotFoundError("Could not find {0}".format(filename))

130

131 132
def get_course_about_section(course, section_key):
    """
Victor Shnayder committed
133 134 135
    This returns the snippet of html to be rendered on the course about page,
    given the key for the section.

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
    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
152
    - ocw_links
153 154
    """

Victor Shnayder committed
155 156 157
    # 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.
158 159

# TODO: Remove number, instructors from this list
Victor Shnayder committed
160 161 162 163
    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',
164
                       'effort', 'end_date', 'prerequisites', 'ocw_links']:
165

166
        try:
167 168 169

            request = get_request_for_thread()

Don Mitchell committed
170
            loc = course.location.replace(category='about', name=section_key)
171 172

            # Use an empty cache
Calen Pennington committed
173
            field_data_cache = FieldDataCache([], course.id, request.user)
174 175 176 177
            about_module = get_module(
                request.user,
                request,
                loc,
Calen Pennington committed
178
                field_data_cache,
179 180
                course.id,
                not_found_ok=True,
181
                wrap_xmodule_display=False,
Calen Pennington committed
182
                static_asset_path=course.static_asset_path
183
            )
184 185 186

            html = ''

187
            if about_module is not None:
188
                html = about_module.render('student_view').content
189 190

            return html
191 192

        except ItemNotFoundError:
Victor Shnayder committed
193 194
            log.warning("Missing about section {key} in course {url}".format(
                key=section_key, url=course.location.url()))
195 196
            return None
    elif section_key == "title":
197
        return course.display_name_with_default
198
    elif section_key == "university":
Chris Dodge committed
199
        return course.display_org_with_default
200
    elif section_key == "number":
Chris Dodge committed
201
        return course.display_number_with_default
202 203 204

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

205

206
def get_course_info_section(request, course, section_key):
207
    """
Victor Shnayder committed
208 209 210
    This returns the snippet of html to be rendered on the course info page,
    given the key for the section.

211 212 213 214 215 216
    Valid keys:
    - handouts
    - guest_handouts
    - updates
    - guest_updates
    """
217
    loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
218 219

    # Use an empty cache
Calen Pennington committed
220
    field_data_cache = FieldDataCache([], course.id, request.user)
221 222 223 224
    info_module = get_module(
        request.user,
        request,
        loc,
Calen Pennington committed
225
        field_data_cache,
226
        course.id,
227
        wrap_xmodule_display=False,
Calen Pennington committed
228
        static_asset_path=course.static_asset_path
229 230
    )

231
    html = ''
232

233
    if info_module is not None:
234
        html = info_module.render('student_view').content
235

236
    return html
237

238

239 240
# 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
241
#       then.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
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:
Don Mitchell committed
258
            filesys = course.system.resources_fs
259 260
            # first look for a run-specific version
            dirs = [path("syllabus") / course.url_name, path("syllabus")]
Don Mitchell committed
261 262
            filepath = find_file(filesys, dirs, section_key + ".html")
            with filesys.open(filepath) as html_file:
263
                return replace_static_urls(
Don Mitchell committed
264
                    html_file.read().decode('utf-8'),
265
                    getattr(course, 'data_dir', None),
266
                    course_id=course.location.course_id,
Calen Pennington committed
267
                    static_asset_path=course.static_asset_path,
268
                )
269 270 271 272 273 274 275
        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))

276 277

def get_courses_by_university(user, domain=None):
278 279 280 281 282 283
    '''
    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.
284
    visible_courses = get_courses(user, domain)
285

286
    universities = defaultdict(list)
287
    for course in visible_courses:
288
        universities[course.org].append(course)
289

290
    return universities
291 292 293 294 295 296 297 298 299


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
300
    courses = sorted(courses, key=lambda course: course.number)
301 302 303 304 305 306 307 308 309 310 311 312 313 314

    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)

315
    return courses
316

317

318 319 320 321 322 323
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>[^/]+)$'
324 325 326
    host = "//{}/".format(settings.CMS_BASE)  # protocol-relative
    m_obj = re.match(format_str, course_id)
    if m_obj:
327
        return "{host}{org}/{course}/course/{name}".format(host=host,
328 329 330
                                                           org=m_obj.group('org'),
                                                           course=m_obj.group('course'),
                                                           name=m_obj.group('name'))
331
    return host