Advertisement
Guest User

Untitled

a guest
Jul 28th, 2015
475
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 104.85 KB | None | 0 0
  1. """
  2. Instructor Dashboard API views
  3.  
  4. JSON views which the instructor dashboard requests.
  5.  
  6. Many of these GETs may become PUTs in the future.
  7. """
  8. import StringIO
  9. import json
  10. import logging
  11. import re
  12. import time
  13. import requests
  14. from django.conf import settings
  15. from django.views.decorators.csrf import ensure_csrf_cookie
  16. from django.views.decorators.http import require_POST
  17. from django.views.decorators.cache import cache_control
  18. from django.core.exceptions import ValidationError, PermissionDenied
  19. from django.core.mail.message import EmailMessage
  20. from django.db import IntegrityError
  21. from django.core.urlresolvers import reverse
  22. from django.core.validators import validate_email
  23. from django.utils.translation import ugettext as _
  24. from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound
  25. from django.utils.html import strip_tags
  26. from django.shortcuts import redirect
  27. import string  # pylint: disable=deprecated-module
  28. import random
  29. import unicodecsv
  30. import urllib
  31. import decimal
  32. from student import auth
  33. from student.roles import GlobalStaff, CourseSalesAdminRole, CourseFinanceAdminRole
  34. from util.file import (
  35.     store_uploaded_file, course_and_time_based_filename_generator,
  36.     FileValidationException, UniversalNewlineIterator
  37. )
  38. from util.json_request import JsonResponse
  39. from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
  40.  
  41. from microsite_configuration import microsite
  42.  
  43. from courseware.access import has_access
  44. from courseware.courses import get_course_with_access, get_course_by_id
  45. from django.contrib.auth.models import User
  46. from django_comment_client.utils import has_forum_access
  47. from django_comment_common.models import (
  48.     Role,
  49.     FORUM_ROLE_ADMINISTRATOR,
  50.     FORUM_ROLE_MODERATOR,
  51.     FORUM_ROLE_COMMUNITY_TA,
  52. )
  53. from edxmako.shortcuts import render_to_response, render_to_string
  54. from courseware.models import StudentModule
  55. from shoppingcart.models import (
  56.     Coupon,
  57.     CourseRegistrationCode,
  58.     RegistrationCodeRedemption,
  59.     Invoice,
  60.     CourseMode,
  61.     CourseRegistrationCodeInvoiceItem,
  62. )
  63. from student.models import (
  64.     CourseEnrollment, unique_id_for_user, anonymous_id_for_user,
  65.     UserProfile, Registration, EntranceExamConfiguration,
  66.     ManualEnrollmentAudit, UNENROLLED_TO_ALLOWEDTOENROLL, ALLOWEDTOENROLL_TO_ENROLLED,
  67.     ENROLLED_TO_ENROLLED, ENROLLED_TO_UNENROLLED, UNENROLLED_TO_ENROLLED,
  68.     UNENROLLED_TO_UNENROLLED, ALLOWEDTOENROLL_TO_UNENROLLED, DEFAULT_TRANSITION_STATE
  69. )
  70. import instructor_task.api
  71. from instructor_task.api_helper import AlreadyRunningError
  72. from instructor_task.models import ReportStore
  73. import instructor.enrollment as enrollment
  74. from instructor.enrollment import (
  75.     get_user_email_language,
  76.     enroll_email,
  77.     send_mail_to_student,
  78.     get_email_params,
  79.     send_beta_role_email,
  80.     unenroll_email,
  81. )
  82. from instructor.access import list_with_level, allow_access, revoke_access, ROLES, update_forum_role
  83. from instructor.offline_gradecalc import student_grades
  84. import instructor_analytics.basic
  85. import instructor_analytics.distributions
  86. import instructor_analytics.csvs
  87. import csv
  88. from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
  89. from instructor.views import INVOICE_KEY
  90.  
  91. from submissions import api as sub_api  # installed from the edx-submissions repository
  92.  
  93. from certificates import api as certs_api
  94.  
  95. from bulk_email.models import CourseEmail
  96.  
  97. from .tools import (
  98.     dump_student_extensions,
  99.     dump_module_extensions,
  100.     find_unit,
  101.     get_student_from_identifier,
  102.     require_student_from_identifier,
  103.     handle_dashboard_error,
  104.     parse_datetime,
  105.     set_due_date_extension,
  106.     strip_if_string,
  107.     bulk_email_is_enabled_for_course,
  108.     add_block_ids,
  109. )
  110. from opaque_keys.edx.keys import CourseKey
  111. from opaque_keys.edx.locations import SlashSeparatedCourseKey
  112. from opaque_keys import InvalidKeyError
  113. from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted
  114.  
  115. log = logging.getLogger(__name__)
  116.  
  117.  
  118. def common_exceptions_400(func):
  119.     """
  120.    Catches common exceptions and renders matching 400 errors.
  121.    (decorator without arguments)
  122.    """
  123.     def wrapped(request, *args, **kwargs):  # pylint: disable=missing-docstring
  124.         use_json = (request.is_ajax() or
  125.                     request.META.get("HTTP_ACCEPT", "").startswith("application/json"))
  126.         try:
  127.             return func(request, *args, **kwargs)
  128.         except User.DoesNotExist:
  129.             message = _("User does not exist.")
  130.             if use_json:
  131.                 return JsonResponse({"error": message}, 400)
  132.             else:
  133.                 return HttpResponseBadRequest(message)
  134.         except AlreadyRunningError:
  135.             message = _("Task is already running.")
  136.             if use_json:
  137.                 return JsonResponse({"error": message}, 400)
  138.             else:
  139.                 return HttpResponseBadRequest(message)
  140.     return wrapped
  141.  
  142.  
  143. def require_query_params(*args, **kwargs):
  144.     """
  145.    Checks for required paremters or renders a 400 error.
  146.    (decorator with arguments)
  147.  
  148.    `args` is a *list of required GET parameter names.
  149.    `kwargs` is a **dict of required GET parameter names
  150.        to string explanations of the parameter
  151.    """
  152.     required_params = []
  153.     required_params += [(arg, None) for arg in args]
  154.     required_params += [(key, kwargs[key]) for key in kwargs]
  155.     # required_params = e.g. [('action', 'enroll or unenroll'), ['emails', None]]
  156.  
  157.     def decorator(func):  # pylint: disable=missing-docstring
  158.         def wrapped(*args, **kwargs):  # pylint: disable=missing-docstring
  159.             request = args[0]
  160.  
  161.             error_response_data = {
  162.                 'error': 'Missing required query parameter(s)',
  163.                 'parameters': [],
  164.                 'info': {},
  165.             }
  166.  
  167.             for (param, extra) in required_params:
  168.                 default = object()
  169.                 if request.GET.get(param, default) == default:
  170.                     error_response_data['parameters'].append(param)
  171.                     error_response_data['info'][param] = extra
  172.  
  173.             if len(error_response_data['parameters']) > 0:
  174.                 return JsonResponse(error_response_data, status=400)
  175.             else:
  176.                 return func(*args, **kwargs)
  177.         return wrapped
  178.     return decorator
  179.  
  180.  
  181. def require_post_params(*args, **kwargs):
  182.     """
  183.    Checks for required parameters or renders a 400 error.
  184.    (decorator with arguments)
  185.  
  186.    Functions like 'require_query_params', but checks for
  187.    POST parameters rather than GET parameters.
  188.    """
  189.     required_params = []
  190.     required_params += [(arg, None) for arg in args]
  191.     required_params += [(key, kwargs[key]) for key in kwargs]
  192.     # required_params = e.g. [('action', 'enroll or unenroll'), ['emails', None]]
  193.  
  194.     def decorator(func):  # pylint: disable=missing-docstring
  195.         def wrapped(*args, **kwargs):  # pylint: disable=missing-docstring
  196.             request = args[0]
  197.  
  198.             error_response_data = {
  199.                 'error': 'Missing required query parameter(s)',
  200.                 'parameters': [],
  201.                 'info': {},
  202.             }
  203.  
  204.             for (param, extra) in required_params:
  205.                 default = object()
  206.                 if request.POST.get(param, default) == default:
  207.                     error_response_data['parameters'].append(param)
  208.                     error_response_data['info'][param] = extra
  209.  
  210.             if len(error_response_data['parameters']) > 0:
  211.                 return JsonResponse(error_response_data, status=400)
  212.             else:
  213.                 return func(*args, **kwargs)
  214.         return wrapped
  215.     return decorator
  216.  
  217.  
  218. def require_level(level):
  219.     """
  220.    Decorator with argument that requires an access level of the requesting
  221.    user. If the requirement is not satisfied, returns an
  222.    HttpResponseForbidden (403).
  223.  
  224.    Assumes that request is in args[0].
  225.    Assumes that course_id is in kwargs['course_id'].
  226.  
  227.    `level` is in ['instructor', 'staff']
  228.    if `level` is 'staff', instructors will also be allowed, even
  229.        if they are not in the staff group.
  230.    """
  231.     if level not in ['instructor', 'staff']:
  232.         raise ValueError("unrecognized level '{}'".format(level))
  233.  
  234.     def decorator(func):  # pylint: disable=missing-docstring
  235.         def wrapped(*args, **kwargs):  # pylint: disable=missing-docstring
  236.             request = args[0]
  237.             course = get_course_by_id(CourseKey.from_string(kwargs['course_id']))
  238.  
  239.             if has_access(request.user, level, course):
  240.                 return func(*args, **kwargs)
  241.             else:
  242.                 return HttpResponseForbidden()
  243.         return wrapped
  244.     return decorator
  245.  
  246.  
  247. def require_global_staff(func):
  248.     """View decorator that requires that the user have global staff permissions. """
  249.     def wrapped(request, *args, **kwargs):  # pylint: disable=missing-docstring
  250.         if GlobalStaff().has_user(request.user):
  251.             return func(request, *args, **kwargs)
  252.         else:
  253.             return HttpResponseForbidden(
  254.                 u"Must be {platform_name} staff to perform this action.".format(
  255.                     platform_name=settings.PLATFORM_NAME
  256.                 )
  257.             )
  258.     return wrapped
  259.  
  260.  
  261. def require_sales_admin(func):
  262.     """
  263.    Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator
  264.    is designed to be used for a request based action on a course. It assumes that there will be a
  265.    request object as well as a course_id attribute to leverage to check course level privileges.
  266.  
  267.    If the user does not have privileges for this operation, this will return HttpResponseForbidden (403).
  268.    """
  269.     def wrapped(request, course_id):  # pylint: disable=missing-docstring
  270.  
  271.         try:
  272.             course_key = CourseKey.from_string(course_id)
  273.         except InvalidKeyError:
  274.             log.error(u"Unable to find course with course key %s", course_id)
  275.             return HttpResponseNotFound()
  276.  
  277.         access = auth.has_access(request.user, CourseSalesAdminRole(course_key))
  278.  
  279.         if access:
  280.             return func(request, course_id)
  281.         else:
  282.             return HttpResponseForbidden()
  283.     return wrapped
  284.  
  285.  
  286. def require_finance_admin(func):
  287.     """
  288.    Decorator for checking finance administrator access before executing an HTTP endpoint. This decorator
  289.    is designed to be used for a request based action on a course. It assumes that there will be a
  290.    request object as well as a course_id attribute to leverage to check course level privileges.
  291.  
  292.    If the user does not have privileges for this operation, this will return HttpResponseForbidden (403).
  293.    """
  294.     def wrapped(request, course_id):  # pylint: disable=missing-docstring
  295.  
  296.         try:
  297.             course_key = CourseKey.from_string(course_id)
  298.         except InvalidKeyError:
  299.             log.error(u"Unable to find course with course key %s", course_id)
  300.             return HttpResponseNotFound()
  301.  
  302.         access = auth.has_access(request.user, CourseFinanceAdminRole(course_key))
  303.  
  304.         if access:
  305.             return func(request, course_id)
  306.         else:
  307.             return HttpResponseForbidden()
  308.     return wrapped
  309.  
  310.  
  311. EMAIL_INDEX = 0
  312. USERNAME_INDEX = 1
  313. NAME_INDEX = 2
  314. COUNTRY_INDEX = 3
  315.  
  316.  
  317. @ensure_csrf_cookie
  318. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  319. @require_level('staff')
  320. def register_and_enroll_students(request, course_id):  # pylint: disable=too-many-statements
  321.     """
  322.    Create new account and Enroll students in this course.
  323.    Passing a csv file that contains a list of students.
  324.    Order in csv should be the following email = 0; username = 1; name = 2; country = 3.
  325.    Requires staff access.
  326.  
  327.    -If the email address and username already exists and the user is enrolled in the course,
  328.    do nothing (including no email gets sent out)
  329.  
  330.    -If the email address already exists, but the username is different,
  331.    match on the email address only and continue to enroll the user in the course using the email address
  332.    as the matching criteria. Note the change of username as a warning message (but not a failure). Send a standard enrollment email
  333.    which is the same as the existing manual enrollment
  334.  
  335.    -If the username already exists (but not the email), assume it is a different user and fail to create the new account.
  336.     The failure will be messaged in a response in the browser.
  337.    """
  338.  
  339.     if not microsite.get_value('ALLOW_AUTOMATED_SIGNUPS', settings.FEATURES.get('ALLOW_AUTOMATED_SIGNUPS', False)):
  340.         return HttpResponseForbidden()
  341.  
  342.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  343.     warnings = []
  344.     row_errors = []
  345.     general_errors = []
  346.  
  347.     if 'students_list' in request.FILES:
  348.         students = []
  349.  
  350.         try:
  351.             upload_file = request.FILES.get('students_list')
  352.             if upload_file.name.endswith('.csv'):
  353.                 students = [row for row in csv.reader(upload_file.read().splitlines())]
  354.                 course = get_course_by_id(course_id)
  355.             else:
  356.                 general_errors.append({
  357.                     'username': '', 'email': '',
  358.                     'response': _('Make sure that the file you upload is in CSV format with no extraneous characters or rows.')
  359.                 })
  360.  
  361.         except Exception:  # pylint: disable=broad-except
  362.             general_errors.append({
  363.                 'username': '', 'email': '', 'response': _('Could not read uploaded file.')
  364.             })
  365.         finally:
  366.             upload_file.close()
  367.  
  368.         generated_passwords = []
  369.         row_num = 0
  370.         for student in students:
  371.             row_num = row_num + 1
  372.  
  373.             # verify that we have exactly four columns in every row but allow for blank lines
  374.             if len(student) != 4:
  375.                 if len(student) > 0:
  376.                     general_errors.append({
  377.                         'username': '',
  378.                         'email': '',
  379.                         'response': _('Data in row #{row_num} must have exactly four columns: email, username, full name, and country').format(row_num=row_num)
  380.                     })
  381.                 continue
  382.  
  383.             # Iterate each student in the uploaded csv file.
  384.             email = student[EMAIL_INDEX]
  385.             username = student[USERNAME_INDEX]
  386.             name = student[NAME_INDEX]
  387.             country = student[COUNTRY_INDEX][:2]
  388.  
  389.             email_params = get_email_params(course, True, secure=request.is_secure())
  390.             try:
  391.                 validate_email(email)  # Raises ValidationError if invalid
  392.             except ValidationError:
  393.                 row_errors.append({
  394.                     'username': username, 'email': email, 'response': _('Invalid email {email_address}.').format(email_address=email)})
  395.             else:
  396.                 if User.objects.filter(email=email).exists():
  397.                     # Email address already exists. assume it is the correct user
  398.                     # and just register the user in the course and send an enrollment email.
  399.                     user = User.objects.get(email=email)
  400.  
  401.                     # see if it is an exact match with email and username
  402.                     # if it's not an exact match then just display a warning message, but continue onwards
  403.                     if not User.objects.filter(email=email, username=username).exists():
  404.                         warning_message = _(
  405.                             'An account with email {email} exists but the provided username {username} '
  406.                             'is different. Enrolling anyway with {email}.'
  407.                         ).format(email=email, username=username)
  408.  
  409.                         warnings.append({
  410.                             'username': username, 'email': email, 'response': warning_message
  411.                         })
  412.                         log.warning(u'email %s already exist', email)
  413.                     else:
  414.                         log.info(
  415.                             u"user already exists with username '%s' and email '%s'",
  416.                             username,
  417.                             email
  418.                         )
  419.  
  420.                     # make sure user is enrolled in course
  421.                     if not CourseEnrollment.is_enrolled(user, course_id):
  422.                         enrollment_obj = CourseEnrollment.enroll(user, course_id)
  423.                         reason = 'Enrolling via csv upload'
  424.                         ManualEnrollmentAudit.create_manual_enrollment_audit(
  425.                             request.user, email, UNENROLLED_TO_ENROLLED, reason, enrollment_obj
  426.                         )
  427.                         log.info(
  428.                             u'user %s enrolled in the course %s',
  429.                             username,
  430.                             course.id,
  431.                         )
  432.                         enroll_email(course_id=course_id, student_email=email, auto_enroll=True, email_students=True, email_params=email_params)
  433.                 else:
  434.                     # This email does not yet exist, so we need to create a new account
  435.                     # If username already exists in the database, then create_and_enroll_user
  436.                     # will raise an IntegrityError exception.
  437.                     password = generate_unique_password(generated_passwords)
  438.  
  439.                     try:
  440.                         enrollment_obj = create_and_enroll_user(email, username, name, country, password, course_id)
  441.                         reason = 'Enrolling via csv upload'
  442.                         ManualEnrollmentAudit.create_manual_enrollment_audit(
  443.                             request.user, email, UNENROLLED_TO_ENROLLED, reason, enrollment_obj
  444.                         )
  445.                     except IntegrityError:
  446.                         row_errors.append({
  447.                             'username': username, 'email': email, 'response': _('Username {user} already exists.').format(user=username)})
  448.                     except Exception as ex:
  449.                         log.exception(type(ex).__name__)
  450.                         row_errors.append({
  451.                             'username': username, 'email': email, 'response': type(ex).__name__})
  452.                     else:
  453.                         # It's a new user, an email will be sent to each newly created user.
  454.                         email_params['message'] = 'account_creation_and_enrollment'
  455.                         email_params['email_address'] = email
  456.                         email_params['password'] = password
  457.                         email_params['platform_name'] = microsite.get_value('platform_name', settings.PLATFORM_NAME)
  458.                         send_mail_to_student(email, email_params)
  459.                         log.info(u'email sent to new created user at %s', email)
  460.  
  461.     else:
  462.         general_errors.append({
  463.             'username': '', 'email': '', 'response': _('File is not attached.')
  464.         })
  465.  
  466.     results = {
  467.         'row_errors': row_errors,
  468.         'general_errors': general_errors,
  469.         'warnings': warnings
  470.     }
  471.     return JsonResponse(results)
  472.  
  473.  
  474. def generate_random_string(length):
  475.     """
  476.    Create a string of random characters of specified length
  477.    """
  478.     chars = [
  479.         char for char in string.ascii_uppercase + string.digits + string.ascii_lowercase
  480.         if char not in 'aAeEiIoOuU1l'
  481.     ]
  482.  
  483.     return string.join((random.choice(chars) for __ in range(length)), '')
  484.  
  485.  
  486. def generate_unique_password(generated_passwords, password_length=12):
  487.     """
  488.    generate a unique password for each student.
  489.    """
  490.  
  491.     password = generate_random_string(password_length)
  492.     while password in generated_passwords:
  493.         password = generate_random_string(password_length)
  494.  
  495.     generated_passwords.append(password)
  496.  
  497.     return password
  498.  
  499.  
  500. def create_and_enroll_user(email, username, name, country, password, course_id):
  501.     """ Creates a user and enroll him/her in the course"""
  502.  
  503.     user = User.objects.create_user(username, email, password)
  504.     reg = Registration()
  505.     reg.register(user)
  506.  
  507.     profile = UserProfile(user=user)
  508.     profile.name = name
  509.     profile.country = country
  510.     profile.save()
  511.  
  512.     # try to enroll the user in this course
  513.     return CourseEnrollment.enroll(user, course_id)
  514.  
  515.  
  516. @ensure_csrf_cookie
  517. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  518. @require_level('staff')
  519. @require_post_params(action="enroll or unenroll", identifiers="stringified list of emails and/or usernames")
  520. def students_update_enrollment(request, course_id):
  521.     """
  522.    Enroll or unenroll students by email.
  523.    Requires staff access.
  524.  
  525.    Query Parameters:
  526.    - action in ['enroll', 'unenroll']
  527.    - identifiers is string containing a list of emails and/or usernames separated by anything split_input_list can handle.
  528.    - auto_enroll is a boolean (defaults to false)
  529.        If auto_enroll is false, students will be allowed to enroll.
  530.        If auto_enroll is true, students will be enrolled as soon as they register.
  531.    - email_students is a boolean (defaults to false)
  532.        If email_students is true, students will be sent email notification
  533.        If email_students is false, students will not be sent email notification
  534.  
  535.    Returns an analog to this JSON structure: {
  536.        "action": "enroll",
  537.        "auto_enroll": false,
  538.        "results": [
  539.            {
  540.                "email": "testemail@test.org",
  541.                "before": {
  542.                    "enrollment": false,
  543.                    "auto_enroll": false,
  544.                    "user": true,
  545.                    "allowed": false
  546.                },
  547.                "after": {
  548.                    "enrollment": true,
  549.                    "auto_enroll": false,
  550.                    "user": true,
  551.                    "allowed": false
  552.                }
  553.            }
  554.        ]
  555.    }
  556.    """
  557.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  558.     action = request.POST.get('action')
  559.     identifiers_raw = request.POST.get('identifiers')
  560.     identifiers = _split_input_list(identifiers_raw)
  561.     auto_enroll = request.POST.get('auto_enroll') in ['true', 'True', True]
  562.     email_students = request.POST.get('email_students') in ['true', 'True', True]
  563.     is_white_label = CourseMode.is_white_label(course_id)
  564.     reason = request.POST.get('reason')
  565.     if is_white_label:
  566.         if not reason:
  567.             return JsonResponse(
  568.                 {
  569.                     'action': action,
  570.                     'results': [{'error': True}],
  571.                     'auto_enroll': auto_enroll,
  572.                 }, status=400)
  573.     enrollment_obj = None
  574.     state_transition = DEFAULT_TRANSITION_STATE
  575.  
  576.     email_params = {}
  577.     if email_students:
  578.         course = get_course_by_id(course_id)
  579.         email_params = get_email_params(course, auto_enroll, secure=request.is_secure())
  580.  
  581.     results = []
  582.     for identifier in identifiers:
  583.         # First try to get a user object from the identifer
  584.         user = None
  585.         email = None
  586.         language = None
  587.         try:
  588.             user = get_student_from_identifier(identifier)
  589.         except User.DoesNotExist:
  590.             email = identifier
  591.         else:
  592.             email = user.email
  593.             language = get_user_email_language(user)
  594.  
  595.         try:
  596.             # Use django.core.validators.validate_email to check email address
  597.             # validity (obviously, cannot check if email actually /exists/,
  598.             # simply that it is plausibly valid)
  599.             validate_email(email)  # Raises ValidationError if invalid
  600.             if action == 'enroll':
  601.                 before, after, enrollment_obj = enroll_email(
  602.                     course_id, email, auto_enroll, email_students, email_params, language=language
  603.                 )
  604.                 before_enrollment = before.to_dict()['enrollment']
  605.                 before_user_registered = before.to_dict()['user']
  606.                 before_allowed = before.to_dict()['allowed']
  607.                 after_enrollment = after.to_dict()['enrollment']
  608.                 after_allowed = after.to_dict()['allowed']
  609.  
  610.                 if before_user_registered:
  611.                     if after_enrollment:
  612.                         if before_enrollment:
  613.                             state_transition = ENROLLED_TO_ENROLLED
  614.                         else:
  615.                             if before_allowed:
  616.                                 state_transition = ALLOWEDTOENROLL_TO_ENROLLED
  617.                             else:
  618.                                 state_transition = UNENROLLED_TO_ENROLLED
  619.                 else:
  620.                     if after_allowed:
  621.                         state_transition = UNENROLLED_TO_ALLOWEDTOENROLL
  622.  
  623.             elif action == 'unenroll':
  624.                 before, after = unenroll_email(
  625.                     course_id, email, email_students, email_params, language=language
  626.                 )
  627.                 before_enrollment = before.to_dict()['enrollment']
  628.                 before_allowed = before.to_dict()['allowed']
  629.  
  630.                 if before_enrollment:
  631.                     state_transition = ENROLLED_TO_UNENROLLED
  632.                 else:
  633.                     if before_allowed:
  634.                         state_transition = ALLOWEDTOENROLL_TO_UNENROLLED
  635.                     else:
  636.                         state_transition = UNENROLLED_TO_UNENROLLED
  637.  
  638.             else:
  639.                 return HttpResponseBadRequest(strip_tags(
  640.                     "Unrecognized action '{}'".format(action)
  641.                 ))
  642.  
  643.         except ValidationError:
  644.             # Flag this email as an error if invalid, but continue checking
  645.             # the remaining in the list
  646.             results.append({
  647.                 'identifier': identifier,
  648.                 'invalidIdentifier': True,
  649.             })
  650.  
  651.         except Exception as exc:  # pylint: disable=broad-except
  652.             # catch and log any exceptions
  653.             # so that one error doesn't cause a 500.
  654.             log.exception(u"Error while #{}ing student")
  655.             log.exception(exc)
  656.             results.append({
  657.                 'identifier': identifier,
  658.                 'error': True,
  659.             })
  660.  
  661.         else:
  662.             ManualEnrollmentAudit.create_manual_enrollment_audit(
  663.                 request.user, email, state_transition, reason, enrollment_obj
  664.             )
  665.             results.append({
  666.                 'identifier': identifier,
  667.                 'before': before.to_dict(),
  668.                 'after': after.to_dict(),
  669.             })
  670.  
  671.     response_payload = {
  672.         'action': action,
  673.         'results': results,
  674.         'auto_enroll': auto_enroll,
  675.     }
  676.     return JsonResponse(response_payload)
  677.  
  678.  
  679. @ensure_csrf_cookie
  680. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  681. @require_level('instructor')
  682. @common_exceptions_400
  683. @require_post_params(
  684.     identifiers="stringified list of emails and/or usernames",
  685.     action="add or remove",
  686. )
  687. def bulk_beta_modify_access(request, course_id):
  688.     """
  689.    Enroll or unenroll users in beta testing program.
  690.  
  691.    Query parameters:
  692.    - identifiers is string containing a list of emails and/or usernames separated by
  693.      anything split_input_list can handle.
  694.    - action is one of ['add', 'remove']
  695.    """
  696.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  697.     action = request.POST.get('action')
  698.     identifiers_raw = request.POST.get('identifiers')
  699.     identifiers = _split_input_list(identifiers_raw)
  700.     email_students = request.POST.get('email_students') in ['true', 'True', True]
  701.     auto_enroll = request.POST.get('auto_enroll') in ['true', 'True', True]
  702.     results = []
  703.     rolename = 'beta'
  704.     course = get_course_by_id(course_id)
  705.  
  706.     email_params = {}
  707.     if email_students:
  708.         secure = request.is_secure()
  709.         email_params = get_email_params(course, auto_enroll=auto_enroll, secure=secure)
  710.  
  711.     for identifier in identifiers:
  712.         try:
  713.             error = False
  714.             user_does_not_exist = False
  715.             user = get_student_from_identifier(identifier)
  716.  
  717.             if action == 'add':
  718.                 allow_access(course, user, rolename)
  719.             elif action == 'remove':
  720.                 revoke_access(course, user, rolename)
  721.             else:
  722.                 return HttpResponseBadRequest(strip_tags(
  723.                     "Unrecognized action '{}'".format(action)
  724.                 ))
  725.         except User.DoesNotExist:
  726.             error = True
  727.             user_does_not_exist = True
  728.         # catch and log any unexpected exceptions
  729.         # so that one error doesn't cause a 500.
  730.         except Exception as exc:  # pylint: disable=broad-except
  731.             log.exception(u"Error while #{}ing student")
  732.             log.exception(exc)
  733.             error = True
  734.         else:
  735.             # If no exception thrown, see if we should send an email
  736.             if email_students:
  737.                 send_beta_role_email(action, user, email_params)
  738.             # See if we should autoenroll the student
  739.             if auto_enroll:
  740.                 # Check if student is already enrolled
  741.                 if not CourseEnrollment.is_enrolled(user, course_id):
  742.                     CourseEnrollment.enroll(user, course_id)
  743.  
  744.         finally:
  745.             # Tabulate the action result of this email address
  746.             results.append({
  747.                 'identifier': identifier,
  748.                 'error': error,
  749.                 'userDoesNotExist': user_does_not_exist
  750.             })
  751.  
  752.     response_payload = {
  753.         'action': action,
  754.         'results': results,
  755.     }
  756.     return JsonResponse(response_payload)
  757.  
  758.  
  759. @ensure_csrf_cookie
  760. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  761. @require_level('instructor')
  762. @common_exceptions_400
  763. @require_query_params(
  764.     unique_student_identifier="email or username of user to change access",
  765.     rolename="'instructor', 'staff', 'beta', or 'ccx_coach'",
  766.     action="'allow' or 'revoke'"
  767. )
  768. def modify_access(request, course_id):
  769.     """
  770.    Modify staff/instructor access of other user.
  771.    Requires instructor access.
  772.  
  773.    NOTE: instructors cannot remove their own instructor access.
  774.  
  775.    Query parameters:
  776.    unique_student_identifer is the target user's username or email
  777.    rolename is one of ['instructor', 'staff', 'beta', 'ccx_coach']
  778.    action is one of ['allow', 'revoke']
  779.    """
  780.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  781.     course = get_course_with_access(
  782.         request.user, 'instructor', course_id, depth=None
  783.     )
  784.     try:
  785.         user = get_student_from_identifier(request.GET.get('unique_student_identifier'))
  786.     except User.DoesNotExist:
  787.         response_payload = {
  788.             'unique_student_identifier': request.GET.get('unique_student_identifier'),
  789.             'userDoesNotExist': True,
  790.         }
  791.         return JsonResponse(response_payload)
  792.  
  793.     # Check that user is active, because add_users
  794.     # in common/djangoapps/student/roles.py fails
  795.     # silently when we try to add an inactive user.
  796.     if not user.is_active:
  797.         response_payload = {
  798.             'unique_student_identifier': user.username,
  799.             'inactiveUser': True,
  800.         }
  801.         return JsonResponse(response_payload)
  802.  
  803.     rolename = request.GET.get('rolename')
  804.     action = request.GET.get('action')
  805.  
  806.     if rolename not in ROLES:
  807.         error = strip_tags("unknown rolename '{}'".format(rolename))
  808.         log.error(error)
  809.         return HttpResponseBadRequest(error)
  810.  
  811.     # disallow instructors from removing their own instructor access.
  812.     if rolename == 'instructor' and user == request.user and action != 'allow':
  813.         response_payload = {
  814.             'unique_student_identifier': user.username,
  815.             'rolename': rolename,
  816.             'action': action,
  817.             'removingSelfAsInstructor': True,
  818.         }
  819.         return JsonResponse(response_payload)
  820.  
  821.     if action == 'allow':
  822.         allow_access(course, user, rolename)
  823.     elif action == 'revoke':
  824.         revoke_access(course, user, rolename)
  825.     else:
  826.         return HttpResponseBadRequest(strip_tags(
  827.             "unrecognized action '{}'".format(action)
  828.         ))
  829.  
  830.     response_payload = {
  831.         'unique_student_identifier': user.username,
  832.         'rolename': rolename,
  833.         'action': action,
  834.         'success': 'yes',
  835.     }
  836.     return JsonResponse(response_payload)
  837.  
  838.  
  839. @ensure_csrf_cookie
  840. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  841. @require_level('instructor')
  842. @require_query_params(rolename="'instructor', 'staff', or 'beta'")
  843. def list_course_role_members(request, course_id):
  844.     """
  845.    List instructors and staff.
  846.    Requires instructor access.
  847.  
  848.    rolename is one of ['instructor', 'staff', 'beta', 'ccx_coach']
  849.  
  850.    Returns JSON of the form {
  851.        "course_id": "some/course/id",
  852.        "staff": [
  853.            {
  854.                "username": "staff1",
  855.                "email": "staff1@example.org",
  856.                "first_name": "Joe",
  857.                "last_name": "Shmoe",
  858.            }
  859.        ]
  860.    }
  861.    """
  862.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  863.     course = get_course_with_access(
  864.         request.user, 'instructor', course_id, depth=None
  865.     )
  866.  
  867.     rolename = request.GET.get('rolename')
  868.  
  869.     if rolename not in ROLES:
  870.         return HttpResponseBadRequest()
  871.  
  872.     def extract_user_info(user):
  873.         """ convert user into dicts for json view """
  874.         return {
  875.             'username': user.username,
  876.             'email': user.email,
  877.             'first_name': user.first_name,
  878.             'last_name': user.last_name,
  879.         }
  880.  
  881.     response_payload = {
  882.         'course_id': course_id.to_deprecated_string(),
  883.         rolename: map(extract_user_info, list_with_level(
  884.             course, rolename
  885.         )),
  886.     }
  887.     return JsonResponse(response_payload)
  888.  
  889.  
  890. @ensure_csrf_cookie
  891. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  892. @require_level('staff')
  893. def get_grading_config(request, course_id):
  894.     """
  895.    Respond with json which contains a html formatted grade summary.
  896.    """
  897.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  898.     course = get_course_with_access(
  899.         request.user, 'staff', course_id, depth=None
  900.     )
  901.     grading_config_summary = instructor_analytics.basic.dump_grading_context(course)
  902.  
  903.     response_payload = {
  904.         'course_id': course_id.to_deprecated_string(),
  905.         'grading_config_summary': grading_config_summary,
  906.     }
  907.     return JsonResponse(response_payload)
  908.  
  909.  
  910. @ensure_csrf_cookie
  911. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  912. @require_level('staff')
  913. def get_sale_records(request, course_id, csv=False):  # pylint: disable=unused-argument, redefined-outer-name
  914.     """
  915.    return the summary of all sales records for a particular course
  916.    """
  917.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  918.     query_features = [
  919.         'company_name', 'company_contact_name', 'company_contact_email', 'total_codes', 'total_used_codes',
  920.         'total_amount', 'created_at', 'customer_reference_number', 'recipient_name', 'recipient_email', 'created_by',
  921.         'internal_reference', 'invoice_number', 'codes', 'course_id'
  922.     ]
  923.  
  924.     sale_data = instructor_analytics.basic.sale_record_features(course_id, query_features)
  925.  
  926.     if not csv:
  927.         for item in sale_data:
  928.             item['created_by'] = item['created_by'].username
  929.  
  930.         response_payload = {
  931.             'course_id': course_id.to_deprecated_string(),
  932.             'sale': sale_data,
  933.             'queried_features': query_features
  934.         }
  935.         return JsonResponse(response_payload)
  936.     else:
  937.         header, datarows = instructor_analytics.csvs.format_dictlist(sale_data, query_features)
  938.         return instructor_analytics.csvs.create_csv_response("e-commerce_sale_invoice_records.csv", header, datarows)
  939.  
  940.  
  941. @ensure_csrf_cookie
  942. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  943. @require_level('staff')
  944. def get_sale_order_records(request, course_id):  # pylint: disable=unused-argument, redefined-outer-name
  945.     """
  946.    return the summary of all sales records for a particular course
  947.    """
  948.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  949.     query_features = [
  950.         ('id', 'Order Id'),
  951.         ('company_name', 'Company Name'),
  952.         ('company_contact_name', 'Company Contact Name'),
  953.         ('company_contact_email', 'Company Contact Email'),
  954.         ('logged_in_username', 'Login Username'),
  955.         ('logged_in_email', 'Login User Email'),
  956.         ('purchase_time', 'Date of Sale'),
  957.         ('customer_reference_number', 'Customer Reference Number'),
  958.         ('recipient_name', 'Recipient Name'),
  959.         ('recipient_email', 'Recipient Email'),
  960.         ('bill_to_street1', 'Street 1'),
  961.         ('bill_to_street2', 'Street 2'),
  962.         ('bill_to_city', 'City'),
  963.         ('bill_to_state', 'State'),
  964.         ('bill_to_postalcode', 'Postal Code'),
  965.         ('bill_to_country', 'Country'),
  966.         ('order_type', 'Order Type'),
  967.         ('status', 'Order Item Status'),
  968.         ('coupon_code', 'Coupon Code'),
  969.         ('list_price', 'List Price'),
  970.         ('unit_cost', 'Unit Price'),
  971.         ('quantity', 'Quantity'),
  972.         ('total_discount', 'Total Discount'),
  973.         ('total_amount', 'Total Amount Paid'),
  974.     ]
  975.  
  976.     db_columns = [x[0] for x in query_features]
  977.     csv_columns = [x[1] for x in query_features]
  978.     sale_data = instructor_analytics.basic.sale_order_record_features(course_id, db_columns)
  979.     header, datarows = instructor_analytics.csvs.format_dictlist(sale_data, db_columns)  # pylint: disable=unused-variable
  980.     return instructor_analytics.csvs.create_csv_response("e-commerce_sale_order_records.csv", csv_columns, datarows)
  981.  
  982.  
  983. @require_level('staff')
  984. @require_POST
  985. def sale_validation(request, course_id):
  986.     """
  987.    This method either invalidate or re validate the sale against the invoice number depending upon the event type
  988.    """
  989.     try:
  990.         invoice_number = request.POST["invoice_number"]
  991.     except KeyError:
  992.         return HttpResponseBadRequest("Missing required invoice_number parameter")
  993.     try:
  994.         invoice_number = int(invoice_number)
  995.     except ValueError:
  996.         return HttpResponseBadRequest(
  997.             "invoice_number must be an integer, {value} provided".format(
  998.                 value=invoice_number
  999.             )
  1000.         )
  1001.     try:
  1002.         event_type = request.POST["event_type"]
  1003.     except KeyError:
  1004.         return HttpResponseBadRequest("Missing required event_type parameter")
  1005.  
  1006.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1007.     try:
  1008.         obj_invoice = CourseRegistrationCodeInvoiceItem.objects.select_related('invoice').get(
  1009.             invoice_id=invoice_number,
  1010.             course_id=course_id
  1011.         )
  1012.         obj_invoice = obj_invoice.invoice
  1013.     except CourseRegistrationCodeInvoiceItem.DoesNotExist:  # Check for old type invoices
  1014.         return HttpResponseNotFound(_("Invoice number '{num}' does not exist.").format(num=invoice_number))
  1015.  
  1016.     if event_type == "invalidate":
  1017.         return invalidate_invoice(obj_invoice)
  1018.     else:
  1019.         return re_validate_invoice(obj_invoice)
  1020.  
  1021.  
  1022. def invalidate_invoice(obj_invoice):
  1023.     """
  1024.    This method invalidate the sale against the invoice number
  1025.    """
  1026.     if not obj_invoice.is_valid:
  1027.         return HttpResponseBadRequest(_("The sale associated with this invoice has already been invalidated."))
  1028.     obj_invoice.is_valid = False
  1029.     obj_invoice.save()
  1030.     message = _('Invoice number {0} has been invalidated.').format(obj_invoice.id)
  1031.     return JsonResponse({'message': message})
  1032.  
  1033.  
  1034. def re_validate_invoice(obj_invoice):
  1035.     """
  1036.    This method re-validate the sale against the invoice number
  1037.    """
  1038.     if obj_invoice.is_valid:
  1039.         return HttpResponseBadRequest(_("This invoice is already active."))
  1040.  
  1041.     obj_invoice.is_valid = True
  1042.     obj_invoice.save()
  1043.     message = _('The registration codes for invoice {0} have been re-activated.').format(obj_invoice.id)
  1044.     return JsonResponse({'message': message})
  1045.  
  1046.  
  1047. @ensure_csrf_cookie
  1048. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1049. @require_level('staff')
  1050. def get_students_features(request, course_id, csv=False):  # pylint: disable=redefined-outer-name
  1051.     """
  1052.    Respond with json which contains a summary of all enrolled students profile information.
  1053.  
  1054.    Responds with JSON
  1055.        {"students": [{-student-info-}, ...]}
  1056.  
  1057.    TO DO accept requests for different attribute sets.
  1058.    """
  1059.     course_key = CourseKey.from_string(course_id)
  1060.     course = get_course_by_id(course_key)
  1061.  
  1062.     available_features = instructor_analytics.basic.AVAILABLE_FEATURES
  1063.  
  1064.     # Allow for microsites to be able to define additional columns (e.g. )
  1065.     query_features = microsite.get_value('student_profile_download_fields')
  1066.  
  1067.     if not query_features:
  1068.         query_features = [
  1069.             'id', 'username', 'name', 'email', 'language', 'location',
  1070.             'year_of_birth', 'gender', 'level_of_education', 'mailing_address',
  1071.             'goals'
  1072.         ]
  1073.  
  1074.     # Provide human-friendly and translatable names for these features. These names
  1075.     # will be displayed in the table generated in data_download.coffee. It is not (yet)
  1076.     # used as the header row in the CSV, but could be in the future.
  1077.     query_features_names = {
  1078.         'id': _('User ID'),
  1079.         'username': _('Username'),
  1080.         'name': _('Name'),
  1081.         'email': _('Email'),
  1082.         'language': _('Language'),
  1083.         'location': _('Location'),
  1084.         'year_of_birth': _('Birth Year'),
  1085.         'gender': _('Gender'),
  1086.         'level_of_education': _('Level of Education'),
  1087.         'mailing_address': _('Mailing Address'),
  1088.         'goals': _('Goals'),
  1089.     }
  1090.  
  1091.     if is_course_cohorted(course.id):
  1092.         # Translators: 'Cohort' refers to a group of students within a course.
  1093.         query_features.append('cohort')
  1094.         query_features_names['cohort'] = _('Cohort')
  1095.  
  1096.     if not csv:
  1097.         student_data = instructor_analytics.basic.enrolled_students_features(course_key, query_features)
  1098.         response_payload = {
  1099.             'course_id': unicode(course_key),
  1100.             'students': student_data,
  1101.             'students_count': len(student_data),
  1102.             'queried_features': query_features,
  1103.             'feature_names': query_features_names,
  1104.             'available_features': available_features,
  1105.         }
  1106.         return JsonResponse(response_payload)
  1107.     else:
  1108.         try:
  1109.             instructor_task.api.submit_calculate_students_features_csv(request, course_key, query_features)
  1110.             success_status = _(
  1111.                 "Your enrolled student profile report is being generated! "
  1112.                 "You can view the status of the generation task in the 'Pending Tasks' section.")
  1113.             return JsonResponse({"status": success_status})
  1114.         except AlreadyRunningError:
  1115.             already_running_status = _(
  1116.                 "An enrolled student profile report generation task is already in progress. "
  1117.                 "Check the 'Pending Tasks' table for the status of the task. When completed, "
  1118.                 "the report will be available for download in the table below.")
  1119.             return JsonResponse({"status": already_running_status})
  1120.  
  1121.  
  1122. @ensure_csrf_cookie
  1123. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1124. @require_level('staff')
  1125. def get_students_who_may_enroll(request, course_id):
  1126.     """
  1127.    Initiate generation of a CSV file containing information about
  1128.    students who may enroll in a course.
  1129.  
  1130.    Responds with JSON
  1131.        {"status": "... status message ..."}
  1132.  
  1133.    """
  1134.     course_key = CourseKey.from_string(course_id)
  1135.     query_features = ['email']
  1136.     try:
  1137.         instructor_task.api.submit_calculate_may_enroll_csv(request, course_key, query_features)
  1138.         success_status = _(
  1139.             "Your students who may enroll report is being generated! "
  1140.             "You can view the status of the generation task in the 'Pending Tasks' section."
  1141.         )
  1142.         return JsonResponse({"status": success_status})
  1143.     except AlreadyRunningError:
  1144.         already_running_status = _(
  1145.             "A students who may enroll report generation task is already in progress. "
  1146.             "Check the 'Pending Tasks' table for the status of the task. "
  1147.             "When completed, the report will be available for download in the table below."
  1148.         )
  1149.         return JsonResponse({"status": already_running_status})
  1150.  
  1151.  
  1152. @ensure_csrf_cookie
  1153. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1154. @require_POST
  1155. @require_level('staff')
  1156. def add_users_to_cohorts(request, course_id):
  1157.     """
  1158.    View method that accepts an uploaded file (using key "uploaded-file")
  1159.    containing cohort assignments for users. This method spawns a celery task
  1160.    to do the assignments, and a CSV file with results is provided via data downloads.
  1161.    """
  1162.     course_key = SlashSeparatedCourseKey.from_string(course_id)
  1163.  
  1164.     try:
  1165.         def validator(file_storage, file_to_validate):
  1166.             """
  1167.            Verifies that the expected columns are present.
  1168.            """
  1169.             with file_storage.open(file_to_validate) as f:
  1170.                 reader = unicodecsv.reader(UniversalNewlineIterator(f), encoding='utf-8')
  1171.                 try:
  1172.                     fieldnames = next(reader)
  1173.                 except StopIteration:
  1174.                     fieldnames = []
  1175.                 msg = None
  1176.                 if "cohort" not in fieldnames:
  1177.                     msg = _("The file must contain a 'cohort' column containing cohort names.")
  1178.                 elif "email" not in fieldnames and "username" not in fieldnames:
  1179.                     msg = _("The file must contain a 'username' column, an 'email' column, or both.")
  1180.                 if msg:
  1181.                     raise FileValidationException(msg)
  1182.  
  1183.         __, filename = store_uploaded_file(
  1184.             request, 'uploaded-file', ['.csv'],
  1185.             course_and_time_based_filename_generator(course_key, "cohorts"),
  1186.             max_file_size=2000000,  # limit to 2 MB
  1187.             validator=validator
  1188.         )
  1189.         # The task will assume the default file storage.
  1190.         instructor_task.api.submit_cohort_students(request, course_key, filename)
  1191.     except (FileValidationException, PermissionDenied) as err:
  1192.         return JsonResponse({"error": unicode(err)}, status=400)
  1193.  
  1194.     return JsonResponse()
  1195.  
  1196.  
  1197. @ensure_csrf_cookie
  1198. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1199. @require_level('staff')
  1200. def get_coupon_codes(request, course_id):  # pylint: disable=unused-argument
  1201.     """
  1202.    Respond with csv which contains a summary of all Active Coupons.
  1203.    """
  1204.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1205.     coupons = Coupon.objects.filter(course_id=course_id)
  1206.  
  1207.     query_features = [
  1208.         ('code', _('Coupon Code')),
  1209.         ('course_id', _('Course Id')),
  1210.         ('percentage_discount', _('% Discount')),
  1211.         ('description', _('Description')),
  1212.         ('expiration_date', _('Expiration Date')),
  1213.         ('is_active', _('Is Active')),
  1214.         ('code_redeemed_count', _('Code Redeemed Count')),
  1215.         ('total_discounted_seats', _('Total Discounted Seats')),
  1216.         ('total_discounted_amount', _('Total Discounted Amount')),
  1217.     ]
  1218.     db_columns = [x[0] for x in query_features]
  1219.     csv_columns = [x[1] for x in query_features]
  1220.  
  1221.     coupons_list = instructor_analytics.basic.coupon_codes_features(db_columns, coupons, course_id)
  1222.     __, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, db_columns)
  1223.     return instructor_analytics.csvs.create_csv_response('Coupons.csv', csv_columns, data_rows)
  1224.  
  1225.  
  1226. @ensure_csrf_cookie
  1227. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1228. @require_level('staff')
  1229. @require_finance_admin
  1230. def get_enrollment_report(request, course_id):
  1231.     """
  1232.    get the enrollment report for the particular course.
  1233.    """
  1234.     course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1235.     try:
  1236.         instructor_task.api.submit_detailed_enrollment_features_csv(request, course_key)
  1237.         success_status = _(
  1238.             "Your detailed enrollment report is being generated! "
  1239.             "You can view the status of the generation task in the 'Pending Tasks' section.")
  1240.         return JsonResponse({"status": success_status})
  1241.     except AlreadyRunningError:
  1242.         already_running_status = _(
  1243.             "A detailed enrollment report generation task is already in progress. "
  1244.             "Check the 'Pending Tasks' table for the status of the task. "
  1245.             "When completed, the report will be available for download in the table below.")
  1246.         return JsonResponse({
  1247.             "status": already_running_status
  1248.         })
  1249.  
  1250.  
  1251. @ensure_csrf_cookie
  1252. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1253. @require_level('staff')
  1254. @require_finance_admin
  1255. def get_exec_summary_report(request, course_id):
  1256.     """
  1257.    get the executive summary report for the particular course.
  1258.    """
  1259.     course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1260.     try:
  1261.         instructor_task.api.submit_executive_summary_report(request, course_key)
  1262.         status_response = _(
  1263.             "Your executive summary report is being created. "
  1264.             "To view the status of the report, see the 'Pending Tasks' section.")
  1265.     except AlreadyRunningError:
  1266.         status_response = _(
  1267.             "An executive summary report is currently in progress. "
  1268.             "To view the status of the report, see the 'Pending Tasks' section. "
  1269.             "When completed, the report will be available for download in the table below. "
  1270.             "You will be able to download the report when it is complete."
  1271.         )
  1272.     return JsonResponse({
  1273.         "status": status_response
  1274.     })
  1275.  
  1276.  
  1277. def save_registration_code(user, course_id, mode_slug, invoice=None, order=None, invoice_item=None):
  1278.     """
  1279.    recursive function that generate a new code every time and saves in the Course Registration Table
  1280.    if validation check passes
  1281.  
  1282.    Args:
  1283.        user (User): The user creating the course registration codes.
  1284.        course_id (str): The string representation of the course ID.
  1285.        mode_slug (str): The Course Mode Slug associated with any enrollment made by these codes.
  1286.        invoice (Invoice): (Optional) The associated invoice for this code.
  1287.        order (Order): (Optional) The associated order for this code.
  1288.        invoice_item (CourseRegistrationCodeInvoiceItem) : (Optional) The associated CourseRegistrationCodeInvoiceItem
  1289.  
  1290.    Returns:
  1291.        The newly created CourseRegistrationCode.
  1292.  
  1293.    """
  1294.     code = random_code_generator()
  1295.  
  1296.     # check if the generated code is in the Coupon Table
  1297.     matching_coupons = Coupon.objects.filter(code=code, is_active=True)
  1298.     if matching_coupons:
  1299.         return save_registration_code(
  1300.             user, course_id, mode_slug, invoice=invoice, order=order, invoice_item=invoice_item
  1301.         )
  1302.  
  1303.     course_registration = CourseRegistrationCode(
  1304.         code=code,
  1305.         course_id=unicode(course_id),
  1306.         created_by=user,
  1307.         invoice=invoice,
  1308.         order=order,
  1309.         mode_slug=mode_slug,
  1310.         invoice_item=invoice_item
  1311.     )
  1312.     try:
  1313.         course_registration.save()
  1314.         return course_registration
  1315.     except IntegrityError:
  1316.         return save_registration_code(
  1317.             user, course_id, mode_slug, invoice=invoice, order=order, invoice_item=invoice_item
  1318.         )
  1319.  
  1320.  
  1321. def registration_codes_csv(file_name, codes_list, csv_type=None):
  1322.     """
  1323.    Respond with the csv headers and data rows
  1324.    given a dict of codes list
  1325.    :param file_name:
  1326.    :param codes_list:
  1327.    :param csv_type:
  1328.    """
  1329.     # csv headers
  1330.     query_features = [
  1331.         'code', 'redeem_code_url', 'course_id', 'company_name', 'created_by',
  1332.         'redeemed_by', 'invoice_id', 'purchaser', 'customer_reference_number', 'internal_reference', 'is_valid'
  1333.     ]
  1334.  
  1335.     registration_codes = instructor_analytics.basic.course_registration_features(query_features, codes_list, csv_type)
  1336.     header, data_rows = instructor_analytics.csvs.format_dictlist(registration_codes, query_features)
  1337.     return instructor_analytics.csvs.create_csv_response(file_name, header, data_rows)
  1338.  
  1339.  
  1340. def random_code_generator():
  1341.     """
  1342.    generate a random alphanumeric code of length defined in
  1343.    REGISTRATION_CODE_LENGTH settings
  1344.    """
  1345.     code_length = getattr(settings, 'REGISTRATION_CODE_LENGTH', 8)
  1346.     return generate_random_string(code_length)
  1347.  
  1348.  
  1349. @ensure_csrf_cookie
  1350. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1351. @require_level('staff')
  1352. @require_POST
  1353. def get_registration_codes(request, course_id):  # pylint: disable=unused-argument
  1354.     """
  1355.    Respond with csv which contains a summary of all Registration Codes.
  1356.    """
  1357.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1358.  
  1359.     #filter all the  course registration codes
  1360.     registration_codes = CourseRegistrationCode.objects.filter(
  1361.         course_id=course_id
  1362.     ).order_by('invoice_item__invoice__company_name')
  1363.  
  1364.     company_name = request.POST['download_company_name']
  1365.     if company_name:
  1366.         registration_codes = registration_codes.filter(invoice_item__invoice__company_name=company_name)
  1367.  
  1368.     csv_type = 'download'
  1369.     return registration_codes_csv("Registration_Codes.csv", registration_codes, csv_type)
  1370.  
  1371.  
  1372. @ensure_csrf_cookie
  1373. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1374. @require_sales_admin
  1375. @require_POST
  1376. def generate_registration_codes(request, course_id):
  1377.     """
  1378.    Respond with csv which contains a summary of all Generated Codes.
  1379.    """
  1380.     course_id = CourseKey.from_string(course_id)
  1381.     invoice_copy = False
  1382.  
  1383.     # covert the course registration code number into integer
  1384.     try:
  1385.         course_code_number = int(request.POST['total_registration_codes'])
  1386.     except ValueError:
  1387.         course_code_number = int(float(request.POST['total_registration_codes']))
  1388.  
  1389.     company_name = request.POST['company_name']
  1390.     company_contact_name = request.POST['company_contact_name']
  1391.     company_contact_email = request.POST['company_contact_email']
  1392.     unit_price = request.POST['unit_price']
  1393.  
  1394.     try:
  1395.         unit_price = (
  1396.             decimal.Decimal(unit_price)
  1397.         ).quantize(
  1398.             decimal.Decimal('.01'),
  1399.             rounding=decimal.ROUND_DOWN
  1400.         )
  1401.     except decimal.InvalidOperation:
  1402.         return HttpResponse(
  1403.             status=400,
  1404.             content=_(u"Could not parse amount as a decimal")
  1405.         )
  1406.  
  1407.     recipient_name = request.POST['recipient_name']
  1408.     recipient_email = request.POST['recipient_email']
  1409.     address_line_1 = request.POST['address_line_1']
  1410.     address_line_2 = request.POST['address_line_2']
  1411.     address_line_3 = request.POST['address_line_3']
  1412.     city = request.POST['city']
  1413.     state = request.POST['state']
  1414.     zip_code = request.POST['zip']
  1415.     country = request.POST['country']
  1416.     internal_reference = request.POST['internal_reference']
  1417.     customer_reference_number = request.POST['customer_reference_number']
  1418.     recipient_list = [recipient_email]
  1419.     if request.POST.get('invoice', False):
  1420.         recipient_list.append(request.user.email)
  1421.         invoice_copy = True
  1422.  
  1423.     sale_price = unit_price * course_code_number
  1424.     set_user_preference(request.user, INVOICE_KEY, invoice_copy)
  1425.     sale_invoice = Invoice.objects.create(
  1426.         total_amount=sale_price,
  1427.         company_name=company_name,
  1428.         company_contact_email=company_contact_email,
  1429.         company_contact_name=company_contact_name,
  1430.         course_id=course_id,
  1431.         recipient_name=recipient_name,
  1432.         recipient_email=recipient_email,
  1433.         address_line_1=address_line_1,
  1434.         address_line_2=address_line_2,
  1435.         address_line_3=address_line_3,
  1436.         city=city,
  1437.         state=state,
  1438.         zip=zip_code,
  1439.         country=country,
  1440.         internal_reference=internal_reference,
  1441.         customer_reference_number=customer_reference_number
  1442.     )
  1443.  
  1444.     invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
  1445.         invoice=sale_invoice,
  1446.         qty=course_code_number,
  1447.         unit_price=unit_price,
  1448.         course_id=course_id
  1449.     )
  1450.  
  1451.     course = get_course_by_id(course_id, depth=0)
  1452.     paid_modes = CourseMode.paid_modes_for_course(course_id)
  1453.  
  1454.     if len(paid_modes) != 1:
  1455.         msg = (
  1456.             u"Generating Code Redeem Codes for Course '{course_id}', which must have a single paid course mode. "
  1457.             u"This is a configuration issue. Current course modes with payment options: {paid_modes}"
  1458.         ).format(course_id=course_id, paid_modes=paid_modes)
  1459.         log.error(msg)
  1460.         return HttpResponse(
  1461.             status=500,
  1462.             content=_(u"Unable to generate redeem codes because of course misconfiguration.")
  1463.         )
  1464.  
  1465.     course_mode = paid_modes[0]
  1466.     course_price = course_mode.min_price
  1467.  
  1468.     registration_codes = []
  1469.     for __ in range(course_code_number):  # pylint: disable=redefined-outer-name
  1470.         generated_registration_code = save_registration_code(
  1471.             request.user, course_id, course_mode.slug, invoice=sale_invoice, order=None, invoice_item=invoice_item
  1472.         )
  1473.         registration_codes.append(generated_registration_code)
  1474.  
  1475.     site_name = microsite.get_value('SITE_NAME', 'localhost')
  1476.     quantity = course_code_number
  1477.     discount = (float(quantity * course_price) - float(sale_price))
  1478.     course_url = '{base_url}{course_about}'.format(
  1479.         base_url=microsite.get_value('SITE_NAME', settings.SITE_NAME),
  1480.         course_about=reverse('about_course', kwargs={'course_id': course_id.to_deprecated_string()})
  1481.     )
  1482.     dashboard_url = '{base_url}{dashboard}'.format(
  1483.         base_url=microsite.get_value('SITE_NAME', settings.SITE_NAME),
  1484.         dashboard=reverse('dashboard')
  1485.     )
  1486.  
  1487.     try:
  1488.         pdf_file = sale_invoice.generate_pdf_invoice(course, course_price, int(quantity), float(sale_price))
  1489.     except Exception:  # pylint: disable=broad-except
  1490.         log.exception('Exception at creating pdf file.')
  1491.         pdf_file = None
  1492.  
  1493.     from_address = microsite.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
  1494.     context = {
  1495.         'invoice': sale_invoice,
  1496.         'site_name': site_name,
  1497.         'course': course,
  1498.         'course_price': course_price,
  1499.         'sub_total': course_price * quantity,
  1500.         'discount': discount,
  1501.         'sale_price': sale_price,
  1502.         'quantity': quantity,
  1503.         'registration_codes': registration_codes,
  1504.         'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
  1505.         'course_url': course_url,
  1506.         'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
  1507.         'dashboard_url': dashboard_url,
  1508.         'contact_email': from_address,
  1509.         'corp_address': microsite.get_value('invoice_corp_address', settings.INVOICE_CORP_ADDRESS),
  1510.         'payment_instructions': microsite.get_value('invoice_payment_instructions', settings. INVOICE_PAYMENT_INSTRUCTIONS),
  1511.         'date': time.strftime("%m/%d/%Y")
  1512.     }
  1513.     # composes registration codes invoice email
  1514.     subject = u'Confirmation and Invoice for {course_name}'.format(course_name=course.display_name)
  1515.     message = render_to_string('emails/registration_codes_sale_email.txt', context)
  1516.  
  1517.     invoice_attachment = render_to_string('emails/registration_codes_sale_invoice_attachment.txt', context)
  1518.  
  1519.     #send_mail(subject, message, from_address, recipient_list, fail_silently=False)
  1520.     csv_file = StringIO.StringIO()
  1521.     csv_writer = csv.writer(csv_file)
  1522.     for registration_code in registration_codes:
  1523.         full_redeem_code_url = 'http://{base_url}{redeem_code_url}'.format(
  1524.             base_url=microsite.get_value('SITE_NAME', settings.SITE_NAME),
  1525.             redeem_code_url=reverse('register_code_redemption', kwargs={'registration_code': registration_code.code})
  1526.         )
  1527.         csv_writer.writerow([registration_code.code, full_redeem_code_url])
  1528.     finance_email = microsite.get_value('finance_email', settings.FINANCE_EMAIL)
  1529.     if finance_email:
  1530.         # append the finance email into the recipient_list
  1531.         recipient_list.append(finance_email)
  1532.  
  1533.     # send a unique email for each recipient, don't put all email addresses in a single email
  1534.     for recipient in recipient_list:
  1535.         email = EmailMessage()
  1536.         email.subject = subject
  1537.         email.body = message
  1538.         email.from_email = from_address
  1539.         email.to = [recipient]
  1540.         email.attach(u'RegistrationCodes.csv', csv_file.getvalue(), 'text/csv')
  1541.         email.attach(u'Invoice.txt', invoice_attachment, 'text/plain')
  1542.         if pdf_file is not None:
  1543.             email.attach(u'Invoice.pdf', pdf_file.getvalue(), 'application/pdf')
  1544.         else:
  1545.             file_buffer = StringIO.StringIO(_('pdf download unavailable right now, please contact support.'))
  1546.             email.attach(u'pdf_unavailable.txt', file_buffer.getvalue(), 'text/plain')
  1547.         email.send()
  1548.  
  1549.     return registration_codes_csv("Registration_Codes.csv", registration_codes)
  1550.  
  1551.  
  1552. @ensure_csrf_cookie
  1553. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1554. @require_level('staff')
  1555. @require_POST
  1556. def active_registration_codes(request, course_id):  # pylint: disable=unused-argument
  1557.     """
  1558.    Respond with csv which contains a summary of all Active Registration Codes.
  1559.    """
  1560.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1561.  
  1562.     # find all the registration codes in this course
  1563.     registration_codes_list = CourseRegistrationCode.objects.filter(
  1564.         course_id=course_id
  1565.     ).order_by('invoice_item__invoice__company_name')
  1566.  
  1567.     company_name = request.POST['active_company_name']
  1568.     if company_name:
  1569.         registration_codes_list = registration_codes_list.filter(invoice_item__invoice__company_name=company_name)
  1570.     # find the redeemed registration codes if any exist in the db
  1571.     code_redemption_set = RegistrationCodeRedemption.objects.select_related(
  1572.         'registration_code', 'registration_code__invoice_item__invoice'
  1573.     ).filter(registration_code__course_id=course_id)
  1574.     if code_redemption_set.exists():
  1575.         redeemed_registration_codes = [code.registration_code.code for code in code_redemption_set]
  1576.         # exclude the redeemed registration codes from the registration codes list and you will get
  1577.         # all the registration codes that are active
  1578.         registration_codes_list = registration_codes_list.exclude(code__in=redeemed_registration_codes)
  1579.  
  1580.     return registration_codes_csv("Active_Registration_Codes.csv", registration_codes_list)
  1581.  
  1582.  
  1583. @ensure_csrf_cookie
  1584. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1585. @require_level('staff')
  1586. @require_POST
  1587. def spent_registration_codes(request, course_id):  # pylint: disable=unused-argument
  1588.     """
  1589.    Respond with csv which contains a summary of all Spent(used) Registration Codes.
  1590.    """
  1591.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1592.  
  1593.     # find the redeemed registration codes if any exist in the db
  1594.     code_redemption_set = RegistrationCodeRedemption.objects.select_related('registration_code').filter(
  1595.         registration_code__course_id=course_id
  1596.     )
  1597.     spent_codes_list = []
  1598.     if code_redemption_set.exists():
  1599.         redeemed_registration_codes = [code.registration_code.code for code in code_redemption_set]
  1600.         # filter the Registration Codes by course id and the redeemed codes and
  1601.         # you will get a list of all the spent(Redeemed) Registration Codes
  1602.         spent_codes_list = CourseRegistrationCode.objects.filter(
  1603.             course_id=course_id, code__in=redeemed_registration_codes
  1604.         ).order_by('invoice_item__invoice__company_name').select_related('invoice_item__invoice')
  1605.  
  1606.         company_name = request.POST['spent_company_name']
  1607.         if company_name:
  1608.             spent_codes_list = spent_codes_list.filter(invoice_item__invoice__company_name=company_name)  # pylint: disable=maybe-no-member
  1609.  
  1610.     csv_type = 'spent'
  1611.     return registration_codes_csv("Spent_Registration_Codes.csv", spent_codes_list, csv_type)
  1612.  
  1613.  
  1614. @ensure_csrf_cookie
  1615. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1616. @require_level('staff')
  1617. def get_anon_ids(request, course_id):  # pylint: disable=unused-argument
  1618.     """
  1619.    Respond with 2-column CSV output of user-id, anonymized-user-id
  1620.    """
  1621.     # TODO: the User.objects query and CSV generation here could be
  1622.     # centralized into instructor_analytics. Currently instructor_analytics
  1623.     # has similar functionality but not quite what's needed.
  1624.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1625.  
  1626.     def csv_response(filename, header, rows):
  1627.         """Returns a CSV http response for the given header and rows (excel/utf-8)."""
  1628.         response = HttpResponse(mimetype='text/csv')
  1629.         response['Content-Disposition'] = 'attachment; filename={0}'.format(unicode(filename).encode('utf-8'))
  1630.         writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
  1631.         # In practice, there should not be non-ascii data in this query,
  1632.         # but trying to do the right thing anyway.
  1633.         encoded = [unicode(s).encode('utf-8') for s in header]
  1634.         writer.writerow(encoded)
  1635.         for row in rows:
  1636.             encoded = [unicode(s).encode('utf-8') for s in row]
  1637.             writer.writerow(encoded)
  1638.         return response
  1639.  
  1640.     students = User.objects.filter(
  1641.         courseenrollment__course_id=course_id,
  1642.     ).order_by('id')
  1643.     header = ['User ID', 'Anonymized User ID', 'Course Specific Anonymized User ID']
  1644.     rows = [[s.id, unique_id_for_user(s, save=False), anonymous_id_for_user(s, course_id, save=False)] for s in students]
  1645.     return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
  1646.  
  1647.  
  1648. @ensure_csrf_cookie
  1649. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1650. @require_level('staff')
  1651. def get_distribution(request, course_id):
  1652.     """
  1653.    Respond with json of the distribution of students over selected features which have choices.
  1654.  
  1655.    Ask for a feature through the `feature` query parameter.
  1656.    If no `feature` is supplied, will return response with an
  1657.        empty response['feature_results'] object.
  1658.    A list of available will be available in the response['available_features']
  1659.    """
  1660.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1661.     feature = request.GET.get('feature')
  1662.     # alternate notations of None
  1663.     if feature in (None, 'null', ''):
  1664.         feature = None
  1665.     else:
  1666.         feature = str(feature)
  1667.  
  1668.     available_features = instructor_analytics.distributions.AVAILABLE_PROFILE_FEATURES
  1669.     # allow None so that requests for no feature can list available features
  1670.     if feature not in available_features + (None,):
  1671.         return HttpResponseBadRequest(strip_tags(
  1672.             "feature '{}' not available.".format(feature)
  1673.         ))
  1674.  
  1675.     response_payload = {
  1676.         'course_id': course_id.to_deprecated_string(),
  1677.         'queried_feature': feature,
  1678.         'available_features': available_features,
  1679.         'feature_display_names': instructor_analytics.distributions.DISPLAY_NAMES,
  1680.     }
  1681.  
  1682.     p_dist = None
  1683.     if feature is not None:
  1684.         p_dist = instructor_analytics.distributions.profile_distribution(course_id, feature)
  1685.         response_payload['feature_results'] = {
  1686.             'feature': p_dist.feature,
  1687.             'feature_display_name': p_dist.feature_display_name,
  1688.             'data': p_dist.data,
  1689.             'type': p_dist.type,
  1690.         }
  1691.  
  1692.         if p_dist.type == 'EASY_CHOICE':
  1693.             response_payload['feature_results']['choices_display_names'] = p_dist.choices_display_names
  1694.  
  1695.     return JsonResponse(response_payload)
  1696.  
  1697.  
  1698. @ensure_csrf_cookie
  1699. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1700. @common_exceptions_400
  1701. @require_level('staff')
  1702. @require_query_params(
  1703.     unique_student_identifier="email or username of student for whom to get progress url"
  1704. )
  1705. def get_student_progress_url(request, course_id):
  1706.     """
  1707.    Get the progress url of a student.
  1708.    Limited to staff access.
  1709.  
  1710.    Takes query paremeter unique_student_identifier and if the student exists
  1711.    returns e.g. {
  1712.        'progress_url': '/../...'
  1713.    }
  1714.    """
  1715.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1716.     user = get_student_from_identifier(request.GET.get('unique_student_identifier'))
  1717.  
  1718.     progress_url = reverse('student_progress', kwargs={'course_id': course_id.to_deprecated_string(), 'student_id': user.id})
  1719.  
  1720.     response_payload = {
  1721.         'course_id': course_id.to_deprecated_string(),
  1722.         'progress_url': progress_url,
  1723.     }
  1724.     return JsonResponse(response_payload)
  1725.  
  1726.  
  1727. @ensure_csrf_cookie
  1728. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1729. @require_level('staff')
  1730. @require_query_params(
  1731.     problem_to_reset="problem urlname to reset"
  1732. )
  1733. @common_exceptions_400
  1734. def reset_student_attempts(request, course_id):
  1735.     """
  1736.  
  1737.    Resets a students attempts counter or starts a task to reset all students
  1738.    attempts counters. Optionally deletes student state for a problem. Limited
  1739.    to staff access. Some sub-methods limited to instructor access.
  1740.  
  1741.    Takes some of the following query paremeters
  1742.        - problem_to_reset is a urlname of a problem
  1743.        - unique_student_identifier is an email or username
  1744.        - all_students is a boolean
  1745.            requires instructor access
  1746.            mutually exclusive with delete_module
  1747.            mutually exclusive with delete_module
  1748.        - delete_module is a boolean
  1749.            requires instructor access
  1750.            mutually exclusive with all_students
  1751.    """
  1752.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1753.     course = get_course_with_access(
  1754.         request.user, 'staff', course_id, depth=None
  1755.     )
  1756.  
  1757.     problem_to_reset = strip_if_string(request.GET.get('problem_to_reset'))
  1758.     student_identifier = request.GET.get('unique_student_identifier', None)
  1759.     student = None
  1760.     if student_identifier is not None:
  1761.         student = get_student_from_identifier(student_identifier)
  1762.     all_students = request.GET.get('all_students', False) in ['true', 'True', True]
  1763.     delete_module = request.GET.get('delete_module', False) in ['true', 'True', True]
  1764.  
  1765.     # parameter combinations
  1766.     if all_students and student:
  1767.         return HttpResponseBadRequest(
  1768.             "all_students and unique_student_identifier are mutually exclusive."
  1769.         )
  1770.     if all_students and delete_module:
  1771.         return HttpResponseBadRequest(
  1772.             "all_students and delete_module are mutually exclusive."
  1773.         )
  1774.  
  1775.     # instructor authorization
  1776.     if all_students or delete_module:
  1777.         if not has_access(request.user, 'instructor', course):
  1778.             return HttpResponseForbidden("Requires instructor access.")
  1779.  
  1780.     try:
  1781.         module_state_key = course_id.make_usage_key_from_deprecated_string(problem_to_reset)
  1782.     except InvalidKeyError:
  1783.         return HttpResponseBadRequest()
  1784.  
  1785.     response_payload = {}
  1786.     response_payload['problem_to_reset'] = problem_to_reset
  1787.  
  1788.     if student:
  1789.         try:
  1790.             enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module)
  1791.         except StudentModule.DoesNotExist:
  1792.             return HttpResponseBadRequest(_("Module does not exist."))
  1793.         except sub_api.SubmissionError:
  1794.             # Trust the submissions API to log the error
  1795.             error_msg = _("An error occurred while deleting the score.")
  1796.             return HttpResponse(error_msg, status=500)
  1797.         response_payload['student'] = student_identifier
  1798.     elif all_students:
  1799.         instructor_task.api.submit_reset_problem_attempts_for_all_students(request, module_state_key)
  1800.         response_payload['task'] = 'created'
  1801.         response_payload['student'] = 'All Students'
  1802.     else:
  1803.         return HttpResponseBadRequest()
  1804.  
  1805.     return JsonResponse(response_payload)
  1806.  
  1807.  
  1808. @ensure_csrf_cookie
  1809. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1810. @require_level('staff')
  1811. @common_exceptions_400
  1812. def reset_student_attempts_for_entrance_exam(request, course_id):  # pylint: disable=invalid-name
  1813.     """
  1814.  
  1815.    Resets a students attempts counter or starts a task to reset all students
  1816.    attempts counters for entrance exam. Optionally deletes student state for
  1817.    entrance exam. Limited to staff access. Some sub-methods limited to instructor access.
  1818.  
  1819.    Following are possible query parameters
  1820.        - unique_student_identifier is an email or username
  1821.        - all_students is a boolean
  1822.            requires instructor access
  1823.            mutually exclusive with delete_module
  1824.        - delete_module is a boolean
  1825.            requires instructor access
  1826.            mutually exclusive with all_students
  1827.    """
  1828.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1829.     course = get_course_with_access(
  1830.         request.user, 'staff', course_id, depth=None
  1831.     )
  1832.  
  1833.     if not course.entrance_exam_id:
  1834.         return HttpResponseBadRequest(
  1835.             _("Course has no entrance exam section.")
  1836.         )
  1837.  
  1838.     student_identifier = request.GET.get('unique_student_identifier', None)
  1839.     student = None
  1840.     if student_identifier is not None:
  1841.         student = get_student_from_identifier(student_identifier)
  1842.     all_students = request.GET.get('all_students', False) in ['true', 'True', True]
  1843.     delete_module = request.GET.get('delete_module', False) in ['true', 'True', True]
  1844.  
  1845.     # parameter combinations
  1846.     if all_students and student:
  1847.         return HttpResponseBadRequest(
  1848.             _("all_students and unique_student_identifier are mutually exclusive.")
  1849.         )
  1850.     if all_students and delete_module:
  1851.         return HttpResponseBadRequest(
  1852.             _("all_students and delete_module are mutually exclusive.")
  1853.         )
  1854.  
  1855.     # instructor authorization
  1856.     if all_students or delete_module:
  1857.         if not has_access(request.user, 'instructor', course):
  1858.             return HttpResponseForbidden(_("Requires instructor access."))
  1859.  
  1860.     try:
  1861.         entrance_exam_key = course_id.make_usage_key_from_deprecated_string(course.entrance_exam_id)
  1862.         if delete_module:
  1863.             instructor_task.api.submit_delete_entrance_exam_state_for_student(request, entrance_exam_key, student)
  1864.         else:
  1865.             instructor_task.api.submit_reset_problem_attempts_in_entrance_exam(request, entrance_exam_key, student)
  1866.     except InvalidKeyError:
  1867.         return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
  1868.  
  1869.     response_payload = {'student': student_identifier or _('All Students'), 'task': 'created'}
  1870.     return JsonResponse(response_payload)
  1871.  
  1872.  
  1873. @ensure_csrf_cookie
  1874. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1875. @require_level('instructor')
  1876. @require_query_params(problem_to_reset="problem urlname to reset")
  1877. @common_exceptions_400
  1878. def rescore_problem(request, course_id):
  1879.     """
  1880.    Starts a background process a students attempts counter. Optionally deletes student state for a problem.
  1881.    Limited to instructor access.
  1882.  
  1883.    Takes either of the following query paremeters
  1884.        - problem_to_reset is a urlname of a problem
  1885.        - unique_student_identifier is an email or username
  1886.        - all_students is a boolean
  1887.  
  1888.    all_students and unique_student_identifier cannot both be present.
  1889.    """
  1890.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1891.     problem_to_reset = strip_if_string(request.GET.get('problem_to_reset'))
  1892.     student_identifier = request.GET.get('unique_student_identifier', None)
  1893.     student = None
  1894.     if student_identifier is not None:
  1895.         student = get_student_from_identifier(student_identifier)
  1896.  
  1897.     all_students = request.GET.get('all_students') in ['true', 'True', True]
  1898.  
  1899.     if not (problem_to_reset and (all_students or student)):
  1900.         return HttpResponseBadRequest("Missing query parameters.")
  1901.  
  1902.     if all_students and student:
  1903.         return HttpResponseBadRequest(
  1904.             "Cannot rescore with all_students and unique_student_identifier."
  1905.         )
  1906.  
  1907.     try:
  1908.         module_state_key = course_id.make_usage_key_from_deprecated_string(problem_to_reset)
  1909.     except InvalidKeyError:
  1910.         return HttpResponseBadRequest("Unable to parse problem id")
  1911.  
  1912.     response_payload = {}
  1913.     response_payload['problem_to_reset'] = problem_to_reset
  1914.  
  1915.     if student:
  1916.         response_payload['student'] = student_identifier
  1917.         instructor_task.api.submit_rescore_problem_for_student(request, module_state_key, student)
  1918.         response_payload['task'] = 'created'
  1919.     elif all_students:
  1920.         instructor_task.api.submit_rescore_problem_for_all_students(request, module_state_key)
  1921.         response_payload['task'] = 'created'
  1922.     else:
  1923.         return HttpResponseBadRequest()
  1924.  
  1925.     return JsonResponse(response_payload)
  1926.  
  1927.  
  1928. @ensure_csrf_cookie
  1929. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1930. @require_level('instructor')
  1931. @common_exceptions_400
  1932. def rescore_entrance_exam(request, course_id):
  1933.     """
  1934.    Starts a background process a students attempts counter for entrance exam.
  1935.    Optionally deletes student state for a problem. Limited to instructor access.
  1936.  
  1937.    Takes either of the following query parameters
  1938.        - unique_student_identifier is an email or username
  1939.        - all_students is a boolean
  1940.  
  1941.    all_students and unique_student_identifier cannot both be present.
  1942.    """
  1943.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1944.     course = get_course_with_access(
  1945.         request.user, 'staff', course_id, depth=None
  1946.     )
  1947.  
  1948.     student_identifier = request.GET.get('unique_student_identifier', None)
  1949.     student = None
  1950.     if student_identifier is not None:
  1951.         student = get_student_from_identifier(student_identifier)
  1952.  
  1953.     all_students = request.GET.get('all_students') in ['true', 'True', True]
  1954.  
  1955.     if not course.entrance_exam_id:
  1956.         return HttpResponseBadRequest(
  1957.             _("Course has no entrance exam section.")
  1958.         )
  1959.  
  1960.     if all_students and student:
  1961.         return HttpResponseBadRequest(
  1962.             _("Cannot rescore with all_students and unique_student_identifier.")
  1963.         )
  1964.  
  1965.     try:
  1966.         entrance_exam_key = course_id.make_usage_key_from_deprecated_string(course.entrance_exam_id)
  1967.     except InvalidKeyError:
  1968.         return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
  1969.  
  1970.     response_payload = {}
  1971.     if student:
  1972.         response_payload['student'] = student_identifier
  1973.     else:
  1974.         response_payload['student'] = _("All Students")
  1975.     instructor_task.api.submit_rescore_entrance_exam_for_student(request, entrance_exam_key, student)
  1976.     response_payload['task'] = 'created'
  1977.     return JsonResponse(response_payload)
  1978.  
  1979.  
  1980. @ensure_csrf_cookie
  1981. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  1982. @require_level('staff')
  1983. def list_background_email_tasks(request, course_id):  # pylint: disable=unused-argument
  1984.     """
  1985.    List background email tasks.
  1986.    """
  1987.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  1988.     task_type = 'bulk_course_email'
  1989.     # Specifying for the history of a single task type
  1990.     tasks = instructor_task.api.get_instructor_task_history(course_id, task_type=task_type)
  1991.  
  1992.     response_payload = {
  1993.         'tasks': map(extract_task_features, tasks),
  1994.     }
  1995.     return JsonResponse(response_payload)
  1996.  
  1997.  
  1998. @ensure_csrf_cookie
  1999. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2000. @require_level('staff')
  2001. def list_email_content(request, course_id):  # pylint: disable=unused-argument
  2002.     """
  2003.    List the content of bulk emails sent
  2004.    """
  2005.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2006.     task_type = 'bulk_course_email'
  2007.     # First get tasks list of bulk emails sent
  2008.     emails = instructor_task.api.get_instructor_task_history(course_id, task_type=task_type)
  2009.  
  2010.     response_payload = {
  2011.         'emails': map(extract_email_features, emails),
  2012.     }
  2013.     return JsonResponse(response_payload)
  2014.  
  2015.  
  2016. @ensure_csrf_cookie
  2017. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2018. @require_level('staff')
  2019. def list_instructor_tasks(request, course_id):
  2020.     """
  2021.    List instructor tasks.
  2022.  
  2023.    Takes optional query paremeters.
  2024.        - With no arguments, lists running tasks.
  2025.        - `problem_location_str` lists task history for problem
  2026.        - `problem_location_str` and `unique_student_identifier` lists task
  2027.            history for problem AND student (intersection)
  2028.    """
  2029.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2030.     problem_location_str = strip_if_string(request.GET.get('problem_location_str', False))
  2031.     student = request.GET.get('unique_student_identifier', None)
  2032.     if student is not None:
  2033.         student = get_student_from_identifier(student)
  2034.  
  2035.     if student and not problem_location_str:
  2036.         return HttpResponseBadRequest(
  2037.             "unique_student_identifier must accompany problem_location_str"
  2038.         )
  2039.  
  2040.     if problem_location_str:
  2041.         try:
  2042.             module_state_key = course_id.make_usage_key_from_deprecated_string(problem_location_str)
  2043.         except InvalidKeyError:
  2044.             return HttpResponseBadRequest()
  2045.         if student:
  2046.             # Specifying for a single student's history on this problem
  2047.             tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student)
  2048.         else:
  2049.             # Specifying for single problem's history
  2050.             tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key)
  2051.     else:
  2052.         # If no problem or student, just get currently running tasks
  2053.         tasks = instructor_task.api.get_running_instructor_tasks(course_id)
  2054.  
  2055.     response_payload = {
  2056.         'tasks': map(extract_task_features, tasks),
  2057.     }
  2058.     return JsonResponse(response_payload)
  2059.  
  2060.  
  2061. @ensure_csrf_cookie
  2062. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2063. @require_level('staff')
  2064. def list_entrance_exam_instructor_tasks(request, course_id):  # pylint: disable=invalid-name
  2065.     """
  2066.    List entrance exam related instructor tasks.
  2067.  
  2068.    Takes either of the following query parameters
  2069.        - unique_student_identifier is an email or username
  2070.        - all_students is a boolean
  2071.    """
  2072.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2073.     course = get_course_by_id(course_id)
  2074.     student = request.GET.get('unique_student_identifier', None)
  2075.     if student is not None:
  2076.         student = get_student_from_identifier(student)
  2077.  
  2078.     try:
  2079.         entrance_exam_key = course_id.make_usage_key_from_deprecated_string(course.entrance_exam_id)
  2080.     except InvalidKeyError:
  2081.         return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
  2082.     if student:
  2083.         # Specifying for a single student's entrance exam history
  2084.         tasks = instructor_task.api.get_entrance_exam_instructor_task_history(course_id, entrance_exam_key, student)
  2085.     else:
  2086.         # Specifying for all student's entrance exam history
  2087.         tasks = instructor_task.api.get_entrance_exam_instructor_task_history(course_id, entrance_exam_key)
  2088.  
  2089.     response_payload = {
  2090.         'tasks': map(extract_task_features, tasks),
  2091.     }
  2092.     return JsonResponse(response_payload)
  2093.  
  2094.  
  2095. @ensure_csrf_cookie
  2096. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2097. @require_level('staff')
  2098. def list_report_downloads(_request, course_id):
  2099.     """
  2100.    List grade CSV files that are available for download for this course.
  2101.    """
  2102.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2103.     report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
  2104.  
  2105.     response_payload = {
  2106.         'downloads': [
  2107.             dict(name=name, url=url, link='<a href="{}">{}</a>'.format(url, name))
  2108.             for name, url in report_store.links_for(course_id)
  2109.         ]
  2110.     }
  2111.     return JsonResponse(response_payload)
  2112.  
  2113.  
  2114. @ensure_csrf_cookie
  2115. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2116. @require_level('staff')
  2117. @require_finance_admin
  2118. def list_financial_report_downloads(_request, course_id):
  2119.     """
  2120.    List grade CSV files that are available for download for this course.
  2121.    """
  2122.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2123.     report_store = ReportStore.from_config(config_name='FINANCIAL_REPORTS')
  2124.  
  2125.     response_payload = {
  2126.         'downloads': [
  2127.             dict(name=name, url=url, link='<a href="{}">{}</a>'.format(url, name))
  2128.             for name, url in report_store.links_for(course_id)
  2129.         ]
  2130.     }
  2131.     return JsonResponse(response_payload)
  2132.  
  2133.  
  2134. @ensure_csrf_cookie
  2135. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2136. @require_level('staff')
  2137. def calculate_grades_csv(request, course_id):
  2138.     """
  2139.    AlreadyRunningError is raised if the course's grades are already being updated.
  2140.    """
  2141.     course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2142.     try:
  2143.         instructor_task.api.submit_calculate_grades_csv(request, course_key)
  2144.         success_status = _(
  2145.             "Your grade report is being generated! "
  2146.             "You can view the status of the generation task in the 'Pending Tasks' section.")
  2147.         return JsonResponse({"status": success_status})
  2148.     except AlreadyRunningError:
  2149.         already_running_status = _(
  2150.             "A grade report generation task is already in progress. "
  2151.             "Check the 'Pending Tasks' table for the status of the task. "
  2152.             "When completed, the report will be available for download in the table below.")
  2153.         return JsonResponse({
  2154.             "status": already_running_status
  2155.         })
  2156.  
  2157.  
  2158. @ensure_csrf_cookie
  2159. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2160. @require_level('staff')
  2161. def problem_grade_report(request, course_id):
  2162.     """
  2163.    Request a CSV showing students' grades for all problems in the
  2164.    course.
  2165.  
  2166.    AlreadyRunningError is raised if the course's grades are already being
  2167.    updated.
  2168.    """
  2169.     course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2170.     try:
  2171.         instructor_task.api.submit_problem_grade_report(request, course_key)
  2172.         success_status = _(
  2173.             "Your problem grade report is being generated! "
  2174.             "You can view the status of the generation task in the 'Pending Tasks' section.")
  2175.         return JsonResponse({"status": success_status})
  2176.     except AlreadyRunningError:
  2177.         already_running_status = _(
  2178.             "A problem grade report is already being generated. "
  2179.             "Check the 'Pending Tasks' table for the status of the task. "
  2180.             "When completed, the report will be available for download in the table below.")
  2181.         return JsonResponse({
  2182.             "status": already_running_status
  2183.         })
  2184.  
  2185.  
  2186. @ensure_csrf_cookie
  2187. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2188. @require_level('staff')
  2189. @require_query_params('rolename')
  2190. def list_forum_members(request, course_id):
  2191.     """
  2192.    Lists forum members of a certain rolename.
  2193.    Limited to staff access.
  2194.  
  2195.    The requesting user must be at least staff.
  2196.    Staff forum admins can access all roles EXCEPT for FORUM_ROLE_ADMINISTRATOR
  2197.        which is limited to instructors.
  2198.  
  2199.    Takes query parameter `rolename`.
  2200.    """
  2201.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2202.     course = get_course_by_id(course_id)
  2203.     has_instructor_access = has_access(request.user, 'instructor', course)
  2204.     has_forum_admin = has_forum_access(
  2205.         request.user, course_id, FORUM_ROLE_ADMINISTRATOR
  2206.     )
  2207.  
  2208.     rolename = request.GET.get('rolename')
  2209.  
  2210.     # default roles require either (staff & forum admin) or (instructor)
  2211.     if not (has_forum_admin or has_instructor_access):
  2212.         return HttpResponseBadRequest(
  2213.             "Operation requires staff & forum admin or instructor access"
  2214.         )
  2215.  
  2216.     # EXCEPT FORUM_ROLE_ADMINISTRATOR requires (instructor)
  2217.     if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access:
  2218.         return HttpResponseBadRequest("Operation requires instructor access.")
  2219.  
  2220.     # filter out unsupported for roles
  2221.     if rolename not in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]:
  2222.         return HttpResponseBadRequest(strip_tags(
  2223.             "Unrecognized rolename '{}'.".format(rolename)
  2224.         ))
  2225.  
  2226.     try:
  2227.         role = Role.objects.get(name=rolename, course_id=course_id)
  2228.         users = role.users.all().order_by('username')
  2229.     except Role.DoesNotExist:
  2230.         users = []
  2231.  
  2232.     def extract_user_info(user):
  2233.         """ Convert user to dict for json rendering. """
  2234.         return {
  2235.             'username': user.username,
  2236.             'email': user.email,
  2237.             'first_name': user.first_name,
  2238.             'last_name': user.last_name,
  2239.         }
  2240.  
  2241.     response_payload = {
  2242.         'course_id': course_id.to_deprecated_string(),
  2243.         rolename: map(extract_user_info, users),
  2244.     }
  2245.     return JsonResponse(response_payload)
  2246.  
  2247.  
  2248. @ensure_csrf_cookie
  2249. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2250. @require_level('staff')
  2251. @require_post_params(send_to="sending to whom", subject="subject line", message="message text")
  2252. def send_email(request, course_id):
  2253.     """
  2254.    Send an email to self, staff, or everyone involved in a course.
  2255.    Query Parameters:
  2256.    - 'send_to' specifies what group the email should be sent to
  2257.       Options are defined by the CourseEmail model in
  2258.       lms/djangoapps/bulk_email/models.py
  2259.    - 'subject' specifies email's subject
  2260.    - 'message' specifies email's content
  2261.    """
  2262.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2263.  
  2264.     if not bulk_email_is_enabled_for_course(course_id):
  2265.         return HttpResponseForbidden("Email is not enabled for this course.")
  2266.  
  2267.     send_to = request.POST.get("send_to")
  2268.     subject = request.POST.get("subject")
  2269.     message = request.POST.get("message")
  2270.  
  2271.     # allow two branding points to come from Microsites: which CourseEmailTemplate should be used
  2272.     # and what the 'from' field in the email should be
  2273.     #
  2274.     # If these are None (because we are not in a Microsite or they are undefined in Microsite config) than
  2275.     # the system will use normal system defaults
  2276.     template_name = microsite.get_value('course_email_template_name')
  2277.     from_addr = microsite.get_value('course_email_from_addr')
  2278.  
  2279.     # Create the CourseEmail object.  This is saved immediately, so that
  2280.     # any transaction that has been pending up to this point will also be
  2281.     # committed.
  2282.     email = CourseEmail.create(
  2283.         course_id,
  2284.         request.user,
  2285.         send_to,
  2286.         subject, message,
  2287.         template_name=template_name,
  2288.         from_addr=from_addr
  2289.     )
  2290.  
  2291.     # Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes)
  2292.     instructor_task.api.submit_bulk_course_email(request, course_id, email.id)  # pylint: disable=no-member
  2293.  
  2294.     response_payload = {
  2295.         'course_id': course_id.to_deprecated_string(),
  2296.         'success': True,
  2297.     }
  2298.     return JsonResponse(response_payload)
  2299.  
  2300.  
  2301. @ensure_csrf_cookie
  2302. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2303. @require_level('staff')
  2304. @require_query_params(
  2305.     unique_student_identifier="email or username of user to change access",
  2306.     rolename="the forum role",
  2307.     action="'allow' or 'revoke'",
  2308. )
  2309. @common_exceptions_400
  2310. def update_forum_role_membership(request, course_id):
  2311.     """
  2312.    Modify user's forum role.
  2313.  
  2314.    The requesting user must be at least staff.
  2315.    Staff forum admins can access all roles EXCEPT for FORUM_ROLE_ADMINISTRATOR
  2316.        which is limited to instructors.
  2317.    No one can revoke an instructors FORUM_ROLE_ADMINISTRATOR status.
  2318.  
  2319.    Query parameters:
  2320.    - `email` is the target users email
  2321.    - `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
  2322.    - `action` is one of ['allow', 'revoke']
  2323.    """
  2324.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2325.     course = get_course_by_id(course_id)
  2326.     has_instructor_access = has_access(request.user, 'instructor', course)
  2327.     has_forum_admin = has_forum_access(
  2328.         request.user, course_id, FORUM_ROLE_ADMINISTRATOR
  2329.     )
  2330.  
  2331.     unique_student_identifier = request.GET.get('unique_student_identifier')
  2332.     rolename = request.GET.get('rolename')
  2333.     action = request.GET.get('action')
  2334.  
  2335.     # default roles require either (staff & forum admin) or (instructor)
  2336.     if not (has_forum_admin or has_instructor_access):
  2337.         return HttpResponseBadRequest(
  2338.             "Operation requires staff & forum admin or instructor access"
  2339.         )
  2340.  
  2341.     # EXCEPT FORUM_ROLE_ADMINISTRATOR requires (instructor)
  2342.     if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access:
  2343.         return HttpResponseBadRequest("Operation requires instructor access.")
  2344.  
  2345.     if rolename not in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]:
  2346.         return HttpResponseBadRequest(strip_tags(
  2347.             "Unrecognized rolename '{}'.".format(rolename)
  2348.         ))
  2349.  
  2350.     user = get_student_from_identifier(unique_student_identifier)
  2351.  
  2352.     try:
  2353.         update_forum_role(course_id, user, rolename, action)
  2354.     except Role.DoesNotExist:
  2355.         return HttpResponseBadRequest("Role does not exist.")
  2356.  
  2357.     response_payload = {
  2358.         'course_id': course_id.to_deprecated_string(),
  2359.         'action': action,
  2360.     }
  2361.     return JsonResponse(response_payload)
  2362.  
  2363.  
  2364. @ensure_csrf_cookie
  2365. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2366. @require_level('staff')
  2367. @require_query_params(
  2368.     aname="name of analytic to query",
  2369. )
  2370. @common_exceptions_400
  2371. def proxy_legacy_analytics(request, course_id):
  2372.     """
  2373.    Proxies to the analytics cron job server.
  2374.  
  2375.    `aname` is a query parameter specifying which analytic to query.
  2376.    """
  2377.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2378.     analytics_name = request.GET.get('aname')
  2379.  
  2380.     # abort if misconfigured
  2381.     if not (hasattr(settings, 'ANALYTICS_SERVER_URL') and
  2382.             hasattr(settings, 'ANALYTICS_API_KEY') and
  2383.             settings.ANALYTICS_SERVER_URL and settings.ANALYTICS_API_KEY):
  2384.         return HttpResponse("Analytics service not configured.", status=501)
  2385.  
  2386.     url = "{}get?aname={}&course_id={}&apikey={}".format(
  2387.         settings.ANALYTICS_SERVER_URL,
  2388.         analytics_name,
  2389.         urllib.quote(unicode(course_id)),
  2390.         settings.ANALYTICS_API_KEY,
  2391.     )
  2392.  
  2393.     try:
  2394.         res = requests.get(url)
  2395.     except Exception:  # pylint: disable=broad-except
  2396.         log.exception(u"Error requesting from analytics server at %s", url)
  2397.         return HttpResponse("Error requesting from analytics server.", status=500)
  2398.  
  2399.     if res.status_code is 200:
  2400.         payload = json.loads(res.content)
  2401.         add_block_ids(payload)
  2402.         content = json.dumps(payload)
  2403.         # return the successful request content
  2404.         return HttpResponse(content, content_type="application/json")
  2405.     elif res.status_code is 404:
  2406.         # forward the 404 and content
  2407.         return HttpResponse(res.content, content_type="application/json", status=404)
  2408.     else:
  2409.         # 500 on all other unexpected status codes.
  2410.         log.error(
  2411.             u"Error fetching %s, code: %s, msg: %s",
  2412.             url, res.status_code, res.content
  2413.         )
  2414.         return HttpResponse(
  2415.             "Error from analytics server ({}).".format(res.status_code),
  2416.             status=500
  2417.         )
  2418.  
  2419.  
  2420. @require_POST
  2421. def get_user_invoice_preference(request, course_id):  # pylint: disable=unused-argument
  2422.     """
  2423.    Gets invoice copy user's preferences.
  2424.    """
  2425.     invoice_copy_preference = True
  2426.     invoice_preference_value = get_user_preference(request.user, INVOICE_KEY)
  2427.     if invoice_preference_value is not None:
  2428.         invoice_copy_preference = invoice_preference_value == 'True'
  2429.  
  2430.     return JsonResponse({
  2431.         'invoice_copy': invoice_copy_preference
  2432.     })
  2433.  
  2434.  
  2435. def _display_unit(unit):
  2436.     """
  2437.    Gets string for displaying unit to user.
  2438.    """
  2439.     name = getattr(unit, 'display_name', None)
  2440.     if name:
  2441.         return u'{0} ({1})'.format(name, unit.location.to_deprecated_string())
  2442.     else:
  2443.         return unit.location.to_deprecated_string()
  2444.  
  2445.  
  2446. @handle_dashboard_error
  2447. @ensure_csrf_cookie
  2448. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2449. @require_level('staff')
  2450. @require_query_params('student', 'url', 'due_datetime')
  2451. def change_due_date(request, course_id):
  2452.     """
  2453.    Grants a due date extension to a student for a particular unit.
  2454.    """
  2455.     course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
  2456.     student = require_student_from_identifier(request.GET.get('student'))
  2457.     unit = find_unit(course, request.GET.get('url'))
  2458.     due_date = parse_datetime(request.GET.get('due_datetime'))
  2459.     set_due_date_extension(course, unit, student, due_date)
  2460.  
  2461.     return JsonResponse(_(
  2462.         'Successfully changed due date for student {0} for {1} '
  2463.         'to {2}').format(student.profile.name, _display_unit(unit),
  2464.                          due_date.strftime('%Y-%m-%d %H:%M')))
  2465.  
  2466.  
  2467. @handle_dashboard_error
  2468. @ensure_csrf_cookie
  2469. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2470. @require_level('staff')
  2471. @require_query_params('student', 'url')
  2472. def reset_due_date(request, course_id):
  2473.     """
  2474.    Rescinds a due date extension for a student on a particular unit.
  2475.    """
  2476.     course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
  2477.     student = require_student_from_identifier(request.GET.get('student'))
  2478.     unit = find_unit(course, request.GET.get('url'))
  2479.     set_due_date_extension(course, unit, student, None)
  2480.     if not getattr(unit, "due", None):
  2481.         # It's possible the normal due date was deleted after an extension was granted:
  2482.         return JsonResponse(
  2483.             _("Successfully removed invalid due date extension (unit has no due date).")
  2484.         )
  2485.  
  2486.     original_due_date_str = unit.due.strftime('%Y-%m-%d %H:%M')
  2487.     return JsonResponse(_(
  2488.         'Successfully reset due date for student {0} for {1} '
  2489.         'to {2}').format(student.profile.name, _display_unit(unit),
  2490.                          original_due_date_str))
  2491.  
  2492.  
  2493. @handle_dashboard_error
  2494. @ensure_csrf_cookie
  2495. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2496. @require_level('staff')
  2497. @require_query_params('url')
  2498. def show_unit_extensions(request, course_id):
  2499.     """
  2500.    Shows all of the students which have due date extensions for the given unit.
  2501.    """
  2502.     course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
  2503.     unit = find_unit(course, request.GET.get('url'))
  2504.     return JsonResponse(dump_module_extensions(course, unit))
  2505.  
  2506.  
  2507. @handle_dashboard_error
  2508. @ensure_csrf_cookie
  2509. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2510. @require_level('staff')
  2511. @require_query_params('student')
  2512. def show_student_extensions(request, course_id):
  2513.     """
  2514.    Shows all of the due date extensions granted to a particular student in a
  2515.    particular course.
  2516.    """
  2517.     student = require_student_from_identifier(request.GET.get('student'))
  2518.     course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
  2519.     return JsonResponse(dump_student_extensions(course, student))
  2520.  
  2521.  
  2522. def _split_input_list(str_list):
  2523.     """
  2524.    Separate out individual student email from the comma, or space separated string.
  2525.  
  2526.    e.g.
  2527.    in: "Lorem@ipsum.dolor, sit@amet.consectetur\nadipiscing@elit.Aenean\r convallis@at.lacus\r, ut@lacinia.Sed"
  2528.    out: ['Lorem@ipsum.dolor', 'sit@amet.consectetur', 'adipiscing@elit.Aenean', 'convallis@at.lacus', 'ut@lacinia.Sed']
  2529.  
  2530.    `str_list` is a string coming from an input text area
  2531.    returns a list of separated values
  2532.    """
  2533.  
  2534.     new_list = re.split(r'[\n\r\s,]', str_list)
  2535.     new_list = [s.strip() for s in new_list]
  2536.     new_list = [s for s in new_list if s != '']
  2537.  
  2538.     return new_list
  2539.  
  2540.  
  2541. def _instructor_dash_url(course_key, section=None):
  2542.     """Return the URL for a section in the instructor dashboard.
  2543.  
  2544.    Arguments:
  2545.        course_key (CourseKey)
  2546.  
  2547.    Keyword Arguments:
  2548.        section (str): The name of the section to load.
  2549.  
  2550.    Returns:
  2551.        unicode: The URL of a section in the instructor dashboard.
  2552.  
  2553.    """
  2554.     url = reverse('instructor_dashboard', kwargs={'course_id': unicode(course_key)})
  2555.     if section is not None:
  2556.         url += u'#view-{section}'.format(section=section)
  2557.     return url
  2558.  
  2559.  
  2560. @require_global_staff
  2561. @require_POST
  2562. def generate_example_certificates(request, course_id=None):  # pylint: disable=unused-argument
  2563.     """Start generating a set of example certificates.
  2564.  
  2565.    Example certificates are used to verify that certificates have
  2566.    been configured correctly for the course.
  2567.  
  2568.    Redirects back to the intructor dashboard once certificate
  2569.    generation has begun.
  2570.  
  2571.    """
  2572.     course_key = CourseKey.from_string(course_id)
  2573.     certs_api.generate_example_certificates(course_key)
  2574.     return redirect(_instructor_dash_url(course_key, section='certificates'))
  2575.  
  2576.  
  2577. @require_global_staff
  2578. @require_POST
  2579. def enable_certificate_generation(request, course_id=None):
  2580.     """Enable/disable self-generated certificates for a course.
  2581.  
  2582.    Once self-generated certificates have been enabled, students
  2583.    who have passed the course will be able to generate certificates.
  2584.  
  2585.    Redirects back to the intructor dashboard once the
  2586.    setting has been updated.
  2587.  
  2588.    """
  2589.     course_key = CourseKey.from_string(course_id)
  2590.     is_enabled = (request.POST.get('certificates-enabled', 'false') == 'true')
  2591.     certs_api.set_cert_generation_enabled(course_key, is_enabled)
  2592.     return redirect(_instructor_dash_url(course_key, section='certificates'))
  2593.  
  2594.  
  2595. #---- Gradebook (shown to small courses only) ----
  2596. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2597. @require_level('staff')
  2598. def spoc_gradebook(request, course_id):
  2599.     """
  2600.    Show the gradebook for this course:
  2601.    - Only shown for courses with enrollment < settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS")
  2602.    - Only displayed to course staff
  2603.    """
  2604.     course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2605.     course = get_course_with_access(request.user, 'staff', course_key, depth=None)
  2606.  
  2607.     enrolled_students = User.objects.filter(
  2608.         courseenrollment__course_id=course_key,
  2609.         courseenrollment__is_active=1
  2610.     ).order_by('username').select_related("profile")
  2611.  
  2612.     # possible extension: implement pagination to show to large courses
  2613.  
  2614.     student_info = [
  2615.         {
  2616.             'username': student.username,
  2617.             'id': student.id,
  2618.             'email': student.email,
  2619.             'grade_summary': student_grades(student, request, course),
  2620.             'realname': student.profile.name,
  2621.         }
  2622.         for student in enrolled_students
  2623.     ]
  2624.  
  2625.     return render_to_response('courseware/gradebook.html', {
  2626.         'students': student_info,
  2627.         'course': course,
  2628.         'course_id': course_key,
  2629.         # Checked above
  2630.         'staff_access': True,
  2631.         'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
  2632.     })
  2633.  
  2634.  
  2635. @ensure_csrf_cookie
  2636. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2637. @require_level('staff')
  2638. @require_POST
  2639. def mark_student_can_skip_entrance_exam(request, course_id):  # pylint: disable=invalid-name
  2640.     """
  2641.    Mark a student to skip entrance exam.
  2642.    Takes `unique_student_identifier` as required POST parameter.
  2643.    """
  2644.     course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
  2645.     student_identifier = request.POST.get('unique_student_identifier')
  2646.     student = get_student_from_identifier(student_identifier)
  2647.  
  2648.     __, created = EntranceExamConfiguration.objects.get_or_create(user=student, course_id=course_id)
  2649.     if created:
  2650.         message = _('This student (%s) will skip the entrance exam.') % student_identifier
  2651.     else:
  2652.         message = _('This student (%s) is already allowed to skip the entrance exam.') % student_identifier
  2653.     response_payload = {
  2654.         'message': message,
  2655.     }
  2656.     return JsonResponse(response_payload)
  2657.  
  2658.  
  2659. @ensure_csrf_cookie
  2660. @cache_control(no_cache=True, no_store=True, must_revalidate=True)
  2661. @require_global_staff
  2662. @require_POST
  2663. def start_certificate_generation(request, course_id):
  2664.     """
  2665.    Start generating certificates for all students enrolled in given course.
  2666.    """
  2667.     course_key = CourseKey.from_string(course_id)
  2668.     task = instructor_task.api.generate_certificates_for_all_students(request, course_key)
  2669.     message = _('Certificate generation task for all students of this course has been started. '
  2670.                 'You can view the status of the generation task in the "Pending Tasks" section.')
  2671.     response_payload = {
  2672.         'message': message,
  2673.         'task_id': task.task_id
  2674.     }
  2675.     return JsonResponse(response_payload)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement