views.py 56.5 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 django.utils import translation
12
from django.utils.translation import ugettext as _
13
from django.utils.translation import ungettext
Piotr Mitros committed
14 15

from django.conf import settings
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.contrib.auth.models import User, AnonymousUser
20
from django.contrib.auth.decorators import login_required
21
from django.utils.timezone import UTC
22
from django.views.decorators.http import require_GET, require_POST, require_http_methods
23
from django.http import Http404, HttpResponse, HttpResponseBadRequest
David Ormsbee committed
24
from django.shortcuts import redirect
25
from certificates import api as certs_api
26
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
27
from django.views.decorators.csrf import ensure_csrf_cookie
28
from django.views.decorators.cache import cache_control
29
from django.db import transaction
30
from markupsafe import escape
31

32
from courseware import grades
33
from courseware.access import has_access, in_preview_mode, _adjust_start_date_for_beta_testers
34
from courseware.access_response import StartDateError
35
from courseware.courses import (
36
    get_courses, get_course, get_course_by_id,
37 38 39
    get_studio_url, get_course_with_access,
    sort_by_announcement,
    sort_by_start_date,
40
    UserNotEnrolled)
41
from courseware.masquerade import setup_masquerade
42 43 44 45 46
from openedx.core.djangoapps.credit.api import (
    get_credit_requirement_status,
    is_user_eligible_for_credit,
    is_credit_course
)
47
from courseware.models import StudentModuleHistory
48
from courseware.model_data import FieldDataCache, ScoresClient
49
from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id
50 51 52 53 54 55 56
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
)
57
from courseware.user_state_client import DjangoXBlockUserStateClient
58
from course_modes.models import CourseMode
59

60
from open_ended_grading import open_ended_notifications
61
from open_ended_grading.views import StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
62
from student.models import UserTestGroup, CourseEnrollment
63
from student.views import is_course_blocked
64
from util.cache import cache, cache_if_anonymous
65
from util.date_utils import strftime_localized
66
from xblock.fragment import Fragment
67
from xmodule.modulestore.django import modulestore
68
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
69
from xmodule.tabs import CourseTabList
70
from xmodule.x_module import STUDENT_VIEW
71
import shoppingcart
72
from shoppingcart.models import CourseRegistrationCode
73
from shoppingcart.utils import is_shopping_cart_enabled
74
from opaque_keys import InvalidKeyError
75
from util.milestones_helpers import get_prerequisite_courses_display
76

77
from microsite_configuration import microsite
78
from opaque_keys.edx.locations import SlashSeparatedCourseKey
79
from opaque_keys.edx.keys import CourseKey, UsageKey
80
from instructor.enrollment import uses_shib
81

82
from util.db import commit_on_success_with_read_committed
83 84 85 86

import survey.utils
import survey.views

87
from util.views import ensure_valid_course_key
88 89
from eventtracking import tracker
import analytics
90
from courseware.url_helpers import get_redirect_url
91

92
log = logging.getLogger("edx.courseware")
93

94
template_imports = {'urllib': urllib}
Piotr Mitros committed
95

96
CONTENT_DEPTH = 2
97

98

99
def user_groups(user):
100 101 102
    """
    TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
    """
103 104 105 106 107 108 109 110 111
    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)
112 113
    if settings.DEBUG:
        group_names = None
114 115 116 117 118 119 120 121

    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


122
@ensure_csrf_cookie
123
@cache_if_anonymous()
124
def courses(request):
125
    """
126
    Render "find courses" page.  The course selection work is done in courseware.courses.
127
    """
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
    courses_list = []
    course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
    if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
        courses_list = get_courses(request.user, request.META.get('HTTP_HOST'))

        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}
    )
143

144

145
def render_accordion(user, request, course, chapter, section, field_data_cache):
146 147 148
    """
    Draws navigation bar. Takes current position in accordion as
    parameter.
149

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

152
    course, chapter, and section are the url_names.
153

154 155
    Returns the html string
    """
156
    # grab the table of contents
157
    toc = toc_for_course(user, request, course, chapter, section, field_data_cache)
158

159 160
    context = dict([
        ('toc', toc),
161
        ('course_id', course.id.to_deprecated_string()),
162 163 164
        ('csrf', csrf(request)['csrf_token']),
        ('due_date_display_format', course.due_date_display_format)
    ] + template_imports.items())
165
    return render_to_string('courseware/accordion.html', context)
166

Piotr Mitros committed
167

168
def get_current_child(xmodule, min_depth=None):
Victor Shnayder committed
169 170
    """
    Get the xmodule.position's display item of an xmodule that has a position and
171 172 173 174 175 176 177
    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.

178
    Returns None only if there are no children at all.
Victor Shnayder committed
179
    """
180 181 182 183 184 185 186 187
    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
188
                                child.has_children_at_depth(min_depth - 1) and child.get_display_items()]
189 190 191 192
            default_child = content_children[0] if content_children else None

        return default_child

Victor Shnayder committed
193 194 195
    if not hasattr(xmodule, 'position'):
        return None

196
    if xmodule.position is None:
197
        return _get_default_child_module(xmodule.get_display_items())
198 199 200
    else:
        # position is 1-indexed.
        pos = xmodule.position - 1
Victor Shnayder committed
201 202

    children = xmodule.get_display_items()
203 204
    if 0 <= pos < len(children):
        child = children[pos]
Victor Shnayder committed
205
    elif len(children) > 0:
206 207 208
        # module has a set position, but the position is out of range.
        # return default child.
        child = _get_default_child_module(children)
Victor Shnayder committed
209 210 211 212 213
    else:
        child = None
    return child


214
def redirect_to_course_position(course_module, content_depth):
Victor Shnayder committed
215
    """
216 217 218 219 220 221 222 223 224
    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
225

Victor Shnayder committed
226
    """
227
    urlargs = {'course_id': course_module.id.to_deprecated_string()}
228
    chapter = get_current_child(course_module, min_depth=content_depth)
Victor Shnayder committed
229
    if chapter is None:
Victor Shnayder committed
230
        # oops.  Something bad has happened.
231
        raise Http404("No chapter found when loading current position in course")
232 233 234 235 236

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

Victor Shnayder committed
237
    # Relying on default of returning first child
238
    section = get_current_child(chapter, min_depth=content_depth - 1)
239 240 241 242 243
    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
244

Calen Pennington committed
245

246
def save_child_position(seq_module, child_name):
Victor Shnayder committed
247
    """
Victor Shnayder committed
248
    child_name: url_name of the child
Victor Shnayder committed
249
    """
250
    for position, c in enumerate(seq_module.get_display_items(), start=1):
251
        if c.location.name == child_name:
Victor Shnayder committed
252
            # Only save if position changed
Victor Shnayder committed
253 254
            if position != seq_module.position:
                seq_module.position = position
255 256
    # Save this new position to the underlying KeyValueStore
    seq_module.save()
Victor Shnayder committed
257

Calen Pennington committed
258

259
def save_positions_recursively_up(user, request, field_data_cache, xmodule, course=None):
260 261 262 263 264 265 266 267 268 269 270
    """
    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)
271 272 273 274 275 276 277 278
            parent = get_module_for_descriptor(
                user,
                request,
                parent_descriptor,
                field_data_cache,
                current_module.location.course_key,
                course=course
            )
279 280 281 282 283 284 285

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

        current_module = parent


286 287 288 289 290
def chat_settings(course, user):
    """
    Returns a dict containing the settings required to connect to a
    Jabber chat server and room.
    """
291 292 293 294 295 296
    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

297
    return {
298
        'domain': domain,
299 300 301 302 303

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

        'username': "{USER}@{DOMAIN}".format(
304
            USER=user.username, DOMAIN=domain
305 306 307 308 309 310
        ),

        # 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(
311
            USER=user.username, DOMAIN=domain
312 313 314 315
        ),
    }


316
@login_required
317 318
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
319
@ensure_valid_course_key
320
@commit_on_success_with_read_committed
321
def index(request, course_id, chapter=None, section=None,
322
          position=None):
323
    """
Victor Shnayder committed
324 325 326 327 328 329 330 331
    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.
332 333 334 335

    Arguments:

     - request    : HTTP request
336 337 338
     - course_id  : course id (str: ORG/course/URL_NAME)
     - chapter    : chapter url_name (str)
     - section    : section url_name (str)
339 340 341 342 343
     - position   : position in module, eg of <sequential> module (str)

    Returns:

     - HTTPresponse
344
    """
345

346
    course_key = CourseKey.from_string(course_id)
347

348
    user = User.objects.prefetch_related("groups").get(id=request.user.id)
349

350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
    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'))
366

367
    request.user = user  # keep just one instance of User
368
    with modulestore().bulk_operations(course_key):
369
        return _index_bulk_op(request, course_key, chapter, section, position)
370 371


372 373 374 375 376
# pylint: disable=too-many-statements
def _index_bulk_op(request, course_key, chapter, section, position):
    """
    Render the index page for the specified course.
    """
377 378 379 380 381
    # Verify that position a string is in fact an int
    if position is not None:
        try:
            int(position)
        except ValueError:
382
            raise Http404(u"Position {} is not an integer!".format(position))
383

384 385 386
    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)
387

388
    registered = registered_for_course(course, user)
389
    if not registered:
390
        # TODO (vshnayder): do course instructors need to be registered to see course?
391 392
        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()]))
393

394 395 396 397 398 399 400 401 402 403 404
    # 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'))

405 406 407 408 409 410 411 412 413
    # 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)]))
414 415 416 417 418
    # 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)]))

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

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

431
        studio_url = get_studio_url(course, 'course')
432

433 434
        context = {
            'csrf': csrf(request)['csrf_token'],
435
            'accordion': render_accordion(user, request, course, chapter, section, field_data_cache),
436
            'COURSE_TITLE': course.display_name_with_default,
437 438
            'course': course,
            'init': '',
439
            'fragment': Fragment(),
440
            'staff_access': staff_access,
441
            'studio_url': studio_url,
442
            'masquerade': masquerade,
443
            'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
444
        }
445

446 447
        now = datetime.now(UTC())
        effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
448
        if not in_preview_mode() and staff_access and now < effective_start:
449 450 451 452
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True

453 454 455 456 457
        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:
458
            # Check first to see if we should instead redirect the user to an Entrance Exam
459 460 461
            if course_has_entrance_exam(course):
                exam_chapter = get_entrance_exam_content(request, course)
                if exam_chapter:
462 463 464 465 466 467 468 469 470
                    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)

471 472 473 474
            # 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)

475 476
        # Only show the chat if it's enabled by the course and in the
        # settings.
477
        show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT']
478
        if show_chat:
479
            context['chat'] = chat_settings(course, request.user)
480 481 482 483 484 485
            # 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
486

487
        chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
Victor Shnayder committed
488
        if chapter_descriptor is not None:
489
            save_child_position(course_module, chapter)
490
        else:
Brian Wilson committed
491
            raise Http404('No chapter descriptor found with name {}'.format(chapter))
Victor Shnayder committed
492

493
        chapter_module = course_module.get_child_by(lambda m: m.location.name == chapter)
494 495
        if chapter_module is None:
            # User may be trying to access a chapter that isn't live yet
496 497
            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)
498
                return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
499
            raise Http404
Victor Shnayder committed
500

501
        if course_has_entrance_exam(course):
502 503 504 505
            # 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)
506
                context['entrance_exam_passed'] = user_has_passed_entrance_exam(request, course)
507

Victor Shnayder committed
508
        if section is not None:
509
            section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
510

Victor Shnayder committed
511 512
            if section_descriptor is None:
                # Specifically asked-for section doesn't exist
513 514
                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)
515
                    return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
Victor Shnayder committed
516 517
                raise Http404

518 519 520 521
            ## Allow chromeless operation
            if section_descriptor.chrome:
                chrome = [s.strip() for s in section_descriptor.chrome.lower().split(",")]
                if 'accordion' not in chrome:
522
                    context['disable_accordion'] = True
523 524 525 526 527 528
                if 'tabs' not in chrome:
                    context['disable_tabs'] = True

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

529 530
            # 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
531
            section_descriptor = modulestore().get_item(section_descriptor.location, depth=None)
532

533
            # Load all descendants of the section, because we're going to display its
534
            # html, which in general will need all of its children
535 536
            field_data_cache.add_descriptor_descendents(
                section_descriptor, depth=None
537
            )
538

539
            section_module = get_module_for_descriptor(
540
                user,
541 542
                request,
                section_descriptor,
543
                field_data_cache,
544
                course_key,
545 546
                position,
                course=course
547
            )
548

Victor Shnayder committed
549
            if section_module is None:
Victor Shnayder committed
550 551 552 553
                # User may be trying to be clever and access something
                # they don't have access to.
                raise Http404

554
            # Save where we are in the chapter.
555
            save_child_position(chapter_module, section)
556 557
            section_render_context = {'activate_block_id': request.GET.get('activate_block_id')}
            context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
558
            context['section_title'] = section_descriptor.display_name_with_default
559
        else:
Victor Shnayder committed
560
            # section is none, so display a message
561
            studio_url = get_studio_url(course, 'course')
Victor Shnayder committed
562 563
            prev_section = get_current_child(chapter_module)
            if prev_section is None:
564 565 566 567 568 569
                # 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()]))
570 571 572 573 574
            prev_section_url = reverse('courseware_section', kwargs={
                'course_id': course_key.to_deprecated_string(),
                'chapter': chapter_descriptor.url_name,
                'section': prev_section.url_name
            })
575 576 577 578
            context['fragment'] = Fragment(content=render_to_string(
                'courseware/welcome-back.html',
                {
                    'course': course,
579
                    'studio_url': studio_url,
580 581 582 583 584
                    'chapter_module': chapter_module,
                    'prev_section': prev_section,
                    'prev_section_url': prev_section_url
                }
            ))
585

586
        result = render_to_response('courseware/courseware.html', context)
587
    except Exception as e:
588 589 590 591 592 593

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

594 595 596
        if isinstance(e, Http404):
            # let it propagate
            raise
597

598 599 600
        # In production, don't want to let a 500 out for any reason
        if settings.DEBUG:
            raise
601
        else:
602
            log.exception(
603 604
                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
605
            )
606
            try:
607 608 609 610
                result = render_to_response('courseware/courseware-error.html', {
                    'staff_access': staff_access,
                    'course': course
                })
611
            except:
612 613 614 615
                # 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
616

617
    return result
618

Victor Shnayder committed
619

620
@ensure_csrf_cookie
621
@ensure_valid_course_key
622 623 624 625 626
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
    """
627
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
628
    items = modulestore().get_items(course_key, qualifiers={'name': module_id})
629 630

    if len(items) == 0:
631 632 633 634
        raise Http404(
            u"Could not find id: {0} in course_id: {1}. Referer: {2}".format(
                module_id, course_id, request.META.get("HTTP_REFERER", "")
            ))
635
    if len(items) > 1:
636
        log.warning(
637 638 639 640 641 642
            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()
        )
643

644
    return jump_to(request, course_id, items[0].location.to_deprecated_string())
645 646 647


@ensure_csrf_cookie
stv committed
648
def jump_to(_request, course_id, location):
649
    """
650
    Show the page that contains a specific location.
651

652
    If the location is invalid or not in any class, return a 404.
653

654 655
    Otherwise, delegates to the index view to figure out whether this user
    has access, and what they should see.
656
    """
657
    try:
658 659
        course_key = CourseKey.from_string(course_id)
        usage_key = UsageKey.from_string(location).replace(course_key=course_key)
660 661
    except InvalidKeyError:
        raise Http404(u"Invalid course_key or usage_key")
662
    try:
663
        redirect_url = get_redirect_url(course_key, usage_key)
664
    except ItemNotFoundError:
665
        raise Http404(u"No data at this location: {0}".format(usage_key))
666
    except NoPathToItem:
667
        raise Http404(u"This location is not in any class: {0}".format(usage_key))
668

669
    return redirect(redirect_url)
670

Calen Pennington committed
671

672
@ensure_csrf_cookie
673
@ensure_valid_course_key
674
def course_info(request, course_id):
675
    """
676 677 678
    Display the course's info.html, or 404 if there is no such course.

    Assumes the course_id is in a valid format.
679
    """
680
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
681
    with modulestore().bulk_operations(course_key):
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
        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.")

698 699
        staff_access = has_access(request.user, 'staff', course)
        masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
700

701 702
        # 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
703
        if user_must_complete_entrance_exam(request, user, course):
704 705
            return redirect(reverse('courseware', args=[unicode(course.id)]))

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

711 712 713 714 715 716 717 718
        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')

719
        show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(user, course.id)
720 721 722 723 724 725 726

        context = {
            'request': request,
            'course_id': course_key.to_deprecated_string(),
            'cache': None,
            'course': course,
            'staff_access': staff_access,
727
            'masquerade': masquerade,
728 729 730 731
            'studio_url': studio_url,
            'show_enroll_banner': show_enroll_banner,
            'url_to_enroll': url_to_enroll,
        }
732

733
        now = datetime.now(UTC())
734
        effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
735
        if not in_preview_mode() and staff_access and now < effective_start:
736 737 738
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True
739

740
        return render_to_response('courseware/info.html', context)
741

Calen Pennington committed
742

Victor Shnayder committed
743
@ensure_csrf_cookie
744
@ensure_valid_course_key
Victor Shnayder committed
745 746 747 748 749 750
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.
    """
751 752

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
753

754
    course = get_course_with_access(request.user, 'load', course_key)
Victor Shnayder committed
755

756
    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
Victor Shnayder committed
757 758
    if tab is None:
        raise Http404
759

760
    contents = get_static_tab_contents(
Calen Pennington committed
761 762 763 764
        request,
        course,
        tab
    )
Victor Shnayder committed
765 766 767
    if contents is None:
        raise Http404

768 769 770 771 772
    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Victor Shnayder committed
773

Calen Pennington committed
774

775
@ensure_csrf_cookie
776
@ensure_valid_course_key
777 778 779 780 781 782
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.
    """
783

784
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
785

786
    course = get_course_with_access(request.user, 'load', course_key)
787
    staff_access = bool(has_access(request.user, 'staff', course))
788

789 790 791 792
    return render_to_response('courseware/syllabus.html', {
        'course': course,
        'staff_access': staff_access,
    })
793

Victor Shnayder committed
794

795
def registered_for_course(course, user):
796
    """
797
    Return True if user is registered for course, else False
798
    """
799 800 801
    if user is None:
        return False
    if user.is_authenticated():
802
        return CourseEnrollment.is_enrolled(user, course.id)
803 804 805
    else:
        return False

Calen Pennington committed
806

807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
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')


826
@ensure_csrf_cookie
827
@cache_if_anonymous()
828
def course_about(request, course_id):
829 830 831 832 833
    """
    Display the course's about page.

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

835
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
836

jsa committed
837 838 839 840 841 842
    with modulestore().bulk_operations(course_key):
        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)
843

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

jsa committed
847
        registered = registered_for_course(course, request.user)
848

849
        staff_access = bool(has_access(request.user, 'staff', course))
jsa committed
850
        studio_url = get_studio_url(course, 'settings/details')
851

jsa committed
852 853 854 855 856
        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()])

857
        show_courseware_link = bool(
858 859 860 861 862 863
            (
                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
864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879

        # 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(
880
                reg_url=reverse('register_user'), course_id=urllib.quote(str(course_id)))
jsa committed
881

882
        course_price = get_cosmetic_display_price(course, registration_price)
883
        can_add_course_to_cart = _is_shopping_cart_enabled and registration_price
884

jsa committed
885
        # Used to provide context to message to student if enrollment not allowed
886
        can_enroll = bool(has_access(request.user, 'enroll', course))
jsa committed
887
        invitation_only = course.invitation_only
888
        is_course_full = CourseEnrollment.objects.is_course_full(course)
jsa committed
889 890 891 892 893 894 895 896 897

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

898 899 900
        # get prerequisite courses display names
        pre_requisite_courses = get_prerequisite_courses_display(course)

jsa committed
901 902 903 904 905 906
        return render_to_response('courseware/course_about.html', {
            'course': course,
            'staff_access': staff_access,
            'studio_url': studio_url,
            'registered': registered,
            'course_target': course_target,
907 908
            'is_cosmetic_price_enabled': settings.FEATURES.get('ENABLE_COSMETIC_DISPLAY_PRICE'),
            'course_price': course_price,
jsa committed
909 910 911 912 913 914 915 916 917 918 919
            '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,
920
            'can_add_course_to_cart': can_add_course_to_cart,
jsa committed
921
            'cart_link': reverse('shoppingcart.views.show_cart'),
922
            'pre_requisite_courses': pre_requisite_courses
jsa committed
923
        })
924 925


926 927
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
928
@transaction.commit_manually
929
@ensure_valid_course_key
930
def progress(request, course_id, student_id=None):
931 932 933 934
    """
    Wraps "_progress" with the manual_transaction context manager just in case
    there are unanticipated errors.
    """
935 936 937

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)

938 939 940
    with modulestore().bulk_operations(course_key):
        with grades.manual_transaction():
            return _progress(request, course_key, student_id)
941 942


943
def _progress(request, course_key, student_id):
944 945 946 947
    """
    Unwrapped version of "progress".

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

949
    Course staff are allowed to see the progress of students in their class.
950
    """
951
    course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
952 953 954 955 956 957

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

958
    staff_access = bool(has_access(request.user, 'staff', course))
959 960 961 962 963 964

    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
965
        if not staff_access:
966
            raise Http404
967 968 969 970 971
        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
972

973 974
    # NOTE: To make sure impersonation by instructor works, use
    # student instead of request.user in the rest of the function.
975

976
    # The pre-fetching of groups is done to make auth checks not require an
977
    # additional DB lookup (this kills the Progress page in particular).
978
    student = User.objects.prefetch_related("groups").get(id=student.id)
979 980 981 982 983 984 985 986
    field_data_cache = grades.field_data_cache_for_grading(course, student)
    scores_client = ScoresClient.from_field_data_cache(field_data_cache)
    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
    )
987
    studio_url = get_studio_url(course, 'settings/grading')
988

989 990 991
    if courseware_summary is None:
        #This means the student didn't have access to the course (which the instructor requested)
        raise Http404
992

993
    # checking certificate generation configuration
994
    show_generate_cert_btn = certs_api.cert_generation_enabled(course_key)
995

996 997 998
    context = {
        'course': course,
        'courseware_summary': courseware_summary,
999
        'studio_url': studio_url,
1000 1001 1002
        'grade_summary': grade_summary,
        'staff_access': staff_access,
        'student': student,
1003
        'passed': is_course_passed(course, grade_summary),
1004
        'show_generate_cert_btn': show_generate_cert_btn,
Will Daly committed
1005
        'credit_course_requirements': _credit_course_requirements(course_key, student),
1006 1007
    }

1008
    if show_generate_cert_btn:
1009
        context.update(certs_api.certificate_downloadable_status(student, course_key))
1010
        # showing the certificate web view button if feature flags are enabled.
1011
        if certs_api.has_html_certificates_enabled(course_key, course):
1012 1013 1014 1015
            if certs_api.get_active_web_certificate(course) is not None:
                context.update({
                    'show_cert_web_view': True,
                    'cert_web_view_url': u'{url}'.format(
1016 1017
                        url=certs_api.get_certificate_url(
                            user_id=student.id,
1018
                            course_id=unicode(course.id)
1019
                        )
1020 1021 1022 1023 1024 1025 1026 1027
                    )
                })
            else:
                context.update({
                    'is_downloadable': False,
                    'is_generating': True,
                    'download_url': None
                })
1028

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

1032
    return response
1033 1034


Will Daly committed
1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 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 1076 1077 1078 1079 1080 1081 1082 1083
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.

    Returns: dict

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

    # 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.
    elif any(requirement['status'] == 'failed' for requirement in requirement_statuses):
        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,
1084
        'requirements': requirement_statuses,
Will Daly committed
1085 1086 1087
    }


1088
@login_required
1089
@ensure_valid_course_key
1090
def submission_history(request, course_id, student_username, location):
1091
    """Render an HTML fragment (meant for inclusion elsewhere) that renders a
1092
    history of all state changes made by this user for this problem location.
1093
    Right now this only works for problems because that's all
1094 1095
    StudentModuleHistory records.
    """
1096 1097

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1098 1099

    try:
1100
        usage_key = course_key.make_usage_key_from_deprecated_string(location)
1101 1102
    except (InvalidKeyError, AssertionError):
        return HttpResponse(escape(_(u'Invalid location.')))
1103

1104
    course = get_course_with_access(request.user, 'load', course_key)
1105
    staff_access = bool(has_access(request.user, 'staff', course))
1106

1107 1108
    # Permission Denied if they don't have staff access and are trying to see
    # somebody else's submission history.
1109 1110 1111
    if (student_username != request.user.username) and (not staff_access):
        raise PermissionDenied

1112
    user_state_client = DjangoXBlockUserStateClient()
1113
    try:
1114
        history_entries = list(user_state_client.get_history(student_username, usage_key))
1115
    except DjangoXBlockUserStateClient.DoesNotExist:
1116 1117 1118 1119
        return HttpResponse(escape(_(u'User {username} has never accessed problem {location}').format(
            username=student_username,
            location=location
        )))
1120

1121 1122 1123
    # 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
1124 1125 1126
        student_module__module_state_key=usage_key,
        student_module__student__username=student_username,
        student_module__course_id=course_key
1127 1128 1129 1130 1131 1132
    ).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. "
1133 1134
            "%d scores were found, and %d history entries were found. "
            "Matching scores to history entries by date for display.",
1135 1136
            course_id,
            student_username,
1137 1138 1139
            location,
            len(scores),
            len(history_entries),
1140 1141
        )
        scores_by_date = {
1142
            score.created: score
1143 1144 1145 1146 1147 1148 1149
            for score in scores
        }
        scores = [
            scores_by_date[history.updated]
            for history in history_entries
        ]

1150 1151
    context = {
        'history_entries': history_entries,
1152
        'scores': scores,
1153
        'username': student_username,
1154
        'location': location,
1155
        'course_id': course_key.to_deprecated_string()
1156 1157 1158
    }

    return render_to_response('courseware/submission_history.html', context)
1159 1160 1161 1162 1163 1164 1165 1166


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 = {
1167 1168 1169
        StaffGradingTab.type: open_ended_notifications.staff_grading_notifications,
        PeerGradingTab.type: open_ended_notifications.peer_grading_notifications,
        OpenEndedGradingTab.type: open_ended_notifications.combined_notifications
1170 1171
    }

1172 1173
    if course_tab.name in tab_notification_handlers:
        notifications = tab_notification_handlers[course_tab.name](course, user)
1174 1175 1176 1177 1178 1179 1180 1181 1182 1183
        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
    """
1184
    loc = course.id.make_usage_key(
1185 1186 1187 1188
        tab.type,
        tab.url_slug,
    )
    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
1189
        course.id, request.user, modulestore().get_item(loc), depth=0
1190 1191
    )
    tab_module = get_module(
1192
        request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path, course=course
1193 1194
    )

1195
    logging.debug('course_module = %s', tab_module)
1196 1197 1198 1199

    html = ''
    if tab_module is not None:
        try:
1200
            html = tab_module.render(STUDENT_VIEW).content
1201 1202
        except Exception:  # pylint: disable=broad-except
            html = render_to_string('courseware/error-message.html', None)
1203
            log.exception(
1204
                u"Error rendering course=%s, tab=%s", course, tab['url_slug']
1205
            )
1206 1207

    return html
1208 1209 1210


@require_GET
1211
@ensure_valid_course_key
1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227
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.
    """
1228 1229

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1230 1231 1232 1233

    try:
        course = get_course(course_key, depth=2)
    except ValueError:
1234 1235 1236 1237
        return HttpResponse(status=404)

    anonymous_user = AnonymousUser()
    anonymous_user.known = False  # make these "noauth" requests like module_render.handle_xblock_callback_noauth
1238
    lti_descriptors = modulestore().get_items(course.id, qualifiers={'category': 'lti'})
1239 1240 1241 1242 1243 1244 1245

    lti_noauth_modules = [
        get_module_for_descriptor(
            anonymous_user,
            request,
            descriptor,
            FieldDataCache.cache_for_descriptor_descendents(
1246
                course_key,
1247 1248 1249
                anonymous_user,
                descriptor
            ),
1250 1251
            course_key,
            course=course
1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267
        )
        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')
1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294


@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,
    )
1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315


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)

1316
    return success_cutoff and grade_summary['percent'] >= success_cutoff
1317 1318 1319 1320


@require_POST
def generate_user_cert(request, course_id):
1321
    """Start generating a new certificate for the user.
1322

1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335
    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.
1336 1337

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

1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359
    """

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

1360
    certificate_status = certs_api.certificate_downloadable_status(student, course.id)
1361

1362 1363 1364
    if certificate_status["is_downloadable"]:
        return HttpResponseBadRequest(_("Certificate has already been created."))
    elif certificate_status["is_generating"]:
1365
        return HttpResponseBadRequest(_("Certificate is being created."))
1366 1367 1368 1369 1370 1371 1372
    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.
1373
        certs_api.generate_user_certificates(student, course.id, course=course, generation_mode='self')
1374
        _track_successful_certificate_generation(student.id, course.id)
1375
        return HttpResponse()
1376 1377 1378


def _track_successful_certificate_generation(user_id, course_id):  # pylint: disable=invalid-name
1379 1380
    """
    Track a successful certificate generation event.
1381 1382 1383

    Arguments:
        user_id (str): The ID of the user generting the certificate.
1384
        course_id (CourseKey): Identifier for the course.
1385 1386 1387 1388 1389
    Returns:
        None

    """
    if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
1390 1391
        event_name = 'edx.bi.user.certificate.generate'
        tracking_context = tracker.get_tracker().resolve_context()
1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405

        analytics.track(
            user_id,
            event_name,
            {
                'category': 'certificates',
                'label': unicode(course_id)
            },
            context={
                'Google Analytics': {
                    'clientId': tracking_context.get('client_id')
                }
            }
        )
1406 1407 1408


@require_http_methods(["GET", "POST"])
1409
def render_xblock(request, usage_key_string, check_if_enrolled=True):
1410 1411 1412 1413 1414 1415 1416 1417 1418 1419
    """
    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

    with modulestore().bulk_operations(course_key):
        # verify the user has access to the course, including enrollment check
1420 1421 1422 1423
        try:
            course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=check_if_enrolled)
        except UserNotEnrolled:
            raise Http404("Course not found.")
1424 1425 1426

        # get the block, which verifies whether the user has access to the block.
        block, _ = get_module_by_usage_id(
1427
            request, unicode(course_key), unicode(usage_key), disable_staff_debug_info=True, course=course
1428 1429 1430 1431 1432 1433 1434 1435 1436 1437
        )

        context = {
            'fragment': block.render('student_view', context=request.GET),
            'course': course,
            'disable_accordion': True,
            'allow_iframing': True,
            'disable_header': True,
            'disable_window_wrap': True,
            'disable_preview_menu': True,
1438
            'staff_access': bool(has_access(request.user, 'staff', course)),
1439 1440 1441
            'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
        }
        return render_to_response('courseware/courseware-chromeless.html', context)