views.py 45.3 KB
Newer Older
1 2 3 4
"""
Courseware views functions
"""

David Ormsbee committed
5
import logging
Piotr Mitros committed
6
import urllib
7
import json
8
import cgi
Piotr Mitros committed
9

10
from datetime import datetime
11
from collections import defaultdict
12
from django.utils import translation
13
from django.utils.translation import ugettext as _
14
from django.utils.translation import ungettext
Piotr Mitros committed
15 16

from django.conf import settings
David Ormsbee committed
17
from django.core.context_processors import csrf
18
from django.core.exceptions import PermissionDenied
19
from django.core.urlresolvers import reverse
20
from django.contrib.auth.models import User, AnonymousUser
21
from django.contrib.auth.decorators import login_required
22
from django.utils.timezone import UTC
23
from django.views.decorators.http import require_GET
Ned Batchelder committed
24
from django.http import Http404, HttpResponse
David Ormsbee committed
25
from django.shortcuts import redirect
26
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
27
from django_future.csrf import ensure_csrf_cookie
28
from django.views.decorators.cache import cache_control
29
from django.db import transaction
30
from functools import wraps
31
from markupsafe import escape
32

33
from courseware import grades
34
from courseware.access import has_access, _adjust_start_date_for_beta_testers
35
from courseware.courses import get_courses, get_course, get_studio_url, get_course_with_access, sort_by_announcement
36
from courseware.masquerade import setup_masquerade
Calen Pennington committed
37
from courseware.model_data import FieldDataCache
38
from .module_render import toc_for_course, get_module_for_descriptor, get_module
39
from courseware.models import StudentModule, StudentModuleHistory
40
from course_modes.models import CourseMode
41

42 43
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig

44
from open_ended_grading import open_ended_notifications
45
from student.models import UserTestGroup, CourseEnrollment
46
from student.views import single_course_reverification_info, is_course_blocked
47
from util.cache import cache, cache_if_anonymous
48
from xblock.fragment import Fragment
49
from xmodule.modulestore.django import modulestore
50
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
51
from xmodule.modulestore.search import path_to_location
52
from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
53
from xmodule.x_module import STUDENT_VIEW
54
import shoppingcart
55
from shoppingcart.models import CourseRegistrationCode
56
from shoppingcart.utils import is_shopping_cart_enabled
57
from opaque_keys import InvalidKeyError
58

59
from microsite_configuration import microsite
60
from opaque_keys.edx.locations import SlashSeparatedCourseKey
61
from instructor.enrollment import uses_shib
62

63
from util.db import commit_on_success_with_read_committed
64 65 66 67

import survey.utils
import survey.views

68
from util.views import ensure_valid_course_key
69
log = logging.getLogger("edx.courseware")
70

71
template_imports = {'urllib': urllib}
Piotr Mitros committed
72

73
CONTENT_DEPTH = 2
74

75

76
def user_groups(user):
77 78 79
    """
    TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
    """
80 81 82 83 84 85 86 87 88
    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)
89 90
    if settings.DEBUG:
        group_names = None
91 92 93 94 95 96 97 98

    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


99
@ensure_csrf_cookie
100
@cache_if_anonymous()
101
def courses(request):
102
    """
103
    Render "find courses" page.  The course selection work is done in courseware.courses.
104
    """
105 106
    courses = get_courses(request.user, request.META.get('HTTP_HOST'))
    courses = sort_by_announcement(courses)
107 108

    return render_to_response("courseware/courses.html", {'courses': courses})
109

110

Calen Pennington committed
111
def render_accordion(request, course, chapter, section, field_data_cache):
112 113 114
    """
    Draws navigation bar. Takes current position in accordion as
    parameter.
115

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

118
    course, chapter, and section are the url_names.
119

120 121
    Returns the html string
    """
122
    # grab the table of contents
123
    toc = toc_for_course(request, course, chapter, section, field_data_cache)
124

125 126
    context = dict([
        ('toc', toc),
127
        ('course_id', course.id.to_deprecated_string()),
128 129 130
        ('csrf', csrf(request)['csrf_token']),
        ('due_date_display_format', course.due_date_display_format)
    ] + template_imports.items())
131
    return render_to_string('courseware/accordion.html', context)
132

Piotr Mitros committed
133

134
def get_current_child(xmodule, min_depth=None):
Victor Shnayder committed
135 136
    """
    Get the xmodule.position's display item of an xmodule that has a position and
137 138 139 140 141 142 143
    children.  If xmodule has no position or is out of bounds, return the first
    child with children extending down to content_depth.

    For example, if chapter_one has no position set, with two child sections,
    section-A having no children and section-B having a discussion unit,
    `get_current_child(chapter, min_depth=1)`  will return section-B.

144
    Returns None only if there are no children at all.
Victor Shnayder committed
145
    """
146 147 148 149 150 151 152 153
    def _get_default_child_module(child_modules):
        """Returns the first child of xmodule, subject to min_depth."""
        if not child_modules:
            default_child = None
        elif not min_depth > 0:
            default_child = child_modules[0]
        else:
            content_children = [child for child in child_modules if
154
                                child.has_children_at_depth(min_depth - 1) and child.get_display_items()]
155 156 157 158
            default_child = content_children[0] if content_children else None

        return default_child

Victor Shnayder committed
159 160 161
    if not hasattr(xmodule, 'position'):
        return None

162
    if xmodule.position is None:
163
        return _get_default_child_module(xmodule.get_display_items())
164 165 166
    else:
        # position is 1-indexed.
        pos = xmodule.position - 1
Victor Shnayder committed
167 168

    children = xmodule.get_display_items()
169 170
    if 0 <= pos < len(children):
        child = children[pos]
Victor Shnayder committed
171
    elif len(children) > 0:
172 173 174
        # module has a set position, but the position is out of range.
        # return default child.
        child = _get_default_child_module(children)
Victor Shnayder committed
175 176 177 178 179
    else:
        child = None
    return child


180
def redirect_to_course_position(course_module, content_depth):
Victor Shnayder committed
181
    """
182 183 184 185 186 187 188 189 190
    Return a redirect to the user's current place in the course.

    If this is the user's first time, redirects to COURSE/CHAPTER/SECTION.
    If this isn't the users's first time, redirects to COURSE/CHAPTER,
    and the view will find the current section and display a message
    about reusing the stored position.

    If there is no current position in the course or chapter, then selects
    the first child.
Victor Shnayder committed
191

Victor Shnayder committed
192
    """
193
    urlargs = {'course_id': course_module.id.to_deprecated_string()}
194
    chapter = get_current_child(course_module, min_depth=content_depth)
Victor Shnayder committed
195
    if chapter is None:
Victor Shnayder committed
196
        # oops.  Something bad has happened.
197
        raise Http404("No chapter found when loading current position in course")
198 199 200 201 202

    urlargs['chapter'] = chapter.url_name
    if course_module.position is not None:
        return redirect(reverse('courseware_chapter', kwargs=urlargs))

Victor Shnayder committed
203
    # Relying on default of returning first child
204
    section = get_current_child(chapter, min_depth=content_depth - 1)
205 206 207 208 209
    if section is None:
        raise Http404("No section found when loading current position in course")

    urlargs['section'] = section.url_name
    return redirect(reverse('courseware_section', kwargs=urlargs))
Victor Shnayder committed
210

Calen Pennington committed
211

212
def save_child_position(seq_module, child_name):
Victor Shnayder committed
213
    """
Victor Shnayder committed
214
    child_name: url_name of the child
Victor Shnayder committed
215
    """
216
    for position, c in enumerate(seq_module.get_display_items(), start=1):
217
        if c.location.name == child_name:
Victor Shnayder committed
218
            # Only save if position changed
Victor Shnayder committed
219 220
            if position != seq_module.position:
                seq_module.position = position
221 222
    # Save this new position to the underlying KeyValueStore
    seq_module.save()
Victor Shnayder committed
223

Calen Pennington committed
224

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
def save_positions_recursively_up(user, request, field_data_cache, xmodule):
    """
    Recurses up the course tree starting from a leaf
    Saving the position property based on the previous node as it goes
    """
    current_module = xmodule

    while current_module:
        parent_location = modulestore().get_parent_location(current_module.location)
        parent = None
        if parent_location:
            parent_descriptor = modulestore().get_item(parent_location)
            parent = get_module_for_descriptor(user, request, parent_descriptor, field_data_cache, current_module.location.course_key)

        if parent and hasattr(parent, 'position'):
            save_child_position(parent, current_module.location.name)

        current_module = parent


245 246 247 248 249
def chat_settings(course, user):
    """
    Returns a dict containing the settings required to connect to a
    Jabber chat server and room.
    """
250 251 252 253 254 255
    domain = getattr(settings, "JABBER_DOMAIN", None)
    if domain is None:
        log.warning('You must set JABBER_DOMAIN in the settings to '
                    'enable the chat widget')
        return None

256
    return {
257
        'domain': domain,
258 259 260 261 262

        # Jabber doesn't like slashes, so replace with dashes
        'room': "{ID}_class".format(ID=course.id.replace('/', '-')),

        'username': "{USER}@{DOMAIN}".format(
263
            USER=user.username, DOMAIN=domain
264 265 266 267 268 269
        ),

        # TODO: clearly this needs to be something other than the username
        #       should also be something that's not necessarily tied to a
        #       particular course
        'password': "{USER}@{DOMAIN}".format(
270
            USER=user.username, DOMAIN=domain
271 272 273 274
        ),
    }


275
@login_required
276 277
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
278
@ensure_valid_course_key
279
@commit_on_success_with_read_committed
280
def index(request, course_id, chapter=None, section=None,
281
          position=None):
282
    """
Victor Shnayder committed
283 284 285 286 287 288 289 290
    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.
291 292 293 294

    Arguments:

     - request    : HTTP request
295 296 297
     - course_id  : course id (str: ORG/course/URL_NAME)
     - chapter    : chapter url_name (str)
     - section    : section url_name (str)
298 299 300 301 302
     - position   : position in module, eg of <sequential> module (str)

    Returns:

     - HTTPresponse
303
    """
304

305
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
306

307
    user = User.objects.prefetch_related("groups").get(id=request.user.id)
308

309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
    redeemed_registration_codes = CourseRegistrationCode.objects.filter(
        course_id=course_key,
        registrationcoderedemption__redeemed_by=request.user
    )

    # Redirect to dashboard if the course is blocked due to non-payment.
    if is_course_blocked(request, redeemed_registration_codes, course_key):
        # registration codes may be generated via Bulk Purchase Scenario
        # we have to check only for the invoice generated registration codes
        # that their invoice is valid or not
        log.warning(
            u'User %s cannot access the course %s because payment has not yet been received',
            user,
            course_key.to_deprecated_string()
        )
        return redirect(reverse('dashboard'))
325

326
    request.user = user  # keep just one instance of User
327
    with modulestore().bulk_operations(course_key):
328
        return _index_bulk_op(request, course_key, chapter, section, position)
329 330


331 332 333 334 335 336
# pylint: disable=too-many-statements
def _index_bulk_op(request, course_key, chapter, section, position):
    """
    Render the index page for the specified course.
    """
    user = request.user
337
    course = get_course_with_access(user, 'load', course_key, depth=2)
338

339
    staff_access = has_access(user, 'staff', course)
340
    registered = registered_for_course(course, user)
341
    if not registered:
342
        # TODO (vshnayder): do course instructors need to be registered to see course?
343 344
        log.debug(u'User %s tried to view course %s but is not enrolled', user, course.location.to_deprecated_string())
        return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
345

346 347 348 349 350
    # check to see if there is a required survey that must be taken before
    # the user can access the course.
    if survey.utils.must_answer_survey(course, user):
        return redirect(reverse('course_survey', args=[unicode(course.id)]))

351 352
    masq = setup_masquerade(request, staff_access)

353
    try:
Calen Pennington committed
354
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
355
            course_key, user, course, depth=2)
Victor Shnayder committed
356

357
        course_module = get_module_for_descriptor(user, request, course, field_data_cache, course_key)
Victor Shnayder committed
358
        if course_module is None:
359 360
            log.warning(u'If you see this, something went wrong: if we got this'
                        u' far, should have gotten a course module for this user')
361
            return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
Victor Shnayder committed
362

363
        studio_url = get_studio_url(course, 'course')
364

365 366
        context = {
            'csrf': csrf(request)['csrf_token'],
Calen Pennington committed
367
            'accordion': render_accordion(request, course, chapter, section, field_data_cache),
368
            'COURSE_TITLE': course.display_name_with_default,
369 370
            'course': course,
            'init': '',
371
            'fragment': Fragment(),
372
            'staff_access': staff_access,
373
            'studio_url': studio_url,
374
            'masquerade': masq,
375
            'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
376
            'reverifications': fetch_reverify_banner_info(request, course_key),
377
        }
378

379 380 381 382 383 384 385
        now = datetime.now(UTC())
        effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
        if staff_access and now < effective_start:
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True

386 387 388 389 390 391 392 393 394
        has_content = course.has_children_at_depth(CONTENT_DEPTH)
        if not has_content:
            # Show empty courseware for a course with no units
            return render_to_response('courseware/courseware.html', context)
        elif chapter is None:
            # passing CONTENT_DEPTH avoids returning 404 for a course with an
            # empty first section and a second section with content
            return redirect_to_course_position(course_module, CONTENT_DEPTH)

395 396
        # Only show the chat if it's enabled by the course and in the
        # settings.
397
        show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT']
398
        if show_chat:
399
            context['chat'] = chat_settings(course, user)
400 401 402 403 404 405
            # If we couldn't load the chat settings, then don't show
            # the widget in the courseware.
            if context['chat'] is None:
                show_chat = False

        context['show_chat'] = show_chat
406

407
        chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
Victor Shnayder committed
408
        if chapter_descriptor is not None:
409
            save_child_position(course_module, chapter)
410
        else:
Brian Wilson committed
411
            raise Http404('No chapter descriptor found with name {}'.format(chapter))
Victor Shnayder committed
412

413
        chapter_module = course_module.get_child_by(lambda m: m.location.name == chapter)
414 415
        if chapter_module is None:
            # User may be trying to access a chapter that isn't live yet
416
            if masq == 'student':  # if staff is masquerading as student be kinder, don't 404
417
                log.debug('staff masq as student: no chapter %s' % chapter)
418
                return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
419
            raise Http404
Victor Shnayder committed
420 421

        if section is not None:
422
            section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
423

Victor Shnayder committed
424 425
            if section_descriptor is None:
                # Specifically asked-for section doesn't exist
426
                if masq == 'student':  # if staff is masquerading as student be kinder, don't 404
427
                    log.debug('staff masq as student: no section %s' % section)
428
                    return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
Victor Shnayder committed
429 430
                raise Http404

431 432 433 434
            ## Allow chromeless operation
            if section_descriptor.chrome:
                chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")]
                if 'accordion' not in chrome:
435
                    context['disable_accordion'] = True
436 437 438 439 440 441
                if 'tabs' not in chrome:
                    context['disable_tabs'] = True

            if section_descriptor.default_tab:
                context['default_tab'] = section_descriptor.default_tab

442 443
            # cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
            # which will prefetch the children more efficiently than doing a recursive load
444
            section_descriptor = modulestore().get_item(section_descriptor.location, depth=None)
445

446
            # Load all descendants of the section, because we're going to display its
447
            # html, which in general will need all of its children
Calen Pennington committed
448
            section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
449 450
                course_key, user, section_descriptor, depth=None, asides=XBlockAsidesConfig.possible_asides()
            )
451

452 453 454 455 456 457 458
            # Verify that position a string is in fact an int
            if position is not None:
                try:
                    int(position)
                except ValueError:
                    raise Http404("Position {} is not an integer!".format(position))

459 460
            section_module = get_module_for_descriptor(
                request.user,
461 462 463
                request,
                section_descriptor,
                section_field_data_cache,
464
                course_key,
465 466
                position
            )
467

Victor Shnayder committed
468
            if section_module is None:
Victor Shnayder committed
469 470 471 472
                # User may be trying to be clever and access something
                # they don't have access to.
                raise Http404

Victor Shnayder committed
473
            # Save where we are in the chapter
474
            save_child_position(chapter_module, section)
475
            context['fragment'] = section_module.render(STUDENT_VIEW)
476
            context['section_title'] = section_descriptor.display_name_with_default
477
        else:
Victor Shnayder committed
478
            # section is none, so display a message
479
            studio_url = get_studio_url(course, 'course')
Victor Shnayder committed
480 481
            prev_section = get_current_child(chapter_module)
            if prev_section is None:
482 483 484 485 486 487
                # Something went wrong -- perhaps this chapter has no sections visible to the user.
                # Clearing out the last-visited state and showing "first-time" view by redirecting
                # to courseware.
                course_module.position = None
                course_module.save()
                return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
488 489 490 491 492
            prev_section_url = reverse('courseware_section', kwargs={
                'course_id': course_key.to_deprecated_string(),
                'chapter': chapter_descriptor.url_name,
                'section': prev_section.url_name
            })
493 494 495 496
            context['fragment'] = Fragment(content=render_to_string(
                'courseware/welcome-back.html',
                {
                    'course': course,
497
                    'studio_url': studio_url,
498 499 500 501 502
                    'chapter_module': chapter_module,
                    'prev_section': prev_section,
                    'prev_section_url': prev_section_url
                }
            ))
503

504
        result = render_to_response('courseware/courseware.html', context)
505
    except Exception as e:
506 507 508 509 510 511

        # Doesn't bar Unicode characters from URL, but if Unicode characters do
        # cause an error it is a graceful failure.
        if isinstance(e, UnicodeEncodeError):
            raise Http404("URL contains Unicode characters")

512 513 514
        if isinstance(e, Http404):
            # let it propagate
            raise
515

516 517 518
        # In production, don't want to let a 500 out for any reason
        if settings.DEBUG:
            raise
519
        else:
520
            log.exception(
521 522
                u"Error in index view: user={user}, course={course}, chapter={chapter}"
                u" section={section} position={position}".format(
523 524 525 526 527 528
                    user=user,
                    course=course,
                    chapter=chapter,
                    section=section,
                    position=position
                ))
529
            try:
530 531 532 533
                result = render_to_response('courseware/courseware-error.html', {
                    'staff_access': staff_access,
                    'course': course
                })
534
            except:
535 536 537 538
                # 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
539

540
    return result
541

Victor Shnayder committed
542

543
@ensure_csrf_cookie
544
@ensure_valid_course_key
545 546 547 548 549
def jump_to_id(request, course_id, module_id):
    """
    This entry point allows for a shorter version of a jump to where just the id of the element is
    passed in. This assumes that id is unique within the course_id namespace
    """
550
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
551
    items = modulestore().get_items(course_key, qualifiers={'name': module_id})
552 553

    if len(items) == 0:
554 555 556 557
        raise Http404(
            u"Could not find id: {0} in course_id: {1}. Referer: {2}".format(
                module_id, course_id, request.META.get("HTTP_REFERER", "")
            ))
558
    if len(items) > 1:
559 560
        log.warning(
            u"Multiple items found with id: {0} in course_id: {1}. Referer: {2}. Using first: {3}".format(
561
                module_id, course_id, request.META.get("HTTP_REFERER", ""), items[0].location.to_deprecated_string()
562
            ))
563

564
    return jump_to(request, course_id, items[0].location.to_deprecated_string())
565 566 567


@ensure_csrf_cookie
568
def jump_to(request, course_id, location):
569
    """
570
    Show the page that contains a specific location.
571

572
    If the location is invalid or not in any class, return a 404.
573

574 575
    Otherwise, delegates to the index view to figure out whether this user
    has access, and what they should see.
576
    """
577
    try:
578 579 580 581
        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
        usage_key = course_key.make_usage_key_from_deprecated_string(location)
    except InvalidKeyError:
        raise Http404(u"Invalid course_key or usage_key")
582
    try:
583
        (course_key, chapter, section, position) = path_to_location(modulestore(), usage_key)
584
    except ItemNotFoundError:
585
        raise Http404(u"No data at this location: {0}".format(usage_key))
586
    except NoPathToItem:
587
        raise Http404(u"This location is not in any class: {0}".format(usage_key))
588

589
    # choose the appropriate view (and provide the necessary args) based on the
590
    # args provided by the redirect.
591
    # Rely on index to do all error handling and access control.
592
    if chapter is None:
593
        return redirect('courseware', course_id=course_key.to_deprecated_string())
594
    elif section is None:
595
        return redirect('courseware_chapter', course_id=course_key.to_deprecated_string(), chapter=chapter)
596
    elif position is None:
597
        return redirect('courseware_section', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section)
598
    else:
599
        return redirect('courseware_position', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section, position=position)
600

Calen Pennington committed
601

602
@ensure_csrf_cookie
603
@ensure_valid_course_key
604
def course_info(request, course_id):
605
    """
606 607 608
    Display the course's info.html, or 404 if there is no such course.

    Assumes the course_id is in a valid format.
609
    """
610

611
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
612

613 614
    with modulestore().bulk_operations(course_key):
        course = get_course_with_access(request.user, 'load', course_key)
615 616 617

        # check to see if there is a required survey that must be taken before
        # the user can access the course.
618
        if request.user.is_authenticated() and survey.utils.must_answer_survey(course, request.user):
619 620
            return redirect(reverse('course_survey', args=[unicode(course.id)]))

621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
        staff_access = has_access(request.user, 'staff', course)
        masq = setup_masquerade(request, staff_access)    # allow staff to toggle masquerade on info page
        reverifications = fetch_reverify_banner_info(request, course_key)
        studio_url = get_studio_url(course, 'course_info')

        # link to where the student should go to enroll in the course:
        # about page if there is not marketing site, SITE_NAME if there is
        url_to_enroll = reverse(course_about, args=[course_id])
        if settings.FEATURES.get('ENABLE_MKTG_SITE'):
            url_to_enroll = marketing_link('COURSES')

        show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(request.user, course.id)

        context = {
            'request': request,
            'course_id': course_key.to_deprecated_string(),
            'cache': None,
            'course': course,
            'staff_access': staff_access,
            'masquerade': masq,
            'studio_url': studio_url,
            'reverifications': reverifications,
            'show_enroll_banner': show_enroll_banner,
            'url_to_enroll': url_to_enroll,
        }
646

647 648 649 650 651 652
        now = datetime.now(UTC())
        effective_start = _adjust_start_date_for_beta_testers(request.user, course, course_key)
        if staff_access and now < effective_start:
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True
653

654
        return render_to_response('courseware/info.html', context)
655

Calen Pennington committed
656

Victor Shnayder committed
657
@ensure_csrf_cookie
658
@ensure_valid_course_key
Victor Shnayder committed
659 660 661 662 663 664
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.
    """
665 666

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
667

668
    course = get_course_with_access(request.user, 'load', course_key)
Victor Shnayder committed
669

670
    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
Victor Shnayder committed
671 672
    if tab is None:
        raise Http404
673

674
    contents = get_static_tab_contents(
Calen Pennington committed
675 676 677 678
        request,
        course,
        tab
    )
Victor Shnayder committed
679 680 681
    if contents is None:
        raise Http404

682 683 684 685 686
    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Victor Shnayder committed
687

688
# TODO arjun: remove when custom tabs in place, see courseware/syllabus.py
Calen Pennington committed
689 690


691
@ensure_csrf_cookie
692
@ensure_valid_course_key
693 694 695 696 697 698
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.
    """
699

700
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
701

702 703
    course = get_course_with_access(request.user, 'load', course_key)
    staff_access = has_access(request.user, 'staff', course)
704

705 706 707 708
    return render_to_response('courseware/syllabus.html', {
        'course': course,
        'staff_access': staff_access,
    })
709

Victor Shnayder committed
710

711
def registered_for_course(course, user):
712
    """
713
    Return True if user is registered for course, else False
714
    """
715 716 717
    if user is None:
        return False
    if user.is_authenticated():
718
        return CourseEnrollment.is_enrolled(user, course.id)
719 720 721
    else:
        return False

Calen Pennington committed
722

723
@ensure_csrf_cookie
724
@cache_if_anonymous()
725
def course_about(request, course_id):
726 727 728 729 730
    """
    Display the course's about page.

    Assumes the course_id is in a valid format.
    """
731

732
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
733 734 735 736 737 738

    permission_name = microsite.get_value(
        'COURSE_ABOUT_VISIBILITY_PERMISSION',
        settings.COURSE_ABOUT_VISIBILITY_PERMISSION
    )
    course = get_course_with_access(request.user, permission_name, course_key)
739

740
    if microsite.get_value(
741 742 743
        'ENABLE_MKTG_SITE',
        settings.FEATURES.get('ENABLE_MKTG_SITE', False)
    ):
744
        return redirect(reverse('info', args=[course.id.to_deprecated_string()]))
745

746
    registered = registered_for_course(course, request.user)
747

748
    staff_access = has_access(request.user, 'staff', course)
749
    studio_url = get_studio_url(course, 'settings/details')
750

751 752
    if has_access(request.user, 'load', course):
        course_target = reverse('info', args=[course.id.to_deprecated_string()])
753
    else:
754
        course_target = reverse('about_course', args=[course.id.to_deprecated_string()])
755

756
    show_courseware_link = (has_access(request.user, 'load', course) or
757
                            settings.FEATURES.get('ENABLE_LMS_MIGRATION'))
758

759 760 761 762
    # Note: this is a flow for payment for course registration, not the Verified Certificate flow.
    registration_price = 0
    in_cart = False
    reg_then_add_to_cart_link = ""
763

764 765
    _is_shopping_cart_enabled = is_shopping_cart_enabled()
    if (_is_shopping_cart_enabled):
766
        registration_price = CourseMode.min_course_price_for_currency(course_key,
767 768 769
                                                                      settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
        if request.user.is_authenticated():
            cart = shoppingcart.models.Order.get_cart_for_user(request.user)
770 771
            in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_key) or \
                shoppingcart.models.CourseRegCodeItem.contained_in_order(cart, course_key)
772 773

        reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format(
774
            reg_url=reverse('register_user'), course_id=course.id.to_deprecated_string())
775

776 777 778
    # Used to provide context to message to student if enrollment not allowed
    can_enroll = has_access(request.user, 'enroll', course)
    invitation_only = course.invitation_only
779 780
    is_course_full = CourseEnrollment.is_course_full(course)

781 782 783 784 785 786
    # Register button should be disabled if one of the following is true:
    # - Student is already registered for course
    # - Course is already full
    # - Student cannot enroll in course
    active_reg_button = not(registered or is_course_full or not can_enroll)

787 788
    is_shib_course = uses_shib(course)

789 790
    return render_to_response('courseware/course_about.html', {
        'course': course,
791 792
        'staff_access': staff_access,
        'studio_url': studio_url,
793 794 795
        'registered': registered,
        'course_target': course_target,
        'registration_price': registration_price,
796
        'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
797 798 799
        'in_cart': in_cart,
        'reg_then_add_to_cart_link': reg_then_add_to_cart_link,
        'show_courseware_link': show_courseware_link,
800 801 802 803
        'is_course_full': is_course_full,
        'can_enroll': can_enroll,
        'invitation_only': invitation_only,
        'active_reg_button': active_reg_button,
804
        'is_shib_course': is_shib_course,
805 806
        # We do not want to display the internal courseware header, which is used when the course is found in the
        # context. This value is therefor explicitly set to render the appropriate header.
807
        'disable_courseware_header': True,
808 809
        'is_shopping_cart_enabled': _is_shopping_cart_enabled,
        'cart_link': reverse('shoppingcart.views.show_cart'),
810
    })
811 812


John Jarvis committed
813
@ensure_csrf_cookie
814
@cache_if_anonymous('org')
815
@ensure_valid_course_key
John Jarvis committed
816
def mktg_course_about(request, course_id):
817
    """This is the button that gets put into an iframe on the Drupal site."""
818
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
819

820
    try:
821 822 823 824 825 826
        permission_name = microsite.get_value(
            'COURSE_ABOUT_VISIBILITY_PERMISSION',
            settings.COURSE_ABOUT_VISIBILITY_PERMISSION
        )
        course = get_course_with_access(request.user, permission_name, course_key)
    except (ValueError, Http404):
827
        # If a course does not exist yet, display a "Coming Soon" button
828
        return render_to_response(
829
            'courseware/mktg_coming_soon.html', {'course_id': course_key.to_deprecated_string()}
830
        )
831

John Jarvis committed
832 833
    registered = registered_for_course(course, request.user)

834 835
    if has_access(request.user, 'load', course):
        course_target = reverse('info', args=[course.id.to_deprecated_string()])
John Jarvis committed
836
    else:
837
        course_target = reverse('about_course', args=[course.id.to_deprecated_string()])
John Jarvis committed
838

839
    allow_registration = has_access(request.user, 'enroll', course)
840

841
    show_courseware_link = (has_access(request.user, 'load', course) or
842
                            settings.FEATURES.get('ENABLE_LMS_MIGRATION'))
843
    course_modes = CourseMode.modes_for_course_dict(course.id)
John Jarvis committed
844

845
    context = {
846 847 848 849 850 851
        'course': course,
        'registered': registered,
        'allow_registration': allow_registration,
        'course_target': course_target,
        'show_courseware_link': show_courseware_link,
        'course_modes': course_modes,
852
    }
John Jarvis committed
853

854
    if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
        # Drupal will pass organization names using a GET parameter, as follows:
        #     ?org=Harvard
        #     ?org=Harvard,MIT
        # If no full names are provided, the marketing iframe won't show the
        # email opt-in checkbox.
        org = request.GET.get('org')
        if org:
            org_list = org.split(',')
            # HTML-escape the provided organization names
            org_list = [cgi.escape(org) for org in org_list]
            if len(org_list) > 1:
                if len(org_list) > 2:
                    # Translators: The join of three or more institution names (e.g., Harvard, MIT, and Dartmouth).
                    org_name_string = _("{first_institutions}, and {last_institution}").format(
                        first_institutions=u", ".join(org_list[:-1]),
                        last_institution=org_list[-1]
                    )
                else:
                    # Translators: The join of two institution names (e.g., Harvard and MIT).
                    org_name_string = _("{first_institution} and {second_institution}").format(
                        first_institution=org_list[0],
                        second_institution=org_list[1]
                    )
            else:
                org_name_string = org_list[0]

            context['checkbox_label'] = ungettext(
                "I would like to receive email from {institution_series} and learn about its other programs.",
                "I would like to receive email from {institution_series} and learn about their other programs.",
                len(org_list)
            ).format(institution_series=org_name_string)
886

887 888 889 890 891 892 893 894 895 896 897 898 899 900 901
    # The edx.org marketing site currently displays only in English.
    # To avoid displaying a different language in the register / access button,
    # we force the language to English.
    # However, OpenEdX installations with a different marketing front-end
    # may want to respect the language specified by the user or the site settings.
    force_english = settings.FEATURES.get('IS_EDX_DOMAIN', False)
    if force_english:
        translation.activate('en-us')

    try:
        return render_to_response('courseware/mktg_course_about.html', context)
    finally:
        # Just to be safe, reset the language if we forced it to be English.
        if force_english:
            translation.deactivate()
902

903

904 905
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
906
@transaction.commit_manually
907
@ensure_valid_course_key
908
def progress(request, course_id, student_id=None):
909 910 911 912
    """
    Wraps "_progress" with the manual_transaction context manager just in case
    there are unanticipated errors.
    """
913 914 915

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)

916 917 918
    with modulestore().bulk_operations(course_key):
        with grades.manual_transaction():
            return _progress(request, course_key, student_id)
919 920


921
def _progress(request, course_key, student_id):
922 923 924 925
    """
    Unwrapped version of "progress".

    User progress. We show the grade bar and every problem score.
926

927
    Course staff are allowed to see the progress of students in their class.
928
    """
929
    course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
930 931 932 933 934 935

    # check to see if there is a required survey that must be taken before
    # the user can access the course.
    if survey.utils.must_answer_survey(course, request.user):
        return redirect(reverse('course_survey', args=[unicode(course.id)]))

936
    staff_access = has_access(request.user, 'staff', course)
937 938 939 940 941 942

    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
943
        if not staff_access:
944 945 946
            raise Http404
        student = User.objects.get(id=int(student_id))

947 948
    # NOTE: To make sure impersonation by instructor works, use
    # student instead of request.user in the rest of the function.
949

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

954
    courseware_summary = grades.progress_summary(student, request, course)
955
    studio_url = get_studio_url(course, 'settings/grading')
956
    grade_summary = grades.grade(student, request, course)
957

958 959 960
    if courseware_summary is None:
        #This means the student didn't have access to the course (which the instructor requested)
        raise Http404
961

962 963 964
    context = {
        'course': course,
        'courseware_summary': courseware_summary,
965
        'studio_url': studio_url,
966 967 968
        'grade_summary': grade_summary,
        'staff_access': staff_access,
        'student': student,
969
        'reverifications': fetch_reverify_banner_info(request, course_key)
970 971 972 973
    }

    with grades.manual_transaction():
        response = render_to_response('courseware/progress.html', context)
974

975
    return response
976 977


978
def fetch_reverify_banner_info(request, course_key):
979
    """
980
    Fetches needed context variable to display reverification banner in courseware
981
    """
982
    reverifications = defaultdict(list)
983 984
    user = request.user
    if not user.id:
985
        return reverifications
986
    enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_key)
987
    course = modulestore().get_course(course_key)
988
    info = single_course_reverification_info(user, course, enrollment)
989
    if info:
990
        reverifications[info.status].append(info)
991
    return reverifications
992

993

994
@login_required
995
@ensure_valid_course_key
996
def submission_history(request, course_id, student_username, location):
997
    """Render an HTML fragment (meant for inclusion elsewhere) that renders a
998
    history of all state changes made by this user for this problem location.
999
    Right now this only works for problems because that's all
1000 1001
    StudentModuleHistory records.
    """
1002 1003

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1004 1005

    try:
1006
        usage_key = course_key.make_usage_key_from_deprecated_string(location)
1007 1008
    except (InvalidKeyError, AssertionError):
        return HttpResponse(escape(_(u'Invalid location.')))
1009

1010 1011
    course = get_course_with_access(request.user, 'load', course_key)
    staff_access = has_access(request.user, 'staff', course)
1012

1013 1014
    # Permission Denied if they don't have staff access and are trying to see
    # somebody else's submission history.
1015 1016 1017 1018 1019
    if (student_username != request.user.username) and (not staff_access):
        raise PermissionDenied

    try:
        student = User.objects.get(username=student_username)
1020
        student_module = StudentModule.objects.get(
1021
            course_id=course_key,
1022
            module_state_key=usage_key,
1023 1024
            student_id=student.id
        )
1025
    except User.DoesNotExist:
1026
        return HttpResponse(escape(_(u'User {username} does not exist.').format(username=student_username)))
1027
    except StudentModule.DoesNotExist:
1028 1029 1030 1031
        return HttpResponse(escape(_(u'User {username} has never accessed problem {location}').format(
            username=student_username,
            location=location
        )))
1032 1033 1034
    history_entries = StudentModuleHistory.objects.filter(
        student_module=student_module
    ).order_by('-id')
1035 1036 1037 1038

    # If no history records exist, let's force a save to get history started.
    if not history_entries:
        student_module.save()
1039 1040 1041
        history_entries = StudentModuleHistory.objects.filter(
            student_module=student_module
        ).order_by('-id')
1042 1043 1044 1045 1046

    context = {
        'history_entries': history_entries,
        'username': student.username,
        'location': location,
1047
        'course_id': course_key.to_deprecated_string()
1048 1049 1050
    }

    return render_to_response('courseware/submission_history.html', context)
1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075


def notification_image_for_tab(course_tab, user, course):
    """
    Returns the notification image path for the given course_tab if applicable, otherwise None.
    """

    tab_notification_handlers = {
        StaffGradingTab.type: open_ended_notifications.staff_grading_notifications,
        PeerGradingTab.type: open_ended_notifications.peer_grading_notifications,
        OpenEndedGradingTab.type: open_ended_notifications.combined_notifications
    }

    if course_tab.type in tab_notification_handlers:
        notifications = tab_notification_handlers[course_tab.type](course, user)
        if notifications and notifications['pending_grading']:
            return notifications['img_path']

    return None


def get_static_tab_contents(request, course, tab):
    """
    Returns the contents for the given static tab
    """
1076
    loc = course.id.make_usage_key(
1077 1078 1079 1080
        tab.type,
        tab.url_slug,
    )
    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
1081
        course.id, request.user, modulestore().get_item(loc), depth=0
1082 1083
    )
    tab_module = get_module(
1084
        request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path
1085 1086 1087 1088 1089 1090 1091
    )

    logging.debug('course_module = {0}'.format(tab_module))

    html = ''
    if tab_module is not None:
        try:
1092
            html = tab_module.render(STUDENT_VIEW).content
1093 1094
        except Exception:  # pylint: disable=broad-except
            html = render_to_string('courseware/error-message.html', None)
1095
            log.exception(
1096
                u"Error rendering course={course}, tab={tab_url}".format(course=course, tab_url=tab['url_slug'])
1097
            )
1098 1099

    return html
1100 1101 1102


@require_GET
1103
@ensure_valid_course_key
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119
def get_course_lti_endpoints(request, course_id):
    """
    View that, given a course_id, returns the a JSON object that enumerates all of the LTI endpoints for that course.

    The LTI 2.0 result service spec at
    http://www.imsglobal.org/lti/ltiv2p0/uml/purl.imsglobal.org/vocab/lis/v2/outcomes/Result/service.html
    says "This specification document does not prescribe a method for discovering the endpoint URLs."  This view
    function implements one way of discovering these endpoints, returning a JSON array when accessed.

    Arguments:
        request (django request object):  the HTTP request object that triggered this view function
        course_id (unicode):  id associated with the course

    Returns:
        (django response object):  HTTP response.  404 if course is not found, otherwise 200 with JSON body.
    """
1120 1121

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1122 1123 1124 1125

    try:
        course = get_course(course_key, depth=2)
    except ValueError:
1126 1127 1128 1129
        return HttpResponse(status=404)

    anonymous_user = AnonymousUser()
    anonymous_user.known = False  # make these "noauth" requests like module_render.handle_xblock_callback_noauth
1130
    lti_descriptors = modulestore().get_items(course.id, qualifiers={'category': 'lti'})
1131 1132 1133 1134 1135 1136 1137

    lti_noauth_modules = [
        get_module_for_descriptor(
            anonymous_user,
            request,
            descriptor,
            FieldDataCache.cache_for_descriptor_descendents(
1138
                course_key,
1139 1140 1141
                anonymous_user,
                descriptor
            ),
1142
            course_key
1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
        )
        for descriptor in lti_descriptors
    ]

    endpoints = [
        {
            'display_name': module.display_name,
            'lti_2_0_result_service_json_endpoint': module.get_outcome_service_url(
                service_name='lti_2_0_result_rest_handler') + "/user/{anon_user_id}",
            'lti_1_1_result_service_xml_endpoint': module.get_outcome_service_url(
                service_name='grade_handler'),
        }
        for module in lti_noauth_modules
    ]

    return HttpResponse(json.dumps(endpoints), content_type='application/json')
1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185


@login_required
def course_survey(request, course_id):
    """
    URL endpoint to present a survey that is associated with a course_id
    Note that the actual implementation of course survey is handled in the
    views.py file in the Survey Djangoapp
    """

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    course = get_course_with_access(request.user, 'load', course_key)

    redirect_url = reverse('info', args=[course_id])

    # if there is no Survey associated with this course,
    # then redirect to the course instead
    if not course.course_survey_name:
        return redirect(redirect_url)

    return survey.views.view_student_survey(
        request.user,
        course.course_survey_name,
        course=course,
        redirect_url=redirect_url,
        is_required=course.course_survey_required,
    )