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

5
import json
6
import logging
7
import urllib
Bill DeRusha committed
8
from collections import OrderedDict
9
from datetime import datetime
Piotr Mitros committed
10

11 12
import analytics
import newrelic.agent
Piotr Mitros committed
13
from django.conf import settings
14 15
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User, AnonymousUser
David Ormsbee committed
16
from django.core.context_processors import csrf
17
from django.core.exceptions import PermissionDenied
18
from django.core.urlresolvers import reverse
19
from django.db import transaction
20 21
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
David Ormsbee committed
22
from django.shortcuts import redirect
23 24
from django.utils.timezone import UTC
from django.utils.translation import ugettext as _
25
from django.views.decorators.cache import cache_control
26 27 28
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_GET, require_POST, require_http_methods
from eventtracking import tracker
29
from ipware.ip import get_ip
30
from markupsafe import escape
31 32 33
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
34
from rest_framework import status
35
from xblock.fragment import Fragment
36

37 38 39 40 41 42
import shoppingcart
import survey.utils
import survey.views
from certificates import api as certs_api
from openedx.core.lib.gating import api as gating_api
from course_modes.models import CourseMode
43
from courseware import grades
44
from courseware.access import has_access, has_ccx_coach_role, _adjust_start_date_for_beta_testers
45
from courseware.access_response import StartDateError
46
from courseware.access_utils import in_preview_mode
47
from courseware.courses import (
48 49 50 51 52 53 54
    get_courses,
    get_course,
    get_course_by_id,
    get_permission_for_course_about,
    get_studio_url,
    get_course_overview_with_access,
    get_course_with_access,
55 56
    sort_by_announcement,
    sort_by_start_date,
57 58
    UserNotEnrolled
)
59
from courseware.masquerade import setup_masquerade
60 61 62 63 64 65 66 67
from courseware.model_data import FieldDataCache, ScoresClient
from courseware.models import StudentModuleHistory
from courseware.url_helpers import get_redirect_url
from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
from instructor.enrollment import uses_shib
from microsite_configuration import microsite
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
68 69 70 71 72
from openedx.core.djangoapps.credit.api import (
    get_credit_requirement_status,
    is_user_eligible_for_credit,
    is_credit_course
)
73 74
from shoppingcart.models import CourseRegistrationCode
from shoppingcart.utils import is_shopping_cart_enabled
75
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
76
from student.models import UserTestGroup, CourseEnrollment
77
from student.views import is_course_blocked
78
from util.cache import cache, cache_if_anonymous
79
from util.date_utils import strftime_localized
80
from util.db import outer_atomic
81 82 83
from util.milestones_helpers import get_prerequisite_courses_display
from util.views import _record_feedback_in_zendesk
from util.views import ensure_valid_course_key
84
from xmodule.modulestore.django import modulestore
85
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
86
from xmodule.tabs import CourseTabList
87
from xmodule.x_module import STUDENT_VIEW
88
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
89 90 91 92 93 94 95 96
from .entrance_exams import (
    course_has_entrance_exam,
    get_entrance_exam_content,
    get_entrance_exam_score,
    user_must_complete_entrance_exam,
    user_has_passed_entrance_exam
)
from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id
97

98 99 100 101
from lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference


102
log = logging.getLogger("edx.courseware")
103

104
template_imports = {'urllib': urllib}
Piotr Mitros committed
105

106
CONTENT_DEPTH = 2
107 108 109
# Only display the requirements on learner dashboard for
# credit and verified modes.
REQUIREMENTS_DISPLAY_MODES = CourseMode.CREDIT_MODES + [CourseMode.VERIFIED]
110

111

112
def user_groups(user):
113 114 115
    """
    TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
    """
116 117 118 119 120 121 122 123 124
    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)
125 126
    if settings.DEBUG:
        group_names = None
127 128 129 130 131 132 133 134

    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


135
@ensure_csrf_cookie
136
@cache_if_anonymous()
137
def courses(request):
138
    """
139
    Render "find courses" page.  The course selection work is done in courseware.courses.
140
    """
141 142 143
    courses_list = []
    course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
    if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
Renzo Lucioni committed
144
        courses_list = get_courses(request.user)
145 146 147 148 149 150 151 152 153 154 155

        if microsite.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
                               settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
            courses_list = sort_by_start_date(courses_list)
        else:
            courses_list = sort_by_announcement(courses_list)

    return render_to_response(
        "courseware/courses.html",
        {'courses': courses_list, 'course_discovery_meanings': course_discovery_meanings}
    )
156

157

158
def render_accordion(user, request, course, chapter, section, field_data_cache):
159 160 161
    """
    Draws navigation bar. Takes current position in accordion as
    parameter.
162

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

165
    course, chapter, and section are the url_names.
166

167 168
    Returns the html string
    """
169
    # grab the table of contents
170
    toc = toc_for_course(user, request, course, chapter, section, field_data_cache)
171

172 173
    context = dict([
        ('toc', toc),
174
        ('course_id', course.id.to_deprecated_string()),
175 176 177
        ('csrf', csrf(request)['csrf_token']),
        ('due_date_display_format', course.due_date_display_format)
    ] + template_imports.items())
178
    return render_to_string('courseware/accordion.html', context)
179

Piotr Mitros committed
180

181
def get_current_child(xmodule, min_depth=None):
Victor Shnayder committed
182 183
    """
    Get the xmodule.position's display item of an xmodule that has a position and
184 185 186 187 188 189 190
    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.

191
    Returns None only if there are no children at all.
Victor Shnayder committed
192
    """
193 194 195 196 197 198 199 200
    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
201
                                child.has_children_at_depth(min_depth - 1) and child.get_display_items()]
202 203 204 205
            default_child = content_children[0] if content_children else None

        return default_child

Victor Shnayder committed
206 207 208
    if not hasattr(xmodule, 'position'):
        return None

209
    if xmodule.position is None:
210
        return _get_default_child_module(xmodule.get_display_items())
211 212 213
    else:
        # position is 1-indexed.
        pos = xmodule.position - 1
Victor Shnayder committed
214 215

    children = xmodule.get_display_items()
216 217
    if 0 <= pos < len(children):
        child = children[pos]
Victor Shnayder committed
218
    elif len(children) > 0:
219 220 221
        # module has a set position, but the position is out of range.
        # return default child.
        child = _get_default_child_module(children)
Victor Shnayder committed
222 223 224 225 226
    else:
        child = None
    return child


227
def redirect_to_course_position(course_module, content_depth):
Victor Shnayder committed
228
    """
229 230 231 232 233 234 235 236 237
    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
238

Victor Shnayder committed
239
    """
240
    urlargs = {'course_id': course_module.id.to_deprecated_string()}
241
    chapter = get_current_child(course_module, min_depth=content_depth)
Victor Shnayder committed
242
    if chapter is None:
Victor Shnayder committed
243
        # oops.  Something bad has happened.
244
        raise Http404("No chapter found when loading current position in course")
245 246 247 248 249

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

Victor Shnayder committed
250
    # Relying on default of returning first child
251
    section = get_current_child(chapter, min_depth=content_depth - 1)
252 253 254 255 256
    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
257

Calen Pennington committed
258

259
def save_child_position(seq_module, child_name):
Victor Shnayder committed
260
    """
Victor Shnayder committed
261
    child_name: url_name of the child
Victor Shnayder committed
262
    """
263
    for position, c in enumerate(seq_module.get_display_items(), start=1):
264
        if c.location.name == child_name:
Victor Shnayder committed
265
            # Only save if position changed
Victor Shnayder committed
266 267
            if position != seq_module.position:
                seq_module.position = position
268 269
    # Save this new position to the underlying KeyValueStore
    seq_module.save()
Victor Shnayder committed
270

Calen Pennington committed
271

272
def save_positions_recursively_up(user, request, field_data_cache, xmodule, course=None):
273 274 275 276 277 278 279 280 281 282 283
    """
    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)
284 285 286 287 288 289 290 291
            parent = get_module_for_descriptor(
                user,
                request,
                parent_descriptor,
                field_data_cache,
                current_module.location.course_key,
                course=course
            )
292 293 294 295 296 297 298

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

        current_module = parent


299
@transaction.non_atomic_requests
300
@login_required
301 302
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
303
@ensure_valid_course_key
304
@outer_atomic(read_committed=True)
305
def index(request, course_id, chapter=None, section=None,
306
          position=None):
307
    """
Victor Shnayder committed
308 309 310 311 312 313 314 315
    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.
316 317 318 319

    Arguments:

     - request    : HTTP request
320 321 322
     - course_id  : course id (str: ORG/course/URL_NAME)
     - chapter    : chapter url_name (str)
     - section    : section url_name (str)
323 324 325 326 327
     - position   : position in module, eg of <sequential> module (str)

    Returns:

     - HTTPresponse
328
    """
329

330
    course_key = CourseKey.from_string(course_id)
331

332 333 334 335
    # Gather metrics for New Relic so we can slice data in New Relic Insights
    newrelic.agent.add_custom_parameter('course_id', unicode(course_key))
    newrelic.agent.add_custom_parameter('org', unicode(course_key.org))

336
    user = User.objects.prefetch_related("groups").get(id=request.user.id)
337

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
    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'))
354

355
    request.user = user  # keep just one instance of User
356
    with modulestore().bulk_operations(course_key):
357
        return _index_bulk_op(request, course_key, chapter, section, position)
358 359


360 361 362 363 364
# pylint: disable=too-many-statements
def _index_bulk_op(request, course_key, chapter, section, position):
    """
    Render the index page for the specified course.
    """
365 366 367 368 369
    # Verify that position a string is in fact an int
    if position is not None:
        try:
            int(position)
        except ValueError:
370
            raise Http404(u"Position {} is not an integer!".format(position))
371

372 373 374
    course = get_course_with_access(request.user, 'load', course_key, depth=2)
    staff_access = has_access(request.user, 'staff', course)
    masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
375

376
    registered = registered_for_course(course, user)
377
    if not registered:
378
        # TODO (vshnayder): do course instructors need to be registered to see course?
379 380
        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()]))
381

382 383 384 385 386 387 388 389 390 391 392
    # see if all pre-requisites (as per the milestones app feature) have been fulfilled
    # Note that if the pre-requisite feature flag has been turned off (default) then this check will
    # always pass
    if not has_access(user, 'view_courseware_with_prerequisites', course):
        # prerequisites have not been fulfilled therefore redirect to the Dashboard
        log.info(
            u'User %d tried to view course %s '
            u'without fulfilling prerequisites',
            user.id, unicode(course.id))
        return redirect(reverse('dashboard'))

393 394 395 396 397 398 399 400 401
    # Entrance Exam Check
    # If the course has an entrance exam and the requested chapter is NOT the entrance exam, and
    # the user hasn't yet met the criteria to bypass the entrance exam, redirect them to the exam.
    if chapter and course_has_entrance_exam(course):
        chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
        if chapter_descriptor and not getattr(chapter_descriptor, 'is_entrance_exam', False) \
                and user_must_complete_entrance_exam(request, user, course):
            log.info(u'User %d tried to view course %s without passing entrance exam', user.id, unicode(course.id))
            return redirect(reverse('courseware', args=[unicode(course.id)]))
402 403 404 405 406 407 408 409

    # Gated Content Check
    gated_content = gating_api.get_gated_content(course, user)
    if section and gated_content:
        for usage_key in gated_content:
            if section in usage_key:
                raise Http404

410 411 412 413 414
    # 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)]))

415 416
    bookmarks_api_url = reverse('bookmarks')

417
    try:
Calen Pennington committed
418
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
419
            course_key, user, course, depth=2)
Victor Shnayder committed
420

421 422 423
        course_module = get_module_for_descriptor(
            user, request, course, field_data_cache, course_key, course=course
        )
Victor Shnayder committed
424
        if course_module is None:
425 426
            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')
427
            return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
Victor Shnayder committed
428

429
        studio_url = get_studio_url(course, 'course')
430

431 432 433 434
        language_preference = get_user_preference(request.user, LANGUAGE_KEY)
        if not language_preference:
            language_preference = settings.LANGUAGE_CODE

435 436
        context = {
            'csrf': csrf(request)['csrf_token'],
437
            'accordion': render_accordion(user, request, course, chapter, section, field_data_cache),
438
            'COURSE_TITLE': course.display_name_with_default_escaped,
439 440
            'course': course,
            'init': '',
441
            'fragment': Fragment(),
442
            'staff_access': staff_access,
443
            'studio_url': studio_url,
444
            'masquerade': masquerade,
445
            'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
446
            'bookmarks_api_url': bookmarks_api_url,
447
            'language_preference': language_preference,
448
            'disable_optimizely': True,
449
        }
450

451 452
        now = datetime.now(UTC())
        effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
453
        if not in_preview_mode() and staff_access and now < effective_start:
454 455 456 457
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True

458 459 460 461 462
        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:
463
            # Check first to see if we should instead redirect the user to an Entrance Exam
464 465 466
            if course_has_entrance_exam(course):
                exam_chapter = get_entrance_exam_content(request, course)
                if exam_chapter:
467 468 469 470 471 472 473 474 475
                    exam_section = None
                    if exam_chapter.get_children():
                        exam_section = exam_chapter.get_children()[0]
                        if exam_section:
                            return redirect('courseware_section',
                                            course_id=unicode(course_key),
                                            chapter=exam_chapter.url_name,
                                            section=exam_section.url_name)

476 477 478 479
            # 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)

480
        chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
Victor Shnayder committed
481
        if chapter_descriptor is not None:
482
            save_child_position(course_module, chapter)
483
        else:
Brian Wilson committed
484
            raise Http404('No chapter descriptor found with name {}'.format(chapter))
Victor Shnayder committed
485

486
        chapter_module = course_module.get_child_by(lambda m: m.location.name == chapter)
487 488
        if chapter_module is None:
            # User may be trying to access a chapter that isn't live yet
489 490
            if masquerade and masquerade.role == 'student':  # if staff is masquerading as student be kinder, don't 404
                log.debug('staff masquerading as student: no chapter %s', chapter)
491
                return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
492
            raise Http404
Victor Shnayder committed
493

494
        if course_has_entrance_exam(course):
495 496 497 498
            # Message should not appear outside the context of entrance exam subsection.
            # if section is none then we don't need to show message on welcome back screen also.
            if getattr(chapter_module, 'is_entrance_exam', False) and section is not None:
                context['entrance_exam_current_score'] = get_entrance_exam_score(request, course)
499
                context['entrance_exam_passed'] = user_has_passed_entrance_exam(request, course)
500

Victor Shnayder committed
501
        if section is not None:
502
            section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
503

Victor Shnayder committed
504 505
            if section_descriptor is None:
                # Specifically asked-for section doesn't exist
506 507
                if masquerade and masquerade.role == 'student':  # don't 404 if staff is masquerading as student
                    log.debug('staff masquerading as student: no section %s', section)
508
                    return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
Victor Shnayder committed
509 510
                raise Http404

511 512 513 514
            ## Allow chromeless operation
            if section_descriptor.chrome:
                chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")]
                if 'accordion' not in chrome:
515
                    context['disable_accordion'] = True
516 517 518 519 520 521
                if 'tabs' not in chrome:
                    context['disable_tabs'] = True

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

522 523
            # 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
524
            section_descriptor = modulestore().get_item(section_descriptor.location, depth=None)
525

526
            # Load all descendants of the section, because we're going to display its
527
            # html, which in general will need all of its children
528 529
            field_data_cache.add_descriptor_descendents(
                section_descriptor, depth=None
530
            )
531

532
            section_module = get_module_for_descriptor(
533
                user,
534 535
                request,
                section_descriptor,
536
                field_data_cache,
537
                course_key,
538 539
                position,
                course=course
540
            )
541

Victor Shnayder committed
542
            if section_module is None:
Victor Shnayder committed
543 544 545 546
                # User may be trying to be clever and access something
                # they don't have access to.
                raise Http404

547
            # Save where we are in the chapter.
548
            save_child_position(chapter_module, section)
549 550
            section_render_context = {'activate_block_id': request.GET.get('activate_block_id')}
            context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
551
            context['section_title'] = section_descriptor.display_name_with_default_escaped
552
        else:
Victor Shnayder committed
553 554
            prev_section = get_current_child(chapter_module)
            if prev_section is None:
555 556 557 558 559 560
                # 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()]))
561
        result = render_to_response('courseware/courseware.html', context)
562
    except Exception as e:
563 564 565 566 567 568

        # 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")

569 570 571
        if isinstance(e, Http404):
            # let it propagate
            raise
572

573 574 575
        # In production, don't want to let a 500 out for any reason
        if settings.DEBUG:
            raise
576
        else:
577
            log.exception(
578 579
                u"Error in index view: user=%s, effective_user=%s, course=%s, chapter=%s section=%s position=%s",
                request.user, user, course, chapter, section, position
580
            )
581
            try:
582 583 584 585
                result = render_to_response('courseware/courseware-error.html', {
                    'staff_access': staff_access,
                    'course': course
                })
586
            except:
587 588 589 590
                # 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
591

592
    return result
593

Victor Shnayder committed
594

595
@ensure_csrf_cookie
596
@ensure_valid_course_key
597 598 599 600 601
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
    """
602
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
603
    items = modulestore().get_items(course_key, qualifiers={'name': module_id})
604 605

    if len(items) == 0:
606 607 608 609
        raise Http404(
            u"Could not find id: {0} in course_id: {1}. Referer: {2}".format(
                module_id, course_id, request.META.get("HTTP_REFERER", "")
            ))
610
    if len(items) > 1:
611
        log.warning(
612 613 614 615 616 617
            u"Multiple items found with id: %s in course_id: %s. Referer: %s. Using first: %s",
            module_id,
            course_id,
            request.META.get("HTTP_REFERER", ""),
            items[0].location.to_deprecated_string()
        )
618

619
    return jump_to(request, course_id, items[0].location.to_deprecated_string())
620 621 622


@ensure_csrf_cookie
stv committed
623
def jump_to(_request, course_id, location):
624
    """
625
    Show the page that contains a specific location.
626

627
    If the location is invalid or not in any class, return a 404.
628

629 630
    Otherwise, delegates to the index view to figure out whether this user
    has access, and what they should see.
631
    """
632
    try:
633 634
        course_key = CourseKey.from_string(course_id)
        usage_key = UsageKey.from_string(location).replace(course_key=course_key)
635 636
    except InvalidKeyError:
        raise Http404(u"Invalid course_key or usage_key")
637
    try:
638
        redirect_url = get_redirect_url(course_key, usage_key)
639
    except ItemNotFoundError:
640
        raise Http404(u"No data at this location: {0}".format(usage_key))
641
    except NoPathToItem:
642
        raise Http404(u"This location is not in any class: {0}".format(usage_key))
643

644
    return redirect(redirect_url)
645

Calen Pennington committed
646

647
@ensure_csrf_cookie
648
@ensure_valid_course_key
649
def course_info(request, course_id):
650
    """
651 652 653
    Display the course's info.html, or 404 if there is no such course.

    Assumes the course_id is in a valid format.
654
    """
655
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
656
    with modulestore().bulk_operations(course_key):
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
        course = get_course_by_id(course_key, depth=2)
        access_response = has_access(request.user, 'load', course, course_key)

        if not access_response:

            # The user doesn't have access to the course. If they're
            # denied permission due to the course not being live yet,
            # redirect to the dashboard page.
            if isinstance(access_response, StartDateError):
                start_date = strftime_localized(course.start, 'SHORT_DATE')
                params = urllib.urlencode({'notlive': start_date})
                return redirect('{0}?{1}'.format(reverse('dashboard'), params))
            # Otherwise, give a 404 to avoid leaking info about access
            # control.
            raise Http404("Course not found.")

673 674
        staff_access = has_access(request.user, 'staff', course)
        masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
675

676 677 678 679 680 681 682 683
        # if user is not enrolled in a course then app will show enroll/get register link inside course info page.
        show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(user, course.id)
        if show_enroll_banner and hasattr(course_key, 'ccx'):
            # if course is CCX and user is not enrolled/registered then do not let him open course direct via link for
            # self registration. Because only CCX coach can register/enroll a student. If un-enrolled user try
            # to access CCX redirect him to dashboard.
            return redirect(reverse('dashboard'))

684 685
        # If the user needs to take an entrance exam to access this course, then we'll need
        # to send them to that specific course module before allowing them into other areas
686
        if user_must_complete_entrance_exam(request, user, course):
687 688
            return redirect(reverse('courseware', args=[unicode(course.id)]))

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

694 695 696 697 698 699 700 701 702 703
        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')

        context = {
            'request': request,
704
            'masquerade_user': user,
705 706 707 708
            'course_id': course_key.to_deprecated_string(),
            'cache': None,
            'course': course,
            'staff_access': staff_access,
709
            'masquerade': masquerade,
710 711 712 713
            'studio_url': studio_url,
            'show_enroll_banner': show_enroll_banner,
            'url_to_enroll': url_to_enroll,
        }
714

715
        # Get the URL of the user's last position in order to display the 'where you were last' message
716
        context['last_accessed_courseware_url'] = None
717
        if SelfPacedConfiguration.current().enable_course_home_improvements:
718
            context['last_accessed_courseware_url'] = get_last_accessed_courseware(course, request)
719

720
        now = datetime.now(UTC())
721
        effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
722
        if not in_preview_mode() and staff_access and now < effective_start:
723 724 725
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True
726

727
        return render_to_response('courseware/info.html', context)
728

Calen Pennington committed
729

730 731
def get_last_accessed_courseware(course, request):
    """
732 733
    Return the URL the courseware module that this request's user last
    accessed, or None if it cannot be found.
734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
    """
    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
        course.id, request.user, course, depth=2
    )
    course_module = get_module_for_descriptor(
        request.user, request, course, field_data_cache, course.id, course=course
    )
    chapter_module = get_current_child(course_module)
    if chapter_module is not None:
        section_module = get_current_child(chapter_module)
        if section_module is not None:
            url = reverse('courseware_section', kwargs={
                'course_id': unicode(course.id),
                'chapter': chapter_module.url_name,
                'section': section_module.url_name
            })
750 751
            return url
    return None
752 753


Victor Shnayder committed
754
@ensure_csrf_cookie
755
@ensure_valid_course_key
Victor Shnayder committed
756 757 758 759 760 761
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.
    """
762 763

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
764

765
    course = get_course_with_access(request.user, 'load', course_key)
Victor Shnayder committed
766

767
    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
Victor Shnayder committed
768 769
    if tab is None:
        raise Http404
770

771
    contents = get_static_tab_contents(
Calen Pennington committed
772 773 774 775
        request,
        course,
        tab
    )
Victor Shnayder committed
776 777 778
    if contents is None:
        raise Http404

779 780 781 782 783
    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Victor Shnayder committed
784

Calen Pennington committed
785

786
@ensure_csrf_cookie
787
@ensure_valid_course_key
788 789 790 791 792 793
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.
    """
794

795
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
796

797
    course = get_course_with_access(request.user, 'load', course_key)
798
    staff_access = bool(has_access(request.user, 'staff', course))
799

800 801 802 803
    return render_to_response('courseware/syllabus.html', {
        'course': course,
        'staff_access': staff_access,
    })
804

Victor Shnayder committed
805

806
def registered_for_course(course, user):
807
    """
808
    Return True if user is registered for course, else False
809
    """
810 811 812
    if user is None:
        return False
    if user.is_authenticated():
813
        return CourseEnrollment.is_enrolled(user, course.id)
814 815 816
    else:
        return False

Calen Pennington committed
817

818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836
def get_cosmetic_display_price(course, registration_price):
    """
    Return Course Price as a string preceded by correct currency, or 'Free'
    """
    currency_symbol = settings.PAID_COURSE_REGISTRATION_CURRENCY[1]

    price = course.cosmetic_display_price
    if registration_price > 0:
        price = registration_price

    if price:
        # Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a
        # numerical amount in that currency. Adjust this display as needed for your language.
        return _("{currency_symbol}{price}").format(currency_symbol=currency_symbol, price=price)
    else:
        # Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free.
        return _('Free')


837
@ensure_csrf_cookie
838
@cache_if_anonymous()
839
def course_about(request, course_id):
840 841 842 843 844
    """
    Display the course's about page.

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

846
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
847

848 849 850 851 852 853 854
    if hasattr(course_key, 'ccx'):
        # if un-enrolled/non-registered user try to access CCX (direct for registration)
        # then do not show him about page to avoid self registration.
        # Note: About page will only be shown to user who is not register. So that he can register. But for
        # CCX only CCX coach can enroll students.
        return redirect(reverse('dashboard'))

jsa committed
855
    with modulestore().bulk_operations(course_key):
856 857
        permission = get_permission_for_course_about()
        course = get_course_with_access(request.user, permission, course_key)
858

jsa committed
859 860
        if microsite.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)):
            return redirect(reverse('info', args=[course.id.to_deprecated_string()]))
861

jsa committed
862
        registered = registered_for_course(course, request.user)
863

864
        staff_access = bool(has_access(request.user, 'staff', course))
jsa committed
865
        studio_url = get_studio_url(course, 'settings/details')
866

jsa committed
867 868 869 870 871
        if has_access(request.user, 'load', course):
            course_target = reverse('info', args=[course.id.to_deprecated_string()])
        else:
            course_target = reverse('about_course', args=[course.id.to_deprecated_string()])

872
        show_courseware_link = bool(
873 874 875 876 877 878
            (
                has_access(request.user, 'load', course)
                and has_access(request.user, 'view_courseware_with_prerequisites', course)
            )
            or settings.FEATURES.get('ENABLE_LMS_MIGRATION')
        )
jsa committed
879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894

        # 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 = ""

        _is_shopping_cart_enabled = is_shopping_cart_enabled()
        if _is_shopping_cart_enabled:
            registration_price = CourseMode.min_course_price_for_currency(course_key,
                                                                          settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
            if request.user.is_authenticated():
                cart = shoppingcart.models.Order.get_cart_for_user(request.user)
                in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_key) or \
                    shoppingcart.models.CourseRegCodeItem.contained_in_order(cart, course_key)

            reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format(
895
                reg_url=reverse('register_user'), course_id=urllib.quote(str(course_id)))
jsa committed
896

897
        course_price = get_cosmetic_display_price(course, registration_price)
898
        can_add_course_to_cart = _is_shopping_cart_enabled and registration_price
899

jsa committed
900
        # Used to provide context to message to student if enrollment not allowed
901
        can_enroll = bool(has_access(request.user, 'enroll', course))
jsa committed
902
        invitation_only = course.invitation_only
903
        is_course_full = CourseEnrollment.objects.is_course_full(course)
jsa committed
904 905 906 907 908 909 910 911 912

        # 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)

        is_shib_course = uses_shib(course)

913 914 915
        # get prerequisite courses display names
        pre_requisite_courses = get_prerequisite_courses_display(course)

916 917 918
        # Overview
        overview = CourseOverview.get_from_id(course.id)

jsa committed
919 920 921 922 923 924
        return render_to_response('courseware/course_about.html', {
            'course': course,
            'staff_access': staff_access,
            'studio_url': studio_url,
            'registered': registered,
            'course_target': course_target,
925 926
            'is_cosmetic_price_enabled': settings.FEATURES.get('ENABLE_COSMETIC_DISPLAY_PRICE'),
            'course_price': course_price,
jsa committed
927 928 929 930 931 932 933 934 935 936 937
            'in_cart': in_cart,
            'reg_then_add_to_cart_link': reg_then_add_to_cart_link,
            'show_courseware_link': show_courseware_link,
            'is_course_full': is_course_full,
            'can_enroll': can_enroll,
            'invitation_only': invitation_only,
            'active_reg_button': active_reg_button,
            'is_shib_course': is_shib_course,
            # 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.
            'disable_courseware_header': True,
938
            'can_add_course_to_cart': can_add_course_to_cart,
jsa committed
939
            'cart_link': reverse('shoppingcart.views.show_cart'),
940 941
            'pre_requisite_courses': pre_requisite_courses,
            'course_image_urls': overview.image_urls,
jsa committed
942
        })
943 944


945
@transaction.non_atomic_requests
946 947
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
948
@ensure_valid_course_key
949
def progress(request, course_id, student_id=None):
950
    """ Display the progress page. """
951

952
    course_key = CourseKey.from_string(course_id)
953

954
    with modulestore().bulk_operations(course_key):
955
        return _progress(request, course_key, student_id)
956 957


958
def _progress(request, course_key, student_id):
959 960 961 962
    """
    Unwrapped version of "progress".

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

964
    Course staff are allowed to see the progress of students in their class.
965
    """
966
    course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
967 968 969 970 971 972

    # 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)]))

973
    staff_access = bool(has_access(request.user, 'staff', course))
974 975 976 977 978 979
    try:
        coach_access = has_ccx_coach_role(request.user, course_key)
    except CCXLocatorValidationException:
        coach_access = False

    has_access_on_students_profiles = staff_access or coach_access
980 981 982 983 984 985

    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
986
        if not has_access_on_students_profiles:
987
            raise Http404
988 989 990 991 992
        try:
            student = User.objects.get(id=student_id)
        # Check for ValueError if 'student_id' cannot be converted to integer.
        except (ValueError, User.DoesNotExist):
            raise Http404
993

994 995
    # NOTE: To make sure impersonation by instructor works, use
    # student instead of request.user in the rest of the function.
996

997
    # The pre-fetching of groups is done to make auth checks not require an
998
    # additional DB lookup (this kills the Progress page in particular).
999
    student = User.objects.prefetch_related("groups").get(id=student.id)
1000 1001 1002 1003 1004

    with outer_atomic():
        field_data_cache = grades.field_data_cache_for_grading(course, student)
        scores_client = ScoresClient.from_field_data_cache(field_data_cache)

1005 1006 1007 1008 1009 1010
    courseware_summary = grades.progress_summary(
        student, request, course, field_data_cache=field_data_cache, scores_client=scores_client
    )
    grade_summary = grades.grade(
        student, request, course, field_data_cache=field_data_cache, scores_client=scores_client
    )
1011
    studio_url = get_studio_url(course, 'settings/grading')
1012

1013 1014 1015
    if courseware_summary is None:
        #This means the student didn't have access to the course (which the instructor requested)
        raise Http404
1016

1017
    # checking certificate generation configuration
1018
    show_generate_cert_btn = certs_api.cert_generation_enabled(course_key)
1019

1020 1021 1022
    context = {
        'course': course,
        'courseware_summary': courseware_summary,
1023
        'studio_url': studio_url,
1024 1025 1026
        'grade_summary': grade_summary,
        'staff_access': staff_access,
        'student': student,
1027
        'passed': is_course_passed(course, grade_summary),
1028
        'show_generate_cert_btn': show_generate_cert_btn,
Will Daly committed
1029
        'credit_course_requirements': _credit_course_requirements(course_key, student),
1030 1031
    }

1032
    if show_generate_cert_btn:
1033 1034
        cert_status = certs_api.certificate_downloadable_status(student, course_key)
        context.update(cert_status)
1035
        # showing the certificate web view button if feature flags are enabled.
1036
        if certs_api.has_html_certificates_enabled(course_key, course):
1037 1038 1039
            if certs_api.get_active_web_certificate(course) is not None:
                context.update({
                    'show_cert_web_view': True,
1040
                    'cert_web_view_url': certs_api.get_certificate_url(course_id=course_key, uuid=cert_status['uuid']),
1041 1042 1043 1044 1045 1046 1047
                })
            else:
                context.update({
                    'is_downloadable': False,
                    'is_generating': True,
                    'download_url': None
                })
1048

1049
    with outer_atomic():
1050
        response = render_to_response('courseware/progress.html', context)
1051

1052
    return response
1053 1054


Will Daly committed
1055 1056 1057 1058 1059 1060 1061
def _credit_course_requirements(course_key, student):
    """Return information about which credit requirements a user has satisfied.

    Arguments:
        course_key (CourseKey): Identifier for the course.
        student (User): Currently logged in user.

1062 1063
    Returns: dict if the credit eligibility enabled and it is a credit course
    and the user is enrolled in either verified or credit mode, and None otherwise.
Will Daly committed
1064 1065 1066 1067 1068 1069 1070 1071

    """
    # If credit eligibility is not enabled or this is not a credit course,
    # short-circuit and return `None`.  This indicates that credit requirements
    # should NOT be displayed on the progress page.
    if not (settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course(course_key)):
        return None

1072 1073 1074 1075 1076 1077 1078
    # If student is enrolled not enrolled in verified or credit mode,
    # short-circuit and return None. This indicates that
    # credit requirements should NOT be displayed on the progress page.
    enrollment = CourseEnrollment.get_enrollment(student, course_key)
    if enrollment.mode not in REQUIREMENTS_DISPLAY_MODES:
        return None

1079 1080 1081
    # Credit requirement statuses for which user does not remain eligible to get credit.
    non_eligible_statuses = ['failed', 'declined']

Will Daly committed
1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
    # Retrieve the status of the user for each eligibility requirement in the course.
    # For each requirement, the user's status is either "satisfied", "failed", or None.
    # In this context, `None` means that we don't know the user's status, either because
    # the user hasn't done something (for example, submitting photos for verification)
    # or we're waiting on more information (for example, a response from the photo
    # verification service).
    requirement_statuses = get_credit_requirement_status(course_key, student.username)

    # If the user has been marked as "eligible", then they are *always* eligible
    # unless someone manually intervenes.  This could lead to some strange behavior
    # if the requirements change post-launch.  For example, if the user was marked as eligible
    # for credit, then a new requirement was added, the user will see that they're eligible
    # AND that one of the requirements is still pending.
    # We're assuming here that (a) we can mitigate this by properly training course teams,
    # and (b) it's a better user experience to allow students who were at one time
    # marked as eligible to continue to be eligible.
    # If we need to, we can always manually move students back to ineligible by
    # deleting CreditEligibility records in the database.
    if is_user_eligible_for_credit(student.username, course_key):
        eligibility_status = "eligible"

    # If the user has *failed* any requirements (for example, if a photo verification is denied),
    # then the user is NOT eligible for credit.
1105
    elif any(requirement['status'] in non_eligible_statuses for requirement in requirement_statuses):
Will Daly committed
1106 1107 1108 1109 1110 1111 1112 1113 1114
        eligibility_status = "not_eligible"

    # Otherwise, the user may be eligible for credit, but the user has not
    # yet completed all the requirements.
    else:
        eligibility_status = "partial_eligible"

    return {
        'eligibility_status': eligibility_status,
1115
        'requirements': requirement_statuses,
Will Daly committed
1116 1117 1118
    }


1119
@login_required
1120
@ensure_valid_course_key
1121
def submission_history(request, course_id, student_username, location):
1122
    """Render an HTML fragment (meant for inclusion elsewhere) that renders a
1123
    history of all state changes made by this user for this problem location.
1124
    Right now this only works for problems because that's all
1125 1126
    StudentModuleHistory records.
    """
1127 1128

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1129 1130

    try:
1131
        usage_key = course_key.make_usage_key_from_deprecated_string(location)
1132 1133
    except (InvalidKeyError, AssertionError):
        return HttpResponse(escape(_(u'Invalid location.')))
1134

1135
    course = get_course_overview_with_access(request.user, 'load', course_key)
1136
    staff_access = bool(has_access(request.user, 'staff', course))
1137

1138 1139
    # Permission Denied if they don't have staff access and are trying to see
    # somebody else's submission history.
1140 1141 1142
    if (student_username != request.user.username) and (not staff_access):
        raise PermissionDenied

1143
    user_state_client = DjangoXBlockUserStateClient()
1144
    try:
1145
        history_entries = list(user_state_client.get_history(student_username, usage_key))
1146
    except DjangoXBlockUserStateClient.DoesNotExist:
1147 1148 1149 1150
        return HttpResponse(escape(_(u'User {username} has never accessed problem {location}').format(
            username=student_username,
            location=location
        )))
1151

1152 1153 1154
    # This is ugly, but until we have a proper submissions API that we can use to provide
    # the scores instead, it will have to do.
    scores = list(StudentModuleHistory.objects.filter(
Peter Fogg committed
1155 1156 1157
        student_module__module_state_key=usage_key,
        student_module__student__username=student_username,
        student_module__course_id=course_key
1158 1159 1160 1161 1162 1163
    ).order_by('-id'))

    if len(scores) != len(history_entries):
        log.warning(
            "Mismatch when fetching scores for student "
            "history for course %s, user %s, xblock %s. "
1164 1165
            "%d scores were found, and %d history entries were found. "
            "Matching scores to history entries by date for display.",
1166 1167
            course_id,
            student_username,
1168 1169 1170
            location,
            len(scores),
            len(history_entries),
1171 1172
        )
        scores_by_date = {
1173
            score.created: score
1174 1175 1176 1177 1178 1179 1180
            for score in scores
        }
        scores = [
            scores_by_date[history.updated]
            for history in history_entries
        ]

1181 1182
    context = {
        'history_entries': history_entries,
1183
        'scores': scores,
1184
        'username': student_username,
1185
        'location': location,
1186
        'course_id': course_key.to_deprecated_string()
1187 1188 1189
    }

    return render_to_response('courseware/submission_history.html', context)
1190 1191 1192 1193 1194 1195


def get_static_tab_contents(request, course, tab):
    """
    Returns the contents for the given static tab
    """
1196
    loc = course.id.make_usage_key(
1197 1198 1199 1200
        tab.type,
        tab.url_slug,
    )
    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
1201
        course.id, request.user, modulestore().get_item(loc), depth=0
1202 1203
    )
    tab_module = get_module(
1204
        request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path, course=course
1205 1206
    )

1207
    logging.debug('course_module = %s', tab_module)
1208 1209 1210 1211

    html = ''
    if tab_module is not None:
        try:
1212
            html = tab_module.render(STUDENT_VIEW).content
1213 1214
        except Exception:  # pylint: disable=broad-except
            html = render_to_string('courseware/error-message.html', None)
1215
            log.exception(
1216
                u"Error rendering course=%s, tab=%s", course, tab['url_slug']
1217
            )
1218 1219

    return html
1220 1221 1222


@require_GET
1223
@ensure_valid_course_key
1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239
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.
    """
1240 1241

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1242 1243 1244 1245

    try:
        course = get_course(course_key, depth=2)
    except ValueError:
1246 1247 1248 1249
        return HttpResponse(status=404)

    anonymous_user = AnonymousUser()
    anonymous_user.known = False  # make these "noauth" requests like module_render.handle_xblock_callback_noauth
1250
    lti_descriptors = modulestore().get_items(course.id, qualifiers={'category': 'lti'})
1251 1252 1253 1254 1255 1256 1257

    lti_noauth_modules = [
        get_module_for_descriptor(
            anonymous_user,
            request,
            descriptor,
            FieldDataCache.cache_for_descriptor_descendents(
1258
                course_key,
1259 1260 1261
                anonymous_user,
                descriptor
            ),
1262 1263
            course_key,
            course=course
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279
        )
        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')
1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306


@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,
    )
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327


def is_course_passed(course, grade_summary=None, student=None, request=None):
    """
    check user's course passing status. return True if passed

    Arguments:
        course : course object
        grade_summary (dict) : contains student grade details.
        student : user object
        request (HttpRequest)

    Returns:
        returns bool value
    """
    nonzero_cutoffs = [cutoff for cutoff in course.grade_cutoffs.values() if cutoff > 0]
    success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else None

    if grade_summary is None:
        grade_summary = grades.grade(student, request, course)

1328
    return success_cutoff and grade_summary['percent'] >= success_cutoff
1329 1330


1331 1332
# Grades can potentially be written - if so, let grading manage the transaction.
@transaction.non_atomic_requests
1333 1334
@require_POST
def generate_user_cert(request, course_id):
1335
    """Start generating a new certificate for the user.
1336

1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349
    Certificate generation is allowed if:
    * The user has passed the course, and
    * The user does not already have a pending/completed certificate.

    Note that if an error occurs during certificate generation
    (for example, if the queue is down), then we simply mark the
    certificate generation task status as "error" and re-run
    the task with a management command.  To students, the certificate
    will appear to be "generating" until it is re-run.

    Args:
        request (HttpRequest): The POST request to this view.
        course_id (unicode): The identifier for the course.
1350 1351

    Returns:
1352 1353
        HttpResponse: 200 on success, 400 if a new certificate cannot be generated.

1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373
    """

    if not request.user.is_authenticated():
        log.info(u"Anon user trying to generate certificate for %s", course_id)
        return HttpResponseBadRequest(
            _('You must be signed in to {platform_name} to create a certificate.').format(
                platform_name=settings.PLATFORM_NAME
            )
        )

    student = request.user
    course_key = CourseKey.from_string(course_id)

    course = modulestore().get_course(course_key, depth=2)
    if not course:
        return HttpResponseBadRequest(_("Course is not valid"))

    if not is_course_passed(course, None, student, request):
        return HttpResponseBadRequest(_("Your certificate will be available when you pass the course."))

1374
    certificate_status = certs_api.certificate_downloadable_status(student, course.id)
1375

1376 1377 1378
    if certificate_status["is_downloadable"]:
        return HttpResponseBadRequest(_("Certificate has already been created."))
    elif certificate_status["is_generating"]:
1379
        return HttpResponseBadRequest(_("Certificate is being created."))
1380 1381 1382 1383 1384 1385 1386
    else:
        # If the certificate is not already in-process or completed,
        # then create a new certificate generation task.
        # If the certificate cannot be added to the queue, this will
        # mark the certificate with "error" status, so it can be re-run
        # with a management command.  From the user's perspective,
        # it will appear that the certificate task was submitted successfully.
1387
        certs_api.generate_user_certificates(student, course.id, course=course, generation_mode='self')
1388
        _track_successful_certificate_generation(student.id, course.id)
1389
        return HttpResponse()
1390 1391 1392


def _track_successful_certificate_generation(user_id, course_id):  # pylint: disable=invalid-name
1393 1394
    """
    Track a successful certificate generation event.
1395 1396 1397

    Arguments:
        user_id (str): The ID of the user generting the certificate.
1398
        course_id (CourseKey): Identifier for the course.
1399 1400 1401 1402
    Returns:
        None

    """
1403
    if settings.LMS_SEGMENT_KEY:
1404 1405
        event_name = 'edx.bi.user.certificate.generate'
        tracking_context = tracker.get_tracker().resolve_context()
1406 1407 1408 1409 1410 1411 1412 1413 1414

        analytics.track(
            user_id,
            event_name,
            {
                'category': 'certificates',
                'label': unicode(course_id)
            },
            context={
1415
                'ip': tracking_context.get('ip'),
1416 1417 1418 1419 1420
                'Google Analytics': {
                    'clientId': tracking_context.get('client_id')
                }
            }
        )
1421 1422 1423


@require_http_methods(["GET", "POST"])
1424
def render_xblock(request, usage_key_string, check_if_enrolled=True):
1425 1426 1427 1428 1429 1430 1431 1432
    """
    Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
    The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
    """
    usage_key = UsageKey.from_string(usage_key_string)
    usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
    course_key = usage_key.course_key

1433 1434 1435 1436
    requested_view = request.GET.get('view', 'student_view')
    if requested_view != 'student_view':
        return HttpResponseBadRequest("Rendering of the xblock view '{}' is not supported.".format(requested_view))

1437 1438
    with modulestore().bulk_operations(course_key):
        # verify the user has access to the course, including enrollment check
1439 1440 1441 1442
        try:
            course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=check_if_enrolled)
        except UserNotEnrolled:
            raise Http404("Course not found.")
1443 1444 1445

        # get the block, which verifies whether the user has access to the block.
        block, _ = get_module_by_usage_id(
1446
            request, unicode(course_key), unicode(usage_key), disable_staff_debug_info=True, course=course
1447 1448 1449 1450 1451 1452 1453 1454
        )

        context = {
            'fragment': block.render('student_view', context=request.GET),
            'course': course,
            'disable_accordion': True,
            'allow_iframing': True,
            'disable_header': True,
1455
            'disable_footer': True,
1456 1457
            'disable_window_wrap': True,
            'disable_preview_menu': True,
1458
            'staff_access': bool(has_access(request.user, 'staff', course)),
1459 1460 1461
            'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
        }
        return render_to_response('courseware/courseware-chromeless.html', context)
1462 1463 1464 1465 1466


# Translators: "percent_sign" is the symbol "%". "platform_name" is a
# string identifying the name of this installation, such as "edX".
FINANCIAL_ASSISTANCE_HEADER = _(
Bill DeRusha committed
1467
    '{platform_name} now offers financial assistance for learners who want to earn Verified Certificates but'
1468
    ' who may not be able to pay the Verified Certificate fee. Eligible learners may receive up to 90{percent_sign} off'
1469 1470
    ' the Verified Certificate fee for a course.\nTo apply for financial assistance, enroll in the'
    ' audit track for a course that offers Verified Certificates, and then complete this application.'
1471
    ' Note that you must complete a separate application for each course you take.\n We plan to use this'
1472 1473
    ' information to evaluate your application for financial assistance and to further develop our'
    ' financial assistance program.'
1474 1475 1476 1477 1478 1479
).format(
    percent_sign="%",
    platform_name=settings.PLATFORM_NAME
).split('\n')


Bill DeRusha committed
1480
FA_INCOME_LABEL = _('Annual Household Income')
1481
FA_REASON_FOR_APPLYING_LABEL = _(
1482
    'Tell us about your current financial situation. Why do you need assistance?'
1483 1484 1485 1486 1487 1488 1489
)
FA_GOALS_LABEL = _(
    'Tell us about your learning or professional goals. How will a Verified Certificate in'
    ' this course help you achieve these goals?'
)
FA_EFFORT_LABEL = _(
    'Tell us about your plans for this course. What steps will you take to help you complete'
Bill DeRusha committed
1490
    ' the course work and receive a certificate?'
1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514
)
FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 250 and 500 words or so in your response.')


@login_required
def financial_assistance(_request):
    """Render the initial financial assistance page."""
    return render_to_response('financial-assistance/financial-assistance.html', {
        'header_text': FINANCIAL_ASSISTANCE_HEADER
    })


@login_required
@require_POST
def financial_assistance_request(request):
    """Submit a request for financial assistance to Zendesk."""
    try:
        data = json.loads(request.body)
        # Simple sanity check that the session belongs to the user
        # submitting an FA request
        username = data['username']
        if request.user.username != username:
            return HttpResponseForbidden()

1515
        course_id = data['course']
Bill DeRusha committed
1516
        course = modulestore().get_course(CourseKey.from_string(course_id))
1517
        legal_name = data['name']
1518 1519 1520 1521 1522 1523
        email = data['email']
        country = data['country']
        income = data['income']
        reason_for_applying = data['reason_for_applying']
        goals = data['goals']
        effort = data['effort']
1524
        marketing_permission = data['mktg-permission']
1525 1526 1527
        ip_address = get_ip(request)
    except ValueError:
        # Thrown if JSON parsing fails
1528
        return HttpResponseBadRequest(u'Could not parse request JSON.')
Bill DeRusha committed
1529 1530
    except InvalidKeyError:
        # Thrown if course key parsing fails
1531
        return HttpResponseBadRequest(u'Could not parse request course key.')
1532 1533
    except KeyError as err:
        # Thrown if fields are missing
1534
        return HttpResponseBadRequest(u'The field {} is required.'.format(err.message))
1535 1536 1537 1538

    zendesk_submitted = _record_feedback_in_zendesk(
        legal_name,
        email,
1539
        u'Financial assistance request for learner {username} in course {course_name}'.format(
1540
            username=username,
Bill DeRusha committed
1541
            course_name=course.display_name
1542
        ),
1543
        u'Financial Assistance Request',
Bill DeRusha committed
1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562
        {'course_id': course_id},
        # Send the application as additional info on the ticket so
        # that it is not shown when support replies. This uses
        # OrderedDict so that information is presented in the right
        # order.
        OrderedDict((
            ('Username', username),
            ('Full Name', legal_name),
            ('Course ID', course_id),
            ('Annual Household Income', income),
            ('Country', country),
            ('Allowed for marketing purposes', 'Yes' if marketing_permission else 'No'),
            (FA_REASON_FOR_APPLYING_LABEL, '\n' + reason_for_applying + '\n\n'),
            (FA_GOALS_LABEL, '\n' + goals + '\n\n'),
            (FA_EFFORT_LABEL, '\n' + effort + '\n\n'),
            ('Client IP', ip_address),
        )),
        group_name='Financial Assistance',
        require_update=True
1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580
    )

    if not zendesk_submitted:
        # The call to Zendesk failed. The frontend will display a
        # message to the user.
        return HttpResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    return HttpResponse(status=status.HTTP_204_NO_CONTENT)


@login_required
def financial_assistance_form(request):
    """Render the financial assistance application form page."""
    user = request.user
    enrolled_courses = [
        {'name': enrollment.course_overview.display_name, 'value': unicode(enrollment.course_id)}
        for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created')
        if CourseMode.objects.filter(
1581
            Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC())),
1582 1583 1584 1585 1586 1587 1588 1589 1590
            course_id=enrollment.course_id,
            mode_slug=CourseMode.VERIFIED
        ).exists()
        and enrollment.mode != CourseMode.VERIFIED
    ]
    return render_to_response('financial-assistance/apply.html', {
        'header_text': FINANCIAL_ASSISTANCE_HEADER,
        'student_faq_url': marketing_link('FAQ'),
        'dashboard_url': reverse('dashboard'),
1591
        'account_settings_url': reverse('account_settings'),
1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618
        'platform_name': settings.PLATFORM_NAME,
        'user_details': {
            'email': user.email,
            'username': user.username,
            'name': user.profile.name,
            'country': str(user.profile.country.name),
        },
        'submit_url': reverse('submit_financial_assistance_request'),
        'fields': [
            {
                'name': 'course',
                'type': 'select',
                'label': _('Course'),
                'placeholder': '',
                'defaultValue': '',
                'required': True,
                'options': enrolled_courses,
                'instructions': _(
                    'Select the course for which you want to earn a verified certificate. If'
                    ' the course does not appear in the list, make sure that you have enrolled'
                    ' in the audit track for the course.'
                )
            },
            {
                'name': 'income',
                'type': 'text',
                'label': FA_INCOME_LABEL,
1619
                'placeholder': _('income in US Dollars ($)'),
1620 1621 1622
                'defaultValue': '',
                'required': True,
                'restrictions': {},
1623
                'instructions': _('Specify your annual household income in US Dollars.')
1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667
            },
            {
                'name': 'reason_for_applying',
                'type': 'textarea',
                'label': FA_REASON_FOR_APPLYING_LABEL,
                'placeholder': '',
                'defaultValue': '',
                'required': True,
                'restrictions': {
                    'min_length': settings.FINANCIAL_ASSISTANCE_MIN_LENGTH,
                    'max_length': settings.FINANCIAL_ASSISTANCE_MAX_LENGTH
                },
                'instructions': FA_SHORT_ANSWER_INSTRUCTIONS
            },
            {
                'name': 'goals',
                'type': 'textarea',
                'label': FA_GOALS_LABEL,
                'placeholder': '',
                'defaultValue': '',
                'required': True,
                'restrictions': {
                    'min_length': settings.FINANCIAL_ASSISTANCE_MIN_LENGTH,
                    'max_length': settings.FINANCIAL_ASSISTANCE_MAX_LENGTH
                },
                'instructions': FA_SHORT_ANSWER_INSTRUCTIONS
            },
            {
                'name': 'effort',
                'type': 'textarea',
                'label': FA_EFFORT_LABEL,
                'placeholder': '',
                'defaultValue': '',
                'required': True,
                'restrictions': {
                    'min_length': settings.FINANCIAL_ASSISTANCE_MIN_LENGTH,
                    'max_length': settings.FINANCIAL_ASSISTANCE_MAX_LENGTH
                },
                'instructions': FA_SHORT_ANSWER_INSTRUCTIONS
            },
            {
                'placeholder': '',
                'name': 'mktg-permission',
                'label': _(
1668 1669
                    'I allow edX to use the information provided in this application '
                    '(except for financial information) for edX marketing purposes.'
1670 1671 1672 1673
                ),
                'defaultValue': '',
                'type': 'checkbox',
                'required': False,
1674
                'instructions': '',
1675 1676 1677 1678
                'restrictions': {}
            }
        ],
    })