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

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

11
import analytics
Piotr Mitros committed
12
from django.conf import settings
13 14
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User, AnonymousUser
15
from django.core.exceptions import PermissionDenied
16
from django.core.urlresolvers import reverse
attiyaishaque committed
17
from django.core.context_processors import csrf
18
from django.db import transaction
19 20
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
David Ormsbee committed
21
from django.shortcuts import redirect
22
from django.utils.decorators import method_decorator
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
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_GET, require_POST, require_http_methods
28
from django.views.generic import View
29
from eventtracking import tracker
30
from ipware.ip import get_ip
31
from markupsafe import escape
32 33 34
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
35
from rest_framework import status
36
from lms.djangoapps.instructor.views.api import require_global_staff
37

38 39 40
import shoppingcart
import survey.utils
import survey.views
41
from lms.djangoapps.ccx.utils import prep_course_for_grading
42
from certificates import api as certs_api
43
from certificates.models import CertificateStatuses
44
from openedx.core.djangoapps.models.course_details import CourseDetails
vkaracic committed
45
from commerce.utils import EcommerceService
attiyaishaque committed
46
from enrollment.api import add_enrollment
47
from course_modes.models import CourseMode
48
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
49
from courseware.access import has_access, has_ccx_coach_role, _adjust_start_date_for_beta_testers
50
from courseware.access_response import StartDateError
51
from courseware.access_utils import in_preview_mode
52
from courseware.courses import (
53 54 55 56 57 58 59
    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,
60 61
    sort_by_announcement,
    sort_by_start_date,
62 63
    UserNotEnrolled
)
64
from courseware.masquerade import setup_masquerade
65
from courseware.model_data import FieldDataCache
66
from courseware.models import StudentModule, BaseStudentModuleHistory
67
from courseware.url_helpers import get_redirect_url, get_redirect_url_for_global_staff
68 69
from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
70
from lms.djangoapps.instructor.enrollment import uses_shib
71
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
72
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
erm0l0v committed
73
from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context
74 75 76 77 78
from openedx.core.djangoapps.credit.api import (
    get_credit_requirement_status,
    is_user_eligible_for_credit,
    is_credit_course
)
79
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
80
from shoppingcart.utils import is_shopping_cart_enabled
81
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
82
from student.models import UserTestGroup, CourseEnrollment
Awais Jibran committed
83
from student.roles import GlobalStaff
84
from util.cache import cache, cache_if_anonymous
85
from util.date_utils import strftime_localized
86
from util.db import outer_atomic
87 88 89
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
90
from xmodule.modulestore.django import modulestore
91
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
92
from xmodule.tabs import CourseTabList
93
from xmodule.x_module import STUDENT_VIEW
94
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
95 96
from ..entrance_exams import user_must_complete_entrance_exam
from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id
97

98

99
log = logging.getLogger("edx.courseware")
100

Piotr Mitros committed
101

102 103 104
# Only display the requirements on learner dashboard for
# credit and verified modes.
REQUIREMENTS_DISPLAY_MODES = CourseMode.CREDIT_MODES + [CourseMode.VERIFIED]
105

106 107
CertData = namedtuple("CertData", ["cert_status", "title", "msg", "download_url", "cert_web_view_url"])

108

109
def user_groups(user):
110 111 112
    """
    TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
    """
113 114 115 116 117 118 119 120
    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
121
    group_names = cache.get(key)  # pylint: disable=no-member
122 123
    if settings.DEBUG:
        group_names = None
124 125 126

    if group_names is None:
        group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
127
        cache.set(key, group_names, cache_expiration)  # pylint: disable=no-member
128 129 130 131

    return group_names


132
@ensure_csrf_cookie
133
@cache_if_anonymous()
134
def courses(request):
135
    """
136
    Render "find courses" page.  The course selection work is done in courseware.courses.
137
    """
138 139 140
    courses_list = []
    course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
    if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
Renzo Lucioni committed
141
        courses_list = get_courses(request.user)
142

143
        if configuration_helpers.get_value(
vkaracic committed
144 145 146
                "ENABLE_COURSE_SORTING_BY_START_DATE",
                settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]
        ):
147 148 149 150 151 152 153 154
            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}
    )
155

156

157
def get_current_child(xmodule, min_depth=None, requested_child=None):
Victor Shnayder committed
158 159
    """
    Get the xmodule.position's display item of an xmodule that has a position and
160
    children.  If xmodule has no position or is out of bounds, return the first
161
    child with children of min_depth.
162 163 164 165 166

    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.

167
    Returns None only if there are no children at all.
Victor Shnayder committed
168
    """
169 170 171 172 173 174 175 176 177 178 179 180 181
    def _get_child(children):
        """
        Returns either the first or last child based on the value of
        the requested_child parameter.  If requested_child is None,
        returns the first child.
        """
        if requested_child == 'first':
            return children[0]
        elif requested_child == 'last':
            return children[-1]
        else:
            return children[0]

182 183
    def _get_default_child_module(child_modules):
        """Returns the first child of xmodule, subject to min_depth."""
184 185
        if min_depth <= 0:
            return _get_child(child_modules)
186
        else:
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
            content_children = [
                child for child in child_modules
                if child.has_children_at_depth(min_depth - 1) and child.get_display_items()
            ]
            return _get_child(content_children) if content_children else None

    child = None
    if hasattr(xmodule, 'position'):
        children = xmodule.get_display_items()
        if len(children) > 0:
            if xmodule.position is not None and not requested_child:
                pos = xmodule.position - 1  # position is 1-indexed
                if 0 <= pos < len(children):
                    child = children[pos]
                    if min_depth > 0 and not child.has_children_at_depth(min_depth - 1):
                        child = None
            if child is None:
                child = _get_default_child_module(children)
205

Victor Shnayder committed
206 207 208
    return child


209
@ensure_csrf_cookie
210
@ensure_valid_course_key
211 212 213 214 215
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
    """
216
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
217
    items = modulestore().get_items(course_key, qualifiers={'name': module_id})
218 219

    if len(items) == 0:
220 221 222 223
        raise Http404(
            u"Could not find id: {0} in course_id: {1}. Referer: {2}".format(
                module_id, course_id, request.META.get("HTTP_REFERER", "")
            ))
224
    if len(items) > 1:
225
        log.warning(
226 227 228 229 230 231
            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()
        )
232

233
    return jump_to(request, course_id, items[0].location.to_deprecated_string())
234 235 236


@ensure_csrf_cookie
stv committed
237
def jump_to(_request, course_id, location):
238
    """
239
    Show the page that contains a specific location.
240

241
    If the location is invalid or not in any class, return a 404.
242

243 244
    Otherwise, delegates to the index view to figure out whether this user
    has access, and what they should see.
245
    """
246
    try:
247 248
        course_key = CourseKey.from_string(course_id)
        usage_key = UsageKey.from_string(location).replace(course_key=course_key)
249 250
    except InvalidKeyError:
        raise Http404(u"Invalid course_key or usage_key")
251
    try:
252
        redirect_url = get_redirect_url(course_key, usage_key)
attiyaishaque committed
253 254 255 256 257
        user = _request.user
        user_is_global_staff = GlobalStaff().has_user(user)
        user_is_enrolled = CourseEnrollment.is_enrolled(user, course_key)
        if user_is_global_staff and not user_is_enrolled:
            redirect_url = get_redirect_url_for_global_staff(course_key, _next=redirect_url)
258
    except ItemNotFoundError:
259
        raise Http404(u"No data at this location: {0}".format(usage_key))
260
    except NoPathToItem:
261
        raise Http404(u"This location is not in any class: {0}".format(usage_key))
262

263
    return redirect(redirect_url)
264

Calen Pennington committed
265

266
@ensure_csrf_cookie
267
@ensure_valid_course_key
268
def course_info(request, course_id):
269
    """
270 271 272
    Display the course's info.html, or 404 if there is no such course.

    Assumes the course_id is in a valid format.
273
    """
274
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
275
    with modulestore().bulk_operations(course_key):
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
        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.")

292 293
        staff_access = has_access(request.user, 'staff', course)
        masquerade, user = setup_masquerade(request, course_key, staff_access, reset_masquerade_data=True)
294

295 296 297 298 299 300 301 302
        # 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'))

303 304
        # 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
305
        if user_must_complete_entrance_exam(request, user, course):
306 307
            return redirect(reverse('courseware', args=[unicode(course.id)]))

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

313 314 315 316
        is_from_dashboard = reverse('dashboard') in request.META.get('HTTP_REFERER', [])
        if course.bypass_home and is_from_dashboard:
            return redirect(reverse('courseware', args=[course_id]))

317 318 319 320 321 322 323 324 325 326
        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,
327
            'masquerade_user': user,
328 329 330 331
            'course_id': course_key.to_deprecated_string(),
            'cache': None,
            'course': course,
            'staff_access': staff_access,
332
            'masquerade': masquerade,
333 334 335 336
            'studio_url': studio_url,
            'show_enroll_banner': show_enroll_banner,
            'url_to_enroll': url_to_enroll,
        }
337

338
        # Get the URL of the user's last position in order to display the 'where you were last' message
339
        context['last_accessed_courseware_url'] = None
340
        if SelfPacedConfiguration.current().enable_course_home_improvements:
341
            context['last_accessed_courseware_url'] = get_last_accessed_courseware(course, request, user)
342

343
        now = datetime.now(UTC())
344
        effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
345
        if not in_preview_mode() and staff_access and now < effective_start:
346 347 348
            # Disable student view button if user is staff and
            # course is not yet visible to students.
            context['disable_student_access'] = True
349

erm0l0v committed
350 351 352
        if CourseEnrollment.is_enrolled(request.user, course.id):
            inject_coursetalk_keys_into_context(context, course_key)

353
        return render_to_response('courseware/info.html', context)
354

Calen Pennington committed
355

356
def get_last_accessed_courseware(course, request, user):
357
    """
358 359
    Return the courseware module URL that the user last accessed,
    or None if it cannot be found.
360 361 362 363 364
    """
    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
        course.id, request.user, course, depth=2
    )
    course_module = get_module_for_descriptor(
365
        user, request, course, field_data_cache, course.id, course=course
366 367 368 369 370 371 372 373 374 375
    )
    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
            })
376 377
            return url
    return None
378 379


Victor Shnayder committed
380
@ensure_csrf_cookie
381
@ensure_valid_course_key
Victor Shnayder committed
382 383 384 385 386 387
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.
    """
388 389

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
390

391
    course = get_course_with_access(request.user, 'load', course_key)
Victor Shnayder committed
392

393
    tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug)
Victor Shnayder committed
394 395
    if tab is None:
        raise Http404
396

397
    contents = get_static_tab_contents(
Calen Pennington committed
398 399 400 401
        request,
        course,
        tab
    )
Victor Shnayder committed
402 403 404
    if contents is None:
        raise Http404

405 406 407 408 409
    return render_to_response('courseware/static_tab.html', {
        'course': course,
        'tab': tab,
        'tab_contents': contents,
    })
Victor Shnayder committed
410

Calen Pennington committed
411

412
@ensure_csrf_cookie
413
@ensure_valid_course_key
414 415 416 417 418 419
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.
    """
420

421
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
422

423
    course = get_course_with_access(request.user, 'load', course_key)
424
    staff_access = bool(has_access(request.user, 'staff', course))
425

426 427 428 429
    return render_to_response('courseware/syllabus.html', {
        'course': course,
        'staff_access': staff_access,
    })
430

Victor Shnayder committed
431

432
def registered_for_course(course, user):
433
    """
434
    Return True if user is registered for course, else False
435
    """
436 437 438
    if user is None:
        return False
    if user.is_authenticated():
439
        return CourseEnrollment.is_enrolled(user, course.id)
440 441 442
    else:
        return False

Calen Pennington committed
443

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
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')

462

463
class EnrollStaffView(View):
attiyaishaque committed
464
    """
attiyaishaque committed
465 466 467 468 469
    Displays view for registering in the course to a global staff user.

    User can either choose to 'Enroll' or 'Don't Enroll' in the course.
      Enroll: Enrolls user in course and redirects to the courseware.
      Don't Enroll: Redirects user to course about page.
Awais Jibran committed
470

471 472 473
    Arguments:
     - request    : HTTP request
     - course_id  : course id
Awais Jibran committed
474

475
    Returns:
attiyaishaque committed
476
     - RedirectResponse
attiyaishaque committed
477
    """
478
    template_name = 'enroll_staff.html'
Awais Jibran committed
479

480 481 482 483
    @method_decorator(require_global_staff)
    @method_decorator(ensure_valid_course_key)
    def get(self, request, course_id):
        """
attiyaishaque committed
484
        Display enroll staff view to global staff user with `Enroll` and `Don't Enroll` options.
485 486
        """
        user = request.user
attiyaishaque committed
487
        course_key = CourseKey.from_string(course_id)
Awais Jibran committed
488
        with modulestore().bulk_operations(course_key):
489 490
            course = get_course_with_access(user, 'load', course_key)
            if not registered_for_course(course, user):
attiyaishaque committed
491 492 493 494
                context = {
                    'course': course,
                    'csrftoken': csrf(request)["csrf_token"]
                }
495 496 497 498 499 500
                return render_to_response(self.template_name, context)

    @method_decorator(require_global_staff)
    @method_decorator(ensure_valid_course_key)
    def post(self, request, course_id):
        """
attiyaishaque committed
501 502
        Either enrolls the user in course or redirects user to course about page
        depending upon the option (Enroll, Don't Enroll) chosen by the user.
503 504
        """
        _next = urllib.quote_plus(request.GET.get('next', 'info'), safe='/:?=')
attiyaishaque committed
505 506 507 508
        course_key = CourseKey.from_string(course_id)
        enroll = 'enroll' in request.POST
        if enroll:
            add_enrollment(request.user.username, course_id)
509 510
            log.info(
                u"User %s enrolled in %s via `enroll_staff` view",
511
                request.user.username,
512 513 514
                course_id
            )
            return redirect(_next)
attiyaishaque committed
515 516 517

        # In any other case redirect to the course about page.
        return redirect(reverse('about_course', args=[unicode(course_key)]))
Awais Jibran committed
518

519

520
@ensure_csrf_cookie
521
@cache_if_anonymous()
522
def course_about(request, course_id):
523 524 525 526 527
    """
    Display the course's about page.

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

529
    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
530

531 532 533 534 535 536 537
    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
538
    with modulestore().bulk_operations(course_key):
539 540
        permission = get_permission_for_course_about()
        course = get_course_with_access(request.user, permission, course_key)
541
        course_details = CourseDetails.populate(course)
vkaracic committed
542
        modes = CourseMode.modes_for_course_dict(course_key)
543

544
        if configuration_helpers.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)):
jsa committed
545
            return redirect(reverse('info', args=[course.id.to_deprecated_string()]))
546

jsa committed
547
        registered = registered_for_course(course, request.user)
548

549
        staff_access = bool(has_access(request.user, 'staff', course))
jsa committed
550
        studio_url = get_studio_url(course, 'settings/details')
551

jsa committed
552 553 554 555 556
        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()])

557
        show_courseware_link = bool(
558
            (
vkaracic committed
559 560 561
                has_access(request.user, 'load', course) and
                has_access(request.user, 'view_courseware_with_prerequisites', course)
            ) or settings.FEATURES.get('ENABLE_LMS_MIGRATION')
562
        )
jsa committed
563 564 565 566 567 568 569 570 571 572 573 574 575

        # Note: this is a flow for payment for course registration, not the Verified Certificate flow.
        in_cart = False
        reg_then_add_to_cart_link = ""

        _is_shopping_cart_enabled = is_shopping_cart_enabled()
        if _is_shopping_cart_enabled:
            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(
vkaracic committed
576 577 578 579 580 581
                reg_url=reverse('register_user'), course_id=urllib.quote(str(course_id))
            )

        # If the ecommerce checkout flow is enabled and the mode of the course is
        # professional or no id professional, we construct links for the enrollment
        # button to add the course to the ecommerce basket.
582 583
        ecomm_service = EcommerceService()
        ecommerce_checkout = ecomm_service.is_enabled(request.user)
vkaracic committed
584
        ecommerce_checkout_link = ''
585 586
        ecommerce_bulk_checkout_link = ''
        professional_mode = None
587
        is_professional_mode = CourseMode.PROFESSIONAL in modes or CourseMode.NO_ID_PROFESSIONAL_MODE in modes
588
        if ecommerce_checkout and is_professional_mode:
vkaracic committed
589 590
            professional_mode = modes.get(CourseMode.PROFESSIONAL, '') or \
                modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE, '')
591 592
            if professional_mode.sku:
                ecommerce_checkout_link = ecomm_service.checkout_page_url(professional_mode.sku)
593 594
            if professional_mode.bulk_sku:
                ecommerce_bulk_checkout_link = ecomm_service.checkout_page_url(professional_mode.bulk_sku)
595

596 597 598 599 600
        # Find the minimum price for the course across all course modes
        registration_price = CourseMode.min_course_price_for_currency(
            course_key,
            settings.PAID_COURSE_REGISTRATION_CURRENCY[0]
        )
601
        course_price = get_cosmetic_display_price(course, registration_price)
602 603 604

        # Determine which checkout workflow to use -- LMS shoppingcart or Otto basket
        can_add_course_to_cart = _is_shopping_cart_enabled and registration_price and not ecommerce_checkout_link
605

jsa committed
606
        # Used to provide context to message to student if enrollment not allowed
607
        can_enroll = bool(has_access(request.user, 'enroll', course))
jsa committed
608
        invitation_only = course.invitation_only
609
        is_course_full = CourseEnrollment.objects.is_course_full(course)
jsa committed
610 611 612 613 614 615 616 617 618

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

619 620 621
        # get prerequisite courses display names
        pre_requisite_courses = get_prerequisite_courses_display(course)

622 623 624
        # Overview
        overview = CourseOverview.get_from_id(course.id)

erm0l0v committed
625
        context = {
jsa committed
626
            'course': course,
627
            'course_details': course_details,
jsa committed
628 629 630 631
            'staff_access': staff_access,
            'studio_url': studio_url,
            'registered': registered,
            'course_target': course_target,
632 633
            'is_cosmetic_price_enabled': settings.FEATURES.get('ENABLE_COSMETIC_DISPLAY_PRICE'),
            'course_price': course_price,
jsa committed
634
            'in_cart': in_cart,
635
            'ecommerce_checkout': ecommerce_checkout,
vkaracic committed
636
            'ecommerce_checkout_link': ecommerce_checkout_link,
637
            'ecommerce_bulk_checkout_link': ecommerce_bulk_checkout_link,
vkaracic committed
638
            'professional_mode': professional_mode,
jsa committed
639 640 641 642 643 644 645 646 647 648
            '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,
649
            'can_add_course_to_cart': can_add_course_to_cart,
jsa committed
650
            'cart_link': reverse('shoppingcart.views.show_cart'),
651 652
            'pre_requisite_courses': pre_requisite_courses,
            'course_image_urls': overview.image_urls,
erm0l0v committed
653 654 655 656
        }
        inject_coursetalk_keys_into_context(context, course_key)

        return render_to_response('courseware/course_about.html', context)
657 658


659
@transaction.non_atomic_requests
660 661
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
662
@ensure_valid_course_key
663
def progress(request, course_id, student_id=None):
664
    """ Display the progress page. """
665
    course_key = CourseKey.from_string(course_id)
666

667
    with modulestore().bulk_operations(course_key):
668
        return _progress(request, course_key, student_id)
669 670


671
def _progress(request, course_key, student_id):
672 673 674 675
    """
    Unwrapped version of "progress".

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

677
    Course staff are allowed to see the progress of students in their class.
678
    """
679 680 681 682 683 684 685 686

    if student_id is not None:
        try:
            student_id = int(student_id)
        # Check for ValueError if 'student_id' cannot be converted to integer.
        except ValueError:
            raise Http404

687
    course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
688
    prep_course_for_grading(course, request)
689 690 691 692 693 694

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

695
    staff_access = bool(has_access(request.user, 'staff', course))
696 697 698 699 700

    if student_id is None or student_id == request.user.id:
        # always allowed to see your own profile
        student = request.user
    else:
701 702 703 704 705 706
        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
707
        # Requesting access to a different student's profile
708
        if not has_access_on_students_profiles:
709
            raise Http404
710 711
        try:
            student = User.objects.get(id=student_id)
712
        except User.DoesNotExist:
713
            raise Http404
714

715 716
    # NOTE: To make sure impersonation by instructor works, use
    # student instead of request.user in the rest of the function.
717

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

722
    course_grade = CourseGradeFactory(student).create(course, read_only=False)
723
    if not course_grade.has_access_to_course:
724
        # This means the student didn't have access to the course (which the instructor requested)
725
        raise Http404
726

727 728 729
    courseware_summary = course_grade.chapter_grades
    grade_summary = course_grade.summary

730 731
    studio_url = get_studio_url(course, 'settings/grading')

732
    # checking certificate generation configuration
733
    enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(student, course_key)
734

735 736 737
    context = {
        'course': course,
        'courseware_summary': courseware_summary,
738
        'studio_url': studio_url,
739 740 741
        'grade_summary': grade_summary,
        'staff_access': staff_access,
        'student': student,
742
        'passed': is_course_passed(course, grade_summary),
Will Daly committed
743
        'credit_course_requirements': _credit_course_requirements(course_key, student),
744
        'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode)
745 746
    }

747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
    with outer_atomic():
        response = render_to_response('courseware/progress.html', context)

    return response


def _get_cert_data(student, course, course_key, is_active, enrollment_mode):
    """Returns students course certificate related data.

    Arguments:
        student (User): Student for whom certificate to retrieve.
        course (Course): Course object for which certificate data to retrieve.
        course_key (CourseKey): Course identifier for course.
        is_active (Bool): Boolean value to check if course is active.
        enrollment_mode (String): Course mode in which student is enrolled.
762

763 764 765 766 767 768 769
    Returns:
        returns dict if course certificate is available else None.
    """

    if enrollment_mode == CourseMode.AUDIT:
        return CertData(
            CertificateStatuses.audit_passing,
770 771
            _('Your enrollment: Audit track'),
            _('You are enrolled in the audit track for this course. The audit track does not include a certificate.'),
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
            download_url=None,
            cert_web_view_url=None
        )

    show_generate_cert_btn = (
        is_active and CourseMode.is_eligible_for_certificate(enrollment_mode)
        and certs_api.cert_generation_enabled(course_key)
    )

    if not show_generate_cert_btn:
        return None

    if certs_api.is_certificate_invalid(student, course_key):
        return CertData(
            CertificateStatuses.invalidated,
787 788
            _('Your certificate has been invalidated'),
            _('Please contact your course team if you have any questions.'),
789 790 791 792 793 794 795 796
            download_url=None,
            cert_web_view_url=None
        )

    cert_downloadable_status = certs_api.certificate_downloadable_status(student, course_key)

    if cert_downloadable_status['is_downloadable']:
        cert_status = CertificateStatuses.downloadable
797 798
        title = _('Your certificate is available')
        msg = _('You can keep working for a higher grade, or request your certificate now.')
799
        if certs_api.has_html_certificates_enabled(course_key, course):
800
            if certs_api.get_active_web_certificate(course) is not None:
801 802 803 804
                cert_web_view_url = certs_api.get_certificate_url(
                    course_id=course_key, uuid=cert_downloadable_status['uuid']
                )
                return CertData(cert_status, title, msg, download_url=None, cert_web_view_url=cert_web_view_url)
805
            else:
806 807
                return CertData(
                    CertificateStatuses.generating,
808 809 810 811 812
                    _("We're working on it..."),
                    _(
                        "We're creating your certificate. You can keep working in your courses and a link "
                        "to it will appear here and on your Dashboard when it is ready."
                    ),
813 814 815 816 817 818 819
                    download_url=None,
                    cert_web_view_url=None
                )

        return CertData(
            cert_status, title, msg, download_url=cert_downloadable_status['download_url'], cert_web_view_url=None
        )
820

821 822 823
    if cert_downloadable_status['is_generating']:
        return CertData(
            CertificateStatuses.generating,
824 825 826 827 828
            _("We're working on it..."),
            _(
                "We're creating your certificate. You can keep working in your courses and a link to "
                "it will appear here and on your Dashboard when it is ready."
            ),
829 830 831
            download_url=None,
            cert_web_view_url=None
        )
832

833 834 835 836 837 838
    # If the learner is in verified modes and the student did not have
    # their ID verified, we need to show message to ask learner to verify their ID first
    missing_required_verification = enrollment_mode in CourseMode.VERIFIED_MODES and \
        not SoftwareSecurePhotoVerification.user_is_verified(student)

    if missing_required_verification or cert_downloadable_status['is_unverified']:
839
        platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
840 841
        return CertData(
            CertificateStatuses.unverified,
842 843 844 845 846
            _('Certificate unavailable'),
            _(
                'You have not received a certificate because you do not have a current {platform_name} '
                'verified identity.'
            ).format(platform_name=platform_name),
847 848 849 850 851 852
            download_url=None,
            cert_web_view_url=None
        )

    return CertData(
        CertificateStatuses.requesting,
853 854
        _('Congratulations, you qualified for a certificate!'),
        _('You can keep working for a higher grade, or request your certificate now.'),
855 856 857
        download_url=None,
        cert_web_view_url=None
    )
858 859


Will Daly committed
860 861 862 863 864 865 866
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.

867 868
    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
869 870 871 872 873 874 875 876

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

877
    # This indicates that credit requirements should NOT be displayed on the progress page.
878
    enrollment = CourseEnrollment.get_enrollment(student, course_key)
879
    if enrollment and enrollment.mode not in REQUIREMENTS_DISPLAY_MODES:
880 881
        return None

882 883 884
    # Credit requirement statuses for which user does not remain eligible to get credit.
    non_eligible_statuses = ['failed', 'declined']

Will Daly committed
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907
    # 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.
908
    elif any(requirement['status'] in non_eligible_statuses for requirement in requirement_statuses):
Will Daly committed
909 910 911 912 913 914 915 916 917
        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,
918
        'requirements': requirement_statuses,
Will Daly committed
919 920 921
    }


922
@login_required
923
@ensure_valid_course_key
924
def submission_history(request, course_id, student_username, location):
925
    """Render an HTML fragment (meant for inclusion elsewhere) that renders a
926
    history of all state changes made by this user for this problem location.
927
    Right now this only works for problems because that's all
928 929
    StudentModuleHistory records.
    """
930 931

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
932 933

    try:
934
        usage_key = course_key.make_usage_key_from_deprecated_string(location)
935 936
    except (InvalidKeyError, AssertionError):
        return HttpResponse(escape(_(u'Invalid location.')))
937

938
    course = get_course_overview_with_access(request.user, 'load', course_key)
939
    staff_access = bool(has_access(request.user, 'staff', course))
940

941 942
    # Permission Denied if they don't have staff access and are trying to see
    # somebody else's submission history.
943 944 945
    if (student_username != request.user.username) and (not staff_access):
        raise PermissionDenied

946
    user_state_client = DjangoXBlockUserStateClient()
947
    try:
948
        history_entries = list(user_state_client.get_history(student_username, usage_key))
949
    except DjangoXBlockUserStateClient.DoesNotExist:
950 951 952 953
        return HttpResponse(escape(_(u'User {username} has never accessed problem {location}').format(
            username=student_username,
            location=location
        )))
954

955 956
    # 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.
957 958 959 960 961 962
    csm = StudentModule.objects.filter(
        module_state_key=usage_key,
        student__username=student_username,
        course_id=course_key)

    scores = BaseStudentModuleHistory.get_history(csm)
963 964 965 966 967

    if len(scores) != len(history_entries):
        log.warning(
            "Mismatch when fetching scores for student "
            "history for course %s, user %s, xblock %s. "
968 969
            "%d scores were found, and %d history entries were found. "
            "Matching scores to history entries by date for display.",
970 971
            course_id,
            student_username,
972 973 974
            location,
            len(scores),
            len(history_entries),
975 976
        )
        scores_by_date = {
977
            score.created: score
978 979 980 981 982 983 984
            for score in scores
        }
        scores = [
            scores_by_date[history.updated]
            for history in history_entries
        ]

985 986
    context = {
        'history_entries': history_entries,
987
        'scores': scores,
988
        'username': student_username,
989
        'location': location,
990
        'course_id': course_key.to_deprecated_string()
991 992 993
    }

    return render_to_response('courseware/submission_history.html', context)
994 995 996 997 998 999


def get_static_tab_contents(request, course, tab):
    """
    Returns the contents for the given static tab
    """
1000
    loc = course.id.make_usage_key(
1001 1002 1003 1004
        tab.type,
        tab.url_slug,
    )
    field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
1005
        course.id, request.user, modulestore().get_item(loc), depth=0
1006 1007
    )
    tab_module = get_module(
1008
        request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path, course=course
1009 1010
    )

1011
    logging.debug('course_module = %s', tab_module)
1012 1013 1014 1015

    html = ''
    if tab_module is not None:
        try:
1016
            html = tab_module.render(STUDENT_VIEW).content
1017 1018
        except Exception:  # pylint: disable=broad-except
            html = render_to_string('courseware/error-message.html', None)
1019
            log.exception(
1020
                u"Error rendering course=%s, tab=%s", course, tab['url_slug']
1021
            )
1022 1023

    return html
1024 1025 1026


@require_GET
1027
@ensure_valid_course_key
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043
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.
    """
1044 1045

    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
1046 1047 1048 1049

    try:
        course = get_course(course_key, depth=2)
    except ValueError:
1050 1051 1052 1053
        return HttpResponse(status=404)

    anonymous_user = AnonymousUser()
    anonymous_user.known = False  # make these "noauth" requests like module_render.handle_xblock_callback_noauth
1054
    lti_descriptors = modulestore().get_items(course.id, qualifiers={'category': 'lti'})
1055 1056 1057 1058 1059 1060 1061

    lti_noauth_modules = [
        get_module_for_descriptor(
            anonymous_user,
            request,
            descriptor,
            FieldDataCache.cache_for_descriptor_descendents(
1062
                course_key,
1063 1064 1065
                anonymous_user,
                descriptor
            ),
1066 1067
            course_key,
            course=course
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083
        )
        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')
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110


@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,
    )
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129


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:
1130
        grade_summary = CourseGradeFactory(student).create(course).summary
1131

1132
    return success_cutoff and grade_summary['percent'] >= success_cutoff
1133 1134


1135 1136
# Grades can potentially be written - if so, let grading manage the transaction.
@transaction.non_atomic_requests
1137 1138
@require_POST
def generate_user_cert(request, course_id):
1139
    """Start generating a new certificate for the user.
1140

1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153
    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.
1154 1155

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

1158 1159 1160 1161 1162 1163
    """

    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(
1164
                platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177
            )
        )

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

1178
    certificate_status = certs_api.certificate_downloadable_status(student, course.id)
1179

1180 1181 1182
    if certificate_status["is_downloadable"]:
        return HttpResponseBadRequest(_("Certificate has already been created."))
    elif certificate_status["is_generating"]:
1183
        return HttpResponseBadRequest(_("Certificate is being created."))
1184 1185 1186 1187 1188 1189 1190
    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.
1191
        certs_api.generate_user_certificates(student, course.id, course=course, generation_mode='self')
1192
        _track_successful_certificate_generation(student.id, course.id)
1193
        return HttpResponse()
1194 1195 1196


def _track_successful_certificate_generation(user_id, course_id):  # pylint: disable=invalid-name
1197 1198
    """
    Track a successful certificate generation event.
1199 1200 1201

    Arguments:
        user_id (str): The ID of the user generting the certificate.
1202
        course_id (CourseKey): Identifier for the course.
1203 1204 1205 1206
    Returns:
        None

    """
1207
    if settings.LMS_SEGMENT_KEY:
1208 1209
        event_name = 'edx.bi.user.certificate.generate'
        tracking_context = tracker.get_tracker().resolve_context()
1210 1211 1212 1213 1214 1215 1216 1217 1218

        analytics.track(
            user_id,
            event_name,
            {
                'category': 'certificates',
                'label': unicode(course_id)
            },
            context={
1219
                'ip': tracking_context.get('ip'),
1220 1221 1222 1223 1224
                'Google Analytics': {
                    'clientId': tracking_context.get('client_id')
                }
            }
        )
1225 1226 1227


@require_http_methods(["GET", "POST"])
1228
def render_xblock(request, usage_key_string, check_if_enrolled=True):
1229 1230 1231 1232 1233 1234 1235 1236
    """
    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

1237 1238 1239 1240
    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))

1241 1242
    with modulestore().bulk_operations(course_key):
        # verify the user has access to the course, including enrollment check
1243 1244 1245 1246
        try:
            course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=check_if_enrolled)
        except UserNotEnrolled:
            raise Http404("Course not found.")
1247 1248 1249

        # get the block, which verifies whether the user has access to the block.
        block, _ = get_module_by_usage_id(
1250
            request, unicode(course_key), unicode(usage_key), disable_staff_debug_info=True, course=course
1251 1252 1253 1254 1255 1256 1257 1258
        )

        context = {
            'fragment': block.render('student_view', context=request.GET),
            'course': course,
            'disable_accordion': True,
            'allow_iframing': True,
            'disable_header': True,
1259
            'disable_footer': True,
1260 1261
            'disable_window_wrap': True,
            'disable_preview_menu': True,
1262
            'staff_access': bool(has_access(request.user, 'staff', course)),
1263 1264 1265
            'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
        }
        return render_to_response('courseware/courseware-chromeless.html', context)
1266 1267 1268 1269 1270


# 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
1271
    '{platform_name} now offers financial assistance for learners who want to earn Verified Certificates but'
1272
    ' who may not be able to pay the Verified Certificate fee. Eligible learners may receive up to 90{percent_sign} off'
1273 1274
    ' 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.'
1275
    ' Note that you must complete a separate application for each course you take.\n We plan to use this'
1276 1277
    ' information to evaluate your application for financial assistance and to further develop our'
    ' financial assistance program.'
1278 1279
).format(
    percent_sign="%",
1280
    platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
1281 1282 1283
).split('\n')


Bill DeRusha committed
1284
FA_INCOME_LABEL = _('Annual Household Income')
1285
FA_REASON_FOR_APPLYING_LABEL = _(
1286
    'Tell us about your current financial situation. Why do you need assistance?'
1287 1288 1289 1290 1291 1292 1293
)
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
1294
    ' the course work and receive a certificate?'
1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318
)
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()

1319
        course_id = data['course']
Bill DeRusha committed
1320
        course = modulestore().get_course(CourseKey.from_string(course_id))
1321
        legal_name = data['name']
1322 1323 1324 1325 1326 1327
        email = data['email']
        country = data['country']
        income = data['income']
        reason_for_applying = data['reason_for_applying']
        goals = data['goals']
        effort = data['effort']
1328
        marketing_permission = data['mktg-permission']
1329 1330 1331
        ip_address = get_ip(request)
    except ValueError:
        # Thrown if JSON parsing fails
1332
        return HttpResponseBadRequest(u'Could not parse request JSON.')
Bill DeRusha committed
1333 1334
    except InvalidKeyError:
        # Thrown if course key parsing fails
1335
        return HttpResponseBadRequest(u'Could not parse request course key.')
1336 1337
    except KeyError as err:
        # Thrown if fields are missing
1338
        return HttpResponseBadRequest(u'The field {} is required.'.format(err.message))
1339 1340 1341 1342

    zendesk_submitted = _record_feedback_in_zendesk(
        legal_name,
        email,
1343
        u'Financial assistance request for learner {username} in course {course_name}'.format(
1344
            username=username,
Bill DeRusha committed
1345
            course_name=course.display_name
1346
        ),
1347
        u'Financial Assistance Request',
Bill DeRusha committed
1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366
        {'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
1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383
    )

    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')
vkaracic committed
1384 1385

        if enrollment.mode != CourseMode.VERIFIED and CourseMode.objects.filter(
1386
            Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC())),
1387 1388 1389 1390
            course_id=enrollment.course_id,
            mode_slug=CourseMode.VERIFIED
        ).exists()
    ]
1391 1392 1393 1394
    incomes = ['Less than $5,000', '$5,000 - $10,000', '$10,000 - $15,000', '$15,000 - $20,000', '$20,000 - $25,000']
    annual_incomes = [
        {'name': _(income), 'value': income} for income in incomes  # pylint: disable=translation-of-non-string
    ]
1395 1396 1397 1398
    return render_to_response('financial-assistance/apply.html', {
        'header_text': FINANCIAL_ASSISTANCE_HEADER,
        'student_faq_url': marketing_link('FAQ'),
        'dashboard_url': reverse('dashboard'),
1399
        'account_settings_url': reverse('account_settings'),
1400
        'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424
        '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',
1425
                'type': 'select',
1426
                'label': FA_INCOME_LABEL,
1427
                'placeholder': '',
1428 1429
                'defaultValue': '',
                'required': True,
1430
                'options': annual_incomes,
1431
                'instructions': _('Specify your annual household income in US Dollars.')
1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475
            },
            {
                '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': _(
1476 1477
                    'I allow edX to use the information provided in this application '
                    '(except for financial information) for edX marketing purposes.'
1478 1479 1480 1481
                ),
                'defaultValue': '',
                'type': 'checkbox',
                'required': False,
1482
                'instructions': '',
1483 1484 1485 1486
                'restrictions': {}
            }
        ],
    })