views.py 19.2 KB
Newer Older
1
import csv
2
import json
David Ormsbee committed
3
import logging
Piotr Mitros committed
4
import urllib
5
import itertools
6
import StringIO
Piotr Mitros committed
7

8
from functools import partial
Piotr Mitros committed
9 10

from django.conf import settings
David Ormsbee committed
11
from django.core.context_processors import csrf
12
from django.core.urlresolvers import reverse
David Ormsbee committed
13
from django.contrib.auth.models import User
14
from django.contrib.auth.decorators import login_required
15
from django.http import Http404, HttpResponse
David Ormsbee committed
16
from django.shortcuts import redirect
17
from mitxmako.shortcuts import render_to_response, render_to_string
18
#from django.views.decorators.csrf import ensure_csrf_cookie
19
from django_future.csrf import ensure_csrf_cookie
20
from django.views.decorators.cache import cache_control
21

22 23 24
from courseware import grades
from courseware.access import has_access
from courseware.courses import (get_course_with_access, get_courses_by_university)
Victor Shnayder committed
25
import courseware.tabs as tabs
26
from courseware.models import StudentModuleCache
Victor Shnayder committed
27
from module_render import toc_for_course, get_module, get_instance_module
28
from student.models import UserProfile
29

30
from multicourse import multicourse_settings
Brittany Cheng committed
31

32
from django_comment_client.utils import get_discussion_title
Piotr Mitros committed
33

34 35 36
from student.models import UserTestGroup, CourseEnrollment
from util.cache import cache, cache_if_anonymous
from xmodule.course_module import CourseDescriptor
37 38
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
39 40
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location
41

42 43
import comment_client

44 45
log = logging.getLogger("mitx.courseware")

46
template_imports = {'urllib': urllib}
Piotr Mitros committed
47

48

49
def user_groups(user):
50 51 52
    """
    TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
    """
53 54 55 56 57 58 59 60 61
    if not user.is_authenticated():
        return []

    # TODO: Rewrite in Django
    key = 'user_group_names_{user.id}'.format(user=user)
    cache_expiration = 60 * 60  # one hour

    # Kill caching on dev machines -- we switch groups a lot
    group_names = cache.get(key)
62 63
    if settings.DEBUG:
        group_names = None
64 65 66 67 68 69 70 71

    if group_names is None:
        group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
        cache.set(key, group_names, cache_expiration)

    return group_names


Piotr Mitros committed
72

73
@ensure_csrf_cookie
74
@cache_if_anonymous
75
def courses(request):
76 77 78
    '''
    Render "find courses" page.  The course selection work is done in courseware.courses.
    '''
79
    universities = get_courses_by_university(request.user,
80
                                             domain=request.META.get('HTTP_HOST'))
81 82
    return render_to_response("courses.html", {'universities': universities})

83

Victor Shnayder committed
84
def render_accordion(request, course, chapter, section):
Piotr Mitros committed
85
    ''' Draws navigation bar. Takes current position in accordion as
86 87 88 89
        parameter.

        If chapter and section are '' or None, renders a default accordion.

90 91
        course, chapter, and section are the url_names.

92
        Returns the html string'''
93

94
    # grab the table of contents
Victor Shnayder committed
95
    toc = toc_for_course(request.user, request, course, chapter, section)
96

97
    context = dict([('toc', toc),
98
                    ('course_id', course.id),
99
                    ('csrf', csrf(request)['csrf_token'])] + template_imports.items())
100 101
    return render_to_string('accordion.html', context)

Piotr Mitros committed
102

Victor Shnayder committed
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
def get_current_child(xmodule):
    """
    Get the xmodule.position's display item of an xmodule that has a position and
    children.  Returns None if the xmodule doesn't have a position, or if there
    are no children.  Otherwise, if position is out of bounds, returns the first child.
    """
    if not hasattr(xmodule, 'position'):
        return None

    children = xmodule.get_display_items()
    # position is 1-indexed.
    if 0 <= xmodule.position - 1 < len(children):
        child = children[xmodule.position - 1]
    elif len(children) > 0:
        # Something is wrong.  Default to first child
        child = children[0]
    else:
        child = None
    return child


def redirect_to_course_position(course_module, first_time):
Victor Shnayder committed
125 126 127 128
    """
    Load the course state for the user, and return a redirect to the
    appropriate place in the course: either the first element if there
    is no state, or their previous place if there is.
Victor Shnayder committed
129 130

    If this is the user's first time, send them to the first section instead.
Victor Shnayder committed
131
    """
Victor Shnayder committed
132 133 134
    course_id = course_module.descriptor.id
    chapter = get_current_child(course_module)
    if chapter is None:
Victor Shnayder committed
135 136
        # oops.  Something bad has happened.
        raise Http404
Victor Shnayder committed
137 138 139 140 141 142 143 144 145 146
    if not first_time:
        return redirect(reverse('courseware_chapter', kwargs={'course_id': course_id,
                                                              'chapter': chapter.url_name}))
    # Relying on default of returning first child
    section = get_current_child(chapter)
    return redirect(reverse('courseware_section', kwargs={'course_id': course_id,
                                                          'chapter': chapter.url_name,
                                                          'section': section.url_name}))

def save_child_position(seq_module, child_name, instance_module):
Victor Shnayder committed
147
    """
Victor Shnayder committed
148 149
    child_name: url_name of the child
    instance_module: the StudentModule object for the seq_module
Victor Shnayder committed
150
    """
Victor Shnayder committed
151 152
    for i, c in enumerate(seq_module.get_display_items()):
        if c.url_name == child_name:
Victor Shnayder committed
153 154 155
            # Position is 1-indexed
            position = i + 1
            # Only save if position changed
Victor Shnayder committed
156 157 158
            if position != seq_module.position:
                seq_module.position = position
                instance_module.state = seq_module.get_instance_state()
Victor Shnayder committed
159 160
                instance_module.save()

161
@login_required
162 163
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
164
def index(request, course_id, chapter=None, section=None,
165
          position=None):
166
    """
Victor Shnayder committed
167 168 169 170 171 172 173 174
    Displays courseware accordion and associated content.  If course, chapter,
    and section are all specified, renders the page, or returns an error if they
    are invalid.

    If section is not specified, displays the accordion opened to the right chapter.

    If neither chapter or section are specified, redirects to user's most recent
    chapter, or the first chapter if this is the user's first visit.
175 176 177 178

    Arguments:

     - request    : HTTP request
179 180 181
     - course_id  : course id (str: ORG/course/URL_NAME)
     - chapter    : chapter url_name (str)
     - section    : section url_name (str)
182 183 184 185 186
     - position   : position in module, eg of <sequential> module (str)

    Returns:

     - HTTPresponse
187 188
    """
    course = get_course_with_access(request.user, course_id, 'load')
189
    staff_access = has_access(request.user, course, 'staff')
190 191
    registered = registered_for_course(course, request.user)
    if not registered:
192
        # TODO (vshnayder): do course instructors need to be registered to see course?
193
        log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
194
        return redirect(reverse('about_course', args=[course.id]))
195

196
    try:
Victor Shnayder committed
197
        student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
Victor Shnayder committed
198 199 200 201 202
            course.id, request.user, course, depth=2)

        # Has this student been in this course before?
        first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None

Victor Shnayder committed
203 204 205 206 207 208
        course_module = get_module(request.user, request, course.location, student_module_cache, course.id)
        if course_module is None:
            log.warning('If you see this, something went wrong: if we got this'
                        ' far, should have gotten a course module for this user')
            return redirect(reverse('about_course', args=[course.id]))

209
        if chapter is None:
Victor Shnayder committed
210
            return redirect_to_course_position(course_module, first_time)
Victor Shnayder committed
211

212 213
        context = {
            'csrf': csrf(request)['csrf_token'],
Victor Shnayder committed
214
            'accordion': render_accordion(request, course, chapter, section),
215 216 217
            'COURSE_TITLE': course.title,
            'course': course,
            'init': '',
218 219
            'content': '',
            'staff_access': staff_access,
220 221
            }

Victor Shnayder committed
222 223 224
        chapter_descriptor = course.get_child_by_url_name(chapter)
        if chapter_descriptor is not None:
            instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
Victor Shnayder committed
225
            save_child_position(course_module, chapter, instance_module)
226 227
        else:
            raise Http404
Victor Shnayder committed
228 229 230

        chapter_module = get_module(request.user, request, chapter_descriptor.location,
                                    student_module_cache, course_id)
231 232 233
        if chapter_module is None:
            # User may be trying to access a chapter that isn't live yet
            raise Http404
Victor Shnayder committed
234 235 236 237 238 239 240

        if section is not None:
            section_descriptor = chapter_descriptor.get_child_by_url_name(section)
            if section_descriptor is None:
                # Specifically asked-for section doesn't exist
                raise Http404

Victor Shnayder committed
241
            section_student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
Victor Shnayder committed
242
                course_id, request.user, section_descriptor)
Victor Shnayder committed
243
            section_module = get_module(request.user, request,
Victor Shnayder committed
244
                                section_descriptor.location,
Victor Shnayder committed
245 246
                                section_student_module_cache, course_id, position)
            if section_module is None:
Victor Shnayder committed
247 248 249 250
                # User may be trying to be clever and access something
                # they don't have access to.
                raise Http404

Victor Shnayder committed
251 252 253 254
            # Save where we are in the chapter
            instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
            save_child_position(chapter_module, section, instance_module)

Victor Shnayder committed
255

Victor Shnayder committed
256
            context['content'] = section_module.get_html()
257
        else:
Victor Shnayder committed
258 259 260 261 262 263 264 265 266 267 268 269 270
            # section is none, so display a message
            prev_section = get_current_child(chapter_module)
            if prev_section is None:
                # Something went wrong -- perhaps this chapter has no sections visible to the user
                raise Http404
            prev_section_url = reverse('courseware_section', kwargs={'course_id': course_id,
                                                                     'chapter': chapter_descriptor.url_name,
                                                                     'section': prev_section.url_name})
            context['content'] = render_to_string('courseware/welcome-back.html',
                                                  {'course': course,
                                                   'chapter_module': chapter_module,
                                                   'prev_section': prev_section,
                                                   'prev_section_url': prev_section_url})
271

272
        result = render_to_response('courseware/courseware.html', context)
273 274 275 276
    except Exception as e:
        if isinstance(e, Http404):
            # let it propagate
            raise
277

278 279 280
        # In production, don't want to let a 500 out for any reason
        if settings.DEBUG:
            raise
281
        else:
282 283 284 285 286 287 288 289 290 291
            log.exception("Error in index view: user={user}, course={course},"
                          " chapter={chapter} section={section}"
                          "position={position}".format(
                              user=request.user,
                              course=course,
                              chapter=chapter,
                              section=section,
                              position=position
                              ))
            try:
292 293 294
                result = render_to_response('courseware/courseware-error.html',
                                            {'staff_access': staff_access,
                                            'course' : course})
295
            except:
296 297 298 299
                # Let the exception propagate, relying on global config to at
                # at least return a nice error message
                log.exception("Error while rendering courseware-error page")
                raise
300

301
    return result
302

Victor Shnayder committed
303

304
@ensure_csrf_cookie
305
def jump_to(request, course_id, location):
306
    '''
307
    Show the page that contains a specific location.
308

309
    If the location is invalid or not in any class, return a 404.
310

311 312
    Otherwise, delegates to the index view to figure out whether this user
    has access, and what they should see.
313 314 315 316 317 318
    '''
    # Complain if the location isn't valid
    try:
        location = Location(location)
    except InvalidLocationError:
        raise Http404("Invalid location")
319

320 321
    # Complain if there's not data for this location
    try:
322
        (course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location)
323 324
    except ItemNotFoundError:
        raise Http404("No data at this location: {0}".format(location))
325 326
    except NoPathToItem:
        raise Http404("This location is not in any class: {0}".format(location))
327

328
    # Rely on index to do all error handling and access control.
329
    return redirect('courseware_position',
330 331 332
                    course_id=course_id,
                    chapter=chapter,
                    section=section,
333
                    position=position)
334
@ensure_csrf_cookie
335
def course_info(request, course_id):
336
    """
337 338 339
    Display the course's info.html, or 404 if there is no such course.

    Assumes the course_id is in a valid format.
340
    """
341
    course = get_course_with_access(request.user, course_id, 'load')
342
    staff_access = has_access(request.user, course, 'staff')
343

344
    return render_to_response('courseware/info.html', {'course': course,
345
                                            'staff_access': staff_access,})
346

Victor Shnayder committed
347 348 349 350 351 352 353 354 355
@ensure_csrf_cookie
def static_tab(request, course_id, tab_slug):
    """
    Display the courses tab with the given name.

    Assumes the course_id is in a valid format.
    """
    course = get_course_with_access(request.user, course_id, 'load')

356
    tab = tabs.get_static_tab_by_slug(course, tab_slug)
Victor Shnayder committed
357 358 359 360 361 362 363 364 365 366 367 368 369 370
    if tab is None:
        raise Http404
    
    contents = tabs.get_static_tab_contents(course, tab)
    if contents is None:
        raise Http404

    staff_access = has_access(request.user, course, 'staff')
    return render_to_response('courseware/static_tab.html',
                              {'course': course,
                               'tab': tab,
                               'tab_contents': contents,
                               'staff_access': staff_access,})

371
# TODO arjun: remove when custom tabs in place, see courseware/syllabus.py
372 373 374 375 376 377 378 379 380 381 382 383
@ensure_csrf_cookie
def syllabus(request, course_id):
    """
    Display the course's syllabus.html, or 404 if there is no such course.

    Assumes the course_id is in a valid format.
    """
    course = get_course_with_access(request.user, course_id, 'load')
    staff_access = has_access(request.user, course, 'staff')

    return render_to_response('courseware/syllabus.html', {'course': course,
                                            'staff_access': staff_access,})
384

Victor Shnayder committed
385

386 387 388 389 390 391 392 393 394
def registered_for_course(course, user):
    '''Return CourseEnrollment if user is registered for course, else False'''
    if user is None:
        return False
    if user.is_authenticated():
        return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists()
    else:
        return False

395
@ensure_csrf_cookie
396
@cache_if_anonymous
397
def course_about(request, course_id):
398
    course = get_course_with_access(request.user, course_id, 'see_exists')
399
    registered = registered_for_course(course, request.user)
400 401 402 403 404 405 406 407 408

    if has_access(request.user, course, 'load'):
        course_target = reverse('info', args=[course.id])
    else:
        course_target = reverse('about_course', args=[course.id])

    show_courseware_link = (has_access(request.user, course, 'load') or
                            settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'))

409 410
    return render_to_response('portal/course_about.html',
                              {'course': course,
411 412
                               'registered': registered,
                               'course_target': course_target,
413
                               'show_courseware_link' : show_courseware_link})
414 415


416 417
@ensure_csrf_cookie
@cache_if_anonymous
418
def university_profile(request, org_id):
419 420 421 422
    """
    Return the profile for the particular org_id.  404 if it's not valid.
    """
    all_courses = modulestore().get_courses()
423 424 425 426 427
    valid_org_ids = set(c.org for c in all_courses)
    if org_id not in valid_org_ids:
        raise Http404("University Profile not found for {0}".format(org_id))

    # Only grab courses for this org...
428
    courses = get_courses_by_university(request.user,
429
                                        domain=request.META.get('HTTP_HOST'))[org_id]
430 431 432 433
    context = dict(courses=courses, org_id=org_id)
    template_file = "university_profile/{0}.html".format(org_id).lower()

    return render_to_response(template_file, context)
434

435 436 437 438 439 440 441 442 443 444
def render_notifications(request, course, notifications):
    context = {
        'notifications': notifications,
        'get_discussion_title': partial(get_discussion_title, request=request, course=course),
        'course': course,
    }
    return render_to_string('notifications.html', context)

@login_required
def news(request, course_id):
445
    course = get_course_with_access(request.user, course_id, 'load')
446 447 448 449 450 451 452 453 454

    notifications = comment_client.get_notifications(request.user.id)

    context = {
        'course': course,
        'content': render_notifications(request, course, notifications),
    }

    return render_to_response('news.html', context)
455 456 457

@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
458 459
def progress(request, course_id, student_id=None):
    """ User progress. We show the grade bar and every problem score.
460

461
    Course staff are allowed to see the progress of students in their class.
462
    """
463
    course = get_course_with_access(request.user, course_id, 'load')
464
    staff_access = has_access(request.user, course, 'staff')
465 466 467 468 469 470

    if student_id is None or student_id == request.user.id:
        # always allowed to see your own profile
        student = request.user
    else:
        # Requesting access to a different student's profile
471
        if not staff_access:
472 473 474
            raise Http404
        student = User.objects.get(id=int(student_id))

475 476
    # NOTE: To make sure impersonation by instructor works, use
    # student instead of request.user in the rest of the function.
477

478
    # The pre-fetching of groups is done to make auth checks not require an
479
    # additional DB lookup (this kills the Progress page in particular).
480
    student = User.objects.prefetch_related("groups").get(id=student.id)
481

482
    student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
483
        course_id, student, course)
484

485
    courseware_summary = grades.progress_summary(student, request, course,
486
                                                 student_module_cache)
487
    grade_summary = grades.grade(student, request, course, student_module_cache)
488 489 490 491
    
    if courseware_summary is None:
        #This means the student didn't have access to the course (which the instructor requested)
        raise Http404
492

493
    context = {'course': course,
494 495 496
               'courseware_summary': courseware_summary,
               'grade_summary': grade_summary,
               'staff_access': staff_access,
497 498 499
               }
    context.update()

500
    return render_to_response('courseware/progress.html', context)
501