helpers.py 11.9 KB
Newer Older
1
"""Helpers for the student app. """
2
from datetime import datetime
Will Daly committed
3 4
import urllib

5
from pytz import UTC
6
from django.core.urlresolvers import reverse, NoReverseMatch
Ahsan committed
7 8 9 10 11 12 13 14
from oauth2_provider.models import (
    AccessToken as dot_access_token,
    RefreshToken as dot_refresh_token
)
from provider.oauth2.models import (
    AccessToken as dop_access_token,
    RefreshToken as dop_refresh_token
)
Will Daly committed
15

16
import third_party_auth
17
from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
18
from course_modes.models import CourseMode
19 20


21 22 23 24
# Enumeration of per-course verification statuses
# we display on the student dashboard.
VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify"
VERIFY_STATUS_SUBMITTED = "verify_submitted"
25
VERIFY_STATUS_RESUBMITTED = "re_verify_submitted"
26 27
VERIFY_STATUS_APPROVED = "verify_approved"
VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
28
VERIFY_STATUS_NEED_TO_REVERIFY = "verify_need_to_reverify"
29

30 31 32 33 34
DISABLE_UNENROLL_CERT_STATES = [
    'generating',
    'ready',
]

35

36
def check_verify_status_by_course(user, course_enrollments):
37 38
    """
    Determine the per-course verification statuses for a given user.
39 40 41 42 43

    The possible statuses are:
        * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
        * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
          but has have not yet been approved.
44 45
        * VERIFY_STATUS_RESUBMITTED: The student has re-submitted photos for re-verification while
          they still have an active but expiring ID verification
46 47
        * VERIFY_STATUS_APPROVED: The student has been successfully verified.
        * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
48 49
        * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is
            set to expire before the verification deadline for the course.
50 51 52 53 54 55 56 57 58 59 60 61

    It is is also possible that a course does NOT have a verification status if:
        * The user is not enrolled in a verified mode, meaning that the user didn't pay.
        * The course does not offer a verified mode.
        * The user submitted photos but an error occurred while verifying them.
        * The user submitted photos but the verification was denied.

    In the last two cases, we rely on messages in the sidebar rather than displaying
    messages for each course.

    Arguments:
        user (User): The currently logged-in user.
62
        course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.
63 64 65 66 67 68 69 70 71 72

    Returns:
        dict: Mapping of course keys verification status dictionaries.
            If no verification status is applicable to a course, it will not
            be included in the dictionary.
            The dictionaries have these keys:
                * status (str): One of the enumerated status codes.
                * days_until_deadline (int): Number of days until the verification deadline.
                * verification_good_until (str): Date string for the verification expiration date.

73
    """
74 75 76 77 78 79
    status_by_course = {}

    # Retrieve all verifications for the user, sorted in descending
    # order by submission datetime
    verifications = SoftwareSecurePhotoVerification.objects.filter(user=user)

80 81 82 83 84 85
    # Check whether the user has an active or pending verification attempt
    # To avoid another database hit, we re-use the queryset we have already retrieved.
    has_active_or_pending = SoftwareSecurePhotoVerification.user_has_valid_or_pending(
        user, queryset=verifications
    )

86 87 88 89 90
    # Retrieve expiration_datetime of most recent approved verification
    # To avoid another database hit, we re-use the queryset we have already retrieved.
    expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(user, verifications)
    verification_expiring_soon = SoftwareSecurePhotoVerification.is_verification_expiring_soon(expiration_datetime)

91 92 93 94
    # Retrieve verification deadlines for the enrolled courses
    enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments]
    course_deadlines = VerificationDeadline.deadlines_for_courses(enrolled_course_keys)

Awais committed
95 96
    recent_verification_datetime = None

97
    for enrollment in course_enrollments:
98

99 100 101 102 103 104 105
        # If the user hasn't enrolled as verified, then the course
        # won't display state related to its verification status.
        if enrollment.mode in CourseMode.VERIFIED_MODES:

            # Retrieve the verification deadline associated with the course.
            # This could be None if the course doesn't have a deadline.
            deadline = course_deadlines.get(enrollment.course_id)
Awais committed
106

107 108
            relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, verifications)

Awais committed
109 110 111 112 113 114 115 116
            # Picking the max verification datetime on each iteration only with approved status
            if relevant_verification is not None and relevant_verification.status == "approved":
                recent_verification_datetime = max(
                    recent_verification_datetime if recent_verification_datetime is not None
                    else relevant_verification.expiration_datetime,
                    relevant_verification.expiration_datetime
                )

117 118 119 120 121 122
            # By default, don't show any status related to verification
            status = None

            # Check whether the user was approved or is awaiting approval
            if relevant_verification is not None:
                if relevant_verification.status == "approved":
123 124 125 126
                    if verification_expiring_soon:
                        status = VERIFY_STATUS_NEED_TO_REVERIFY
                    else:
                        status = VERIFY_STATUS_APPROVED
127
                elif relevant_verification.status == "submitted":
128 129 130 131
                    if verification_expiring_soon:
                        status = VERIFY_STATUS_RESUBMITTED
                    else:
                        status = VERIFY_STATUS_SUBMITTED
132 133 134 135 136 137 138 139 140 141 142 143

            # If the user didn't submit at all, then tell them they need to verify
            # If the deadline has already passed, then tell them they missed it.
            # If they submitted but something went wrong (error or denied),
            # then don't show any messaging next to the course, since we already
            # show messages related to this on the left sidebar.
            submitted = (
                relevant_verification is not None and
                relevant_verification.status not in ["created", "ready"]
            )
            if status is None and not submitted:
                if deadline is None or deadline > datetime.now(UTC):
144 145 146 147 148 149
                    if SoftwareSecurePhotoVerification.user_is_verified(user):
                        if verification_expiring_soon:
                            # The user has an active verification, but the verification
                            # is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks).
                            # Tell the student to reverify.
                            status = VERIFY_STATUS_NEED_TO_REVERIFY
150
                    else:
151
                        status = VERIFY_STATUS_NEED_TO_VERIFY
152
                else:
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
                    # If a user currently has an active or pending verification,
                    # then they may have submitted an additional attempt after
                    # the verification deadline passed.  This can occur,
                    # for example, when the support team asks a student
                    # to reverify after the deadline so they can receive
                    # a verified certificate.
                    # In this case, we still want to show them as "verified"
                    # on the dashboard.
                    if has_active_or_pending:
                        status = VERIFY_STATUS_APPROVED

                    # Otherwise, the student missed the deadline, so show
                    # them as "honor" (the kind of certificate they will receive).
                    else:
                        status = VERIFY_STATUS_MISSED_DEADLINE
168 169 170 171 172 173 174 175 176 177

            # Set the status for the course only if we're displaying some kind of message
            # Otherwise, leave the course out of the dictionary.
            if status is not None:
                days_until_deadline = None

                now = datetime.now(UTC)
                if deadline is not None and deadline > now:
                    days_until_deadline = (deadline - now).days

178
                status_by_course[enrollment.course_id] = {
179
                    'status': status,
Awais committed
180
                    'days_until_deadline': days_until_deadline
181 182
                }

Awais committed
183 184 185 186
    if recent_verification_datetime:
        for key, value in status_by_course.iteritems():  # pylint: disable=unused-variable
            status_by_course[key]['verification_good_until'] = recent_verification_datetime.strftime("%m/%d/%Y")

187
    return status_by_course
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217


def auth_pipeline_urls(auth_entry, redirect_url=None):
    """Retrieve URLs for each enabled third-party auth provider.

    These URLs are used on the "sign up" and "sign in" buttons
    on the login/registration forms to allow users to begin
    authentication with a third-party provider.

    Optionally, we can redirect the user to an arbitrary
    url after auth completes successfully.  We use this
    to redirect the user to a page that required login,
    or to send users to the payment flow when enrolling
    in a course.

    Args:
        auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER`

    Keyword Args:
        redirect_url (unicode): If provided, send users to this URL
            after they successfully authenticate.

    Returns:
        dict mapping provider IDs to URLs

    """
    if not third_party_auth.is_enabled():
        return {}

    return {
218 219
        provider.provider_id: third_party_auth.pipeline.get_login_url(
            provider.provider_id, auth_entry, redirect_url=redirect_url
220
        ) for provider in third_party_auth.provider.Registry.displayed_for_login()
221 222 223 224 225
    }


# Query string parameters that can be passed to the "finish_auth" view to manage
# things like auto-enrollment.
226
POST_AUTH_PARAMS = ('course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow')
227 228 229 230 231 232


def get_next_url_for_login_page(request):
    """
    Determine the URL to redirect to following login/registration/third_party_auth

vkaracic committed
233
    The user is currently on a login or registration page.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    If 'course_id' is set, or other POST_AUTH_PARAMS, we will need to send the user to the
    /account/finish_auth/ view following login, which will take care of auto-enrollment in
    the specified course.

    Otherwise, we go to the ?next= query param or to the dashboard if nothing else is
    specified.
    """
    redirect_to = request.GET.get('next', None)
    if not redirect_to:
        try:
            redirect_to = reverse('dashboard')
        except NoReverseMatch:
            redirect_to = reverse('home')
    if any(param in request.GET for param in POST_AUTH_PARAMS):
        # Before we redirect to next/dashboard, we need to handle auto-enrollment:
        params = [(param, request.GET[param]) for param in POST_AUTH_PARAMS if param in request.GET]
        params.append(('next', redirect_to))  # After auto-enrollment, user will be sent to payment page or to this URL
        redirect_to = '{}?{}'.format(reverse('finish_auth'), urllib.urlencode(params))
        # Note: if we are resuming a third party auth pipeline, then the next URL will already
        # be saved in the session as part of the pipeline state. That URL will take priority
        # over this one.
    return redirect_to
Ahsan committed
256 257 258 259 260 261 262 263 264 265


def destroy_oauth_tokens(user):
    """
    Destroys ALL OAuth access and refresh tokens for the given user.
    """
    dop_access_token.objects.filter(user=user).delete()
    dop_refresh_token.objects.filter(user=user).delete()
    dot_access_token.objects.filter(user=user).delete()
    dot_refresh_token.objects.filter(user=user).delete()