Skip to content
Snippets Groups Projects
views.py 64 KiB
Newer Older
ichuang's avatar
ichuang committed
        eamap = request.session['ExternalAuthMap']
        try:
            validate_email(eamap.external_email)
            email = eamap.external_email
        except ValidationError:
            email = post_vars.get('email', '')
        if eamap.external_name.strip() == '':
            name = post_vars.get('name', '')
        else:
David Baumgold's avatar
David Baumgold committed
            name = eamap.external_name
ichuang's avatar
ichuang committed
        password = eamap.internal_password
        post_vars = dict(post_vars.items())
        post_vars.update(dict(email=email, name=name, password=password))
        log.debug(u'In create_account with external_auth: user = %s, email=%s', name, email)
Piotr Mitros's avatar
Piotr Mitros committed
    # Confirm we have a properly formed request
Matthew Mongeau's avatar
Matthew Mongeau committed
    for a in ['username', 'email', 'password', 'name']:
David Baumgold's avatar
David Baumgold committed
            js['value'] = _("Error (401 {field}). E-mail us.").format(field=a)
            return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

    if extra_fields.get('honor_code', 'required') == 'required' and \
            post_vars.get('honor_code', 'false') != u'true':
David Baumgold's avatar
David Baumgold committed
        js['value'] = _("To enroll, you must follow the honor code.").format(field=a)
        js['field'] = 'honor_code'
        return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

    # Can't have terms of service for certain SHIB users, like at Stanford
    tos_required = (
        not settings.FEATURES.get("AUTH_USE_SHIB") or
        not settings.FEATURES.get("SHIB_DISABLE_TOS") or
        not DoExternalAuth or
        not eamap.external_domain.startswith(
            external_auth.views.SHIBBOLETH_DOMAIN_PREFIX
        )
    )
        if post_vars.get('terms_of_service', 'false') != u'true':
David Baumgold's avatar
David Baumgold committed
            js['value'] = _("You must accept the terms of service.").format(field=a)
            js['field'] = 'terms_of_service'
            return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

Matthew Mongeau's avatar
Matthew Mongeau committed
    # Confirm appropriate fields are there.
    # TODO: Check e-mail format is correct.
    # TODO: Confirm e-mail is not from a generic domain (mailinator, etc.)? Not sure if
Piotr Mitros's avatar
Piotr Mitros committed
    # this is a good idea
    # TODO: Check password is sane
    required_post_vars = ['username', 'email', 'name', 'password']
    required_post_vars += [fieldname for fieldname, val in extra_fields.items()
                           if val == 'required']
    if tos_required:
        required_post_vars.append('terms_of_service')

    for field_name in required_post_vars:
        if field_name in ('gender', 'level_of_education'):
            min_length = 1
        else:
            min_length = 2

        if len(post_vars[field_name]) < min_length:
            error_str = {
                'username': _('Username must be minimum of two characters long'),
                'email': _('A properly formatted e-mail is required'),
                'name': _('Your legal name must be a minimum of two characters long'),
                'password': _('A valid password is required'),
                'terms_of_service': _('Accepting Terms of Service is required'),
                'honor_code': _('Agreeing to the Honor Code is required'),
                'level_of_education': _('A level of education is required'),
                'gender': _('Your gender is required'),
                'year_of_birth': _('Your year of birth is required'),
                'mailing_address': _('Your mailing address is required'),
                'goals': _('A description of your goals is required'),
                'city': _('A city is required'),
                'country': _('A country is required')
            }
            js['value'] = error_str[field_name]
            js['field'] = field_name
            return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

    try:
    except ValidationError:
David Baumgold's avatar
David Baumgold committed
        js['value'] = _("Valid e-mail is required.").format(field=a)
        js['field'] = 'email'
        return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

    try:
    except ValidationError:
David Baumgold's avatar
David Baumgold committed
        js['value'] = _("Username should only consist of A-Z and 0-9, with no spaces.").format(field=a)
        js['field'] = 'username'
        return JsonResponse(js, status=400)
    # enforce password complexity as an optional feature
    if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
        try:
            password = post_vars['password']

            validate_password_length(password)
            validate_password_complexity(password)
            validate_password_dictionary(password)
        except ValidationError, err:
            js['value'] = _('Password: ') + '; '.join(err.messages)
            js['field'] = 'password'
            return JsonResponse(js, status=400)

    # Ok, looks like everything is legit.  Create the account.
    ret = _do_create_account(post_vars)
    if isinstance(ret, HttpResponse):  # if there was an error then return that
        return ret
    (user, profile, registration) = ret
    context = {
        'name': post_vars['name'],
        'key': registration.activation_key,
    }
Piotr Mitros's avatar
Piotr Mitros committed

ichuang's avatar
ichuang committed
    # composes activation email
    subject = render_to_string('emails/activation_email_subject.txt', context)
ichuang's avatar
ichuang committed
    # Email subject *must not* contain newlines
Piotr Mitros's avatar
Piotr Mitros committed
    subject = ''.join(subject.splitlines())
    message = render_to_string('emails/activation_email.txt', context)
Piotr Mitros's avatar
Piotr Mitros committed

    # don't send email if we are doing load testing or random user generation for some reason
    if not (settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')):
        from_address = MicrositeConfiguration.get_microsite_configuration_value(
            'email_from_address',
            settings.DEFAULT_FROM_EMAIL
        )
            if settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
                dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL']
                message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) +
                           '-' * 80 + '\n\n' + message)
                send_mail(subject, message, from_address, [dest_addr], fail_silently=False)
David Baumgold's avatar
David Baumgold committed
                user.email_user(subject, message, from_address)
        except Exception:  # pylint: disable=broad-except
            log.warning('Unable to send activation email to user', exc_info=True)
            js['value'] = _('Could not send activation e-mail.')
            # What is the correct status code to use here? I think it's 500, because
            # the problem is on the server's end -- but also, the account was created.
            # Seems like the core part of the request was successful.
            return JsonResponse(js, status=500)
    # Immediately after a user creates an account, we log them in. They are only
    # logged in until they close the browser. They can't log in again until they click
    # the activation link from the email.
    login_user = authenticate(username=post_vars['username'], password=post_vars['password'])
    request.session.set_expiry(0)

    # TODO: there is no error checking here to see that the user actually logged in successfully,
    # and is not yet an active user.
    if login_user is not None:
        AUDIT_LOG.info(u"Login success on new account creation - {0}".format(login_user.username))

    if DoExternalAuth:
ichuang's avatar
ichuang committed
        eamap.user = login_user
        eamap.dtsignup = datetime.datetime.now(UTC)
ichuang's avatar
ichuang committed
        eamap.save()
        AUDIT_LOG.info("User registered with external_auth %s", post_vars['username'])
        AUDIT_LOG.info('Updated ExternalAuthMap for %s to be %s', post_vars['username'], eamap)
        if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
            log.info('bypassing activation email')
ichuang's avatar
ichuang committed
            login_user.is_active = True
            login_user.save()
            AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(login_user.username, login_user.email))
    dog_stats_api.increment("common.student.account_created")
    response = JsonResponse({
        'success': True,
        'redirect_url': try_change_enrollment(request),
    })

    # set the login cookie for the edx marketing site
    # we want this cookie to be accessed via javascript
    # so httponly is set to None

    if request.session.get_expire_at_browser_close():
        max_age = None
        expires = None
    else:
        max_age = request.session.get_expiry_age()
        expires_time = time.time() + max_age
        expires = cookie_date(expires_time)

    response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
                        'true', max_age=max_age,
                        expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
                        path='/',
                        secure=None,
                        httponly=None)
    return response

    Create or configure a user account, then log in as that user.
    Enabled only when
    settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] is true.
    Accepts the following querystring parameters:
    * `username`, `email`, and `password` for the user account
    * `full_name` for the user profile (the user's full name; defaults to the username)
    * `staff`: Set to "true" to make the user global staff.
    * `course_id`: Enroll the student in the course with `course_id`
    If username, email, or password are not provided, use
    randomly generated credentials.
    """
    # Generate a unique name to use if none provided
    unique_name = uuid.uuid4().hex[0:30]

    # Use the params from the request, otherwise use these defaults
    username = request.GET.get('username', unique_name)
    password = request.GET.get('password', unique_name)
    email = request.GET.get('email', unique_name + "@example.com")
    full_name = request.GET.get('full_name', username)
    is_staff = request.GET.get('staff', None)
    course_id = request.GET.get('course_id', None)

    # Get or create the user object
    post_data = {
        'username': username,
        'email': email,
        'password': password,
        'name': full_name,
        'honor_code': u'true',
        'terms_of_service': u'true',
    }
    # Attempt to create the account.
    # If successful, this will return a tuple containing
    # the new user object; otherwise it will return an error
    # message.
    result = _do_create_account(post_data)

    if isinstance(result, tuple):
        user = result[0]

    # If we did not create a new account, the user might already
    # exist.  Attempt to retrieve it.
    else:
        user = User.objects.get(username=username)
        user.email = email
        user.set_password(password)
        user.save()

    # Set the user's global staff bit
    if is_staff is not None:
        user.is_staff = (is_staff == "true")
        user.save()
    # Activate the user
    reg = Registration.objects.get(user=user)
    reg.activate()
    reg.save()
    # Enroll the user in a course
    if course_id is not None:
        CourseEnrollment.enroll(user, course_id)

    # Log in as the user
    user = authenticate(username=username, password=password)
    login(request, user)

    # Provide the user with a valid CSRF token
    # then return a 200 response
    success_msg = u"Logged in user {0} ({1}) with password {2}".format(
        username, email, password
    )
    response = HttpResponse(success_msg)
    response.set_cookie('csrftoken', csrf(request)['csrf_token'])
    return response
Piotr Mitros's avatar
Piotr Mitros committed
def activate_account(request, key):
    """When link in activation e-mail is clicked"""
    r = Registration.objects.filter(activation_key=key)
    if len(r) == 1:
        user_logged_in = request.user.is_authenticated()
        already_active = True
        if not r[0].user.is_active:
            r[0].activate()
        # Enroll student in any pending courses he/she may have if auto_enroll flag is set
        student = User.objects.filter(id=r[0].user_id)
        if student:
            ceas = CourseEnrollmentAllowed.objects.filter(email=student[0].email)
            for cea in ceas:
                if cea.auto_enroll:
                    CourseEnrollment.enroll(student[0], cea.course_id)

        resp = render_to_response(
            "registration/activation_complete.html",
            {
                'user_logged_in': user_logged_in,
                'already_active': already_active
            }
        )
    if len(r) == 0:
        return render_to_response(
            "registration/activation_invalid.html",
            {'csrf': csrf(request)['csrf_token']}
        )
David Baumgold's avatar
David Baumgold committed
    return HttpResponse(_("Unknown error. Please e-mail us to let us know how it happened."))
Piotr Mitros's avatar
Piotr Mitros committed

Piotr Mitros's avatar
Piotr Mitros committed
def password_reset(request):
    """ Attempts to send a password reset e-mail. """
Piotr Mitros's avatar
Piotr Mitros committed
    if request.method != "POST":
        raise Http404
    form = PasswordResetFormNoActive(request.POST)
Piotr Mitros's avatar
Piotr Mitros committed
    if form.is_valid():
Calen Pennington's avatar
Calen Pennington committed
        form.save(use_https=request.is_secure(),
                  from_email=settings.DEFAULT_FROM_EMAIL,
                  request=request,
                  domain_override=request.get_host())
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({
        'success': True,
        'value': render_to_string('registration/password_reset_done.html', {}),
    })
David Baumgold's avatar
David Baumgold committed

def password_reset_confirm_wrapper(
    request,
    uidb36=None,
    token=None,
):
    """ A wrapper around django.contrib.auth.views.password_reset_confirm.
        Needed because we want to set the user as active at this step.
    # cribbed from django.contrib.auth.views.password_reset_confirm
    try:
        uid_int = base36_to_int(uidb36)
        user = User.objects.get(id=uid_int)
        user.is_active = True
        user.save()
    except (ValueError, User.DoesNotExist):
        pass
    # we also want to pass settings.PLATFORM_NAME in as extra_context

    extra_context = {"platform_name": settings.PLATFORM_NAME}
    return password_reset_confirm(
        request, uidb36=uidb36, token=token, extra_context=extra_context
    )
Calen Pennington's avatar
Calen Pennington committed

def reactivation_email_for_user(user):
    try:
        reg = Registration.objects.get(user=user)
    except Registration.DoesNotExist:
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('No inactive user with this e-mail exists'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
David Baumgold's avatar
David Baumgold committed
    context = {
        'name': user.profile.name,
        'key': reg.activation_key,
    }
David Baumgold's avatar
David Baumgold committed
    subject = render_to_string('emails/activation_email_subject.txt', context)
    subject = ''.join(subject.splitlines())
David Baumgold's avatar
David Baumgold committed
    message = render_to_string('emails/activation_email.txt', context)
David Baumgold's avatar
David Baumgold committed
        user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
    except Exception:  # pylint: disable=broad-except
        log.warning('Unable to send reactivation email', exc_info=True)
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Unable to send reactivation email')
        })  # TODO: this should be status code 500  # pylint: disable=fixme
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})

@ensure_csrf_cookie
def change_email_request(request):
    """ AJAX call from the profile page. User wants a new e-mail.
    """
    ## Make sure it checks for existing e-mail conflicts
    if not request.user.is_authenticated:
        raise Http404
    user = request.user

    if not user.check_password(request.POST['password']):
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Invalid password'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
    new_email = request.POST['new_email']
    try:
        validate_email(new_email)
    except ValidationError:
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Valid e-mail address required.'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
    if User.objects.filter(email=new_email).count() != 0:
        ## CRITICAL TODO: Handle case sensitivity for e-mails
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('An account with this e-mail already exists.'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
    pec_list = PendingEmailChange.objects.filter(user=request.user)
Matthew Mongeau's avatar
Matthew Mongeau committed
    if len(pec_list) == 0:
        pec = PendingEmailChange()
        pec.user = user
        pec = pec_list[0]

    pec.new_email = request.POST['new_email']
    pec.activation_key = uuid.uuid4().hex
    pec.save()

Matthew Mongeau's avatar
Matthew Mongeau committed
    if pec.new_email == user.email:
        pec.delete()
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Old email is the same as the new email.'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
    context = {
        'key': pec.activation_key,
        'old_email': user.email,
        'new_email': pec.new_email
    }
    subject = render_to_string('emails/email_change_subject.txt', context)
    subject = ''.join(subject.splitlines())
    message = render_to_string('emails/email_change.txt', context)

    from_address = MicrositeConfiguration.get_microsite_configuration_value(
        'email_from_address',
        settings.DEFAULT_FROM_EMAIL
    )

David Baumgold's avatar
David Baumgold committed
    send_mail(subject, message, from_address, [pec.new_email])
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})

@ensure_csrf_cookie
def confirm_email_change(request, key):
    """ User requested a new e-mail. This is called when the activation
    link is clicked. We confirm with the old e-mail, and update
        try:
            pec = PendingEmailChange.objects.get(activation_key=key)
        except PendingEmailChange.DoesNotExist:
            response = render_to_response("invalid_email_key.html", {})

        user = pec.user
        address_context = {
            'old_email': user.email,
            'new_email': pec.new_email
        }
        if len(User.objects.filter(email=pec.new_email)) != 0:
            response = render_to_response("email_exists.html", {})

        subject = render_to_string('emails/email_change_subject.txt', address_context)
        subject = ''.join(subject.splitlines())
        message = render_to_string('emails/confirm_email_change.txt', address_context)
        up = UserProfile.objects.get(user=user)
        meta = up.get_meta()
        if 'old_emails' not in meta:
            meta['old_emails'] = []
        meta['old_emails'].append([user.email, datetime.datetime.now(UTC).isoformat()])
        up.set_meta(meta)
        up.save()
        # Send it to the old email...
        try:
            user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
        except Exception:
            log.warning('Unable to send confirmation email to old address', exc_info=True)
            response = render_to_response("email_change_failed.html", {'email': user.email})
            transaction.rollback()
            return response
        user.email = pec.new_email
        user.save()
        pec.delete()
        # And send it to the new email...
        try:
            user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
        except Exception:
            log.warning('Unable to send confirmation email to new address', exc_info=True)
            response = render_to_response("email_change_failed.html", {'email': pec.new_email})
            transaction.rollback()
            return response
        response = render_to_response("email_change_successful.html", address_context)
    except Exception:
        # If we get an unexpected exception, be sure to rollback the transaction
        transaction.rollback()
        raise
@ensure_csrf_cookie
def change_name_request(request):
    """ Log a request for a new name. """
    if not request.user.is_authenticated:
        raise Http404
        pnc = PendingNameChange.objects.get(user=request.user)
    except PendingNameChange.DoesNotExist:
        pnc = PendingNameChange()
    pnc.user = request.user
    pnc.new_name = request.POST['new_name']
    pnc.rationale = request.POST['rationale']
    if len(pnc.new_name) < 2:
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Name required'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
    pnc.save()

    # The following automatically accepts name change requests. Remove this to
    # go back to the old system where it gets queued up for admin approval.
    accept_name_change_by_id(pnc.id)

David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})
@ensure_csrf_cookie
def pending_name_changes(request):
    """ Web page which allows staff to approve or reject name changes. """
    if not request.user.is_staff:
        raise Http404

David Baumgold's avatar
David Baumgold committed
    students = []
    for change in PendingNameChange.objects.all():
        profile = UserProfile.objects.get(user=change.user)
        students.append({
            "new_name": change.new_name,
            "rationale": change.rationale,
            "old_name": profile.name,
            "email": change.user.email,
            "uid": change.user.id,
            "cid": change.id,
        })

    return render_to_response("name_changes.html", {"students": students})
@ensure_csrf_cookie
def reject_name_change(request):
    """ JSON: Name change process. Course staff clicks 'reject' on a given name change """
    if not request.user.is_staff:
        raise Http404

Matthew Mongeau's avatar
Matthew Mongeau committed
    try:
        pnc = PendingNameChange.objects.get(id=int(request.POST['id']))
Matthew Mongeau's avatar
Matthew Mongeau committed
    except PendingNameChange.DoesNotExist:
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Invalid ID'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme
    pnc.delete()
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})
def accept_name_change_by_id(id):
Matthew Mongeau's avatar
Matthew Mongeau committed
    try:
        pnc = PendingNameChange.objects.get(id=id)
Matthew Mongeau's avatar
Matthew Mongeau committed
    except PendingNameChange.DoesNotExist:
David Baumgold's avatar
David Baumgold committed
        return JsonResponse({
            "success": False,
            "error": _('Invalid ID'),
        })  # TODO: this should be status code 400  # pylint: disable=fixme

    u = pnc.user
    up = UserProfile.objects.get(user=u)

    # Save old name
    meta = up.get_meta()
    if 'old_names' not in meta:
        meta['old_names'] = []
    meta['old_names'].append([up.name, pnc.rationale, datetime.datetime.now(UTC).isoformat()])
    up.set_meta(meta)

    up.name = pnc.new_name
    up.save()
    pnc.delete()

David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})


@ensure_csrf_cookie
def accept_name_change(request):
    """ JSON: Name change process. Course staff clicks 'accept' on a given name change
    We used this during the prototype but now we simply record name changes instead
    of manually approving them. Still keeping this around in case we want to go
    back to this approval method.
    if not request.user.is_staff:
        raise Http404

    return accept_name_change_by_id(int(request.POST['id']))
@require_POST
@login_required
@ensure_csrf_cookie
def change_email_settings(request):
    """Modify logged-in user's setting for receiving emails from a course."""
    user = request.user

    course_id = request.POST.get("course_id")
    receive_emails = request.POST.get("receive_emails")
    if receive_emails:
        optout_object = Optout.objects.filter(user=user, course_id=course_id)
        if optout_object:
            optout_object.delete()
        log.info(u"User {0} ({1}) opted in to receive emails from course {2}".format(user.username, user.email, course_id))
        track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
    else:
        Optout.objects.get_or_create(user=user, course_id=course_id)
        log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
        track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')

David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})
daniel cebrian's avatar
daniel cebrian committed


@login_required
def token(request):
    '''
    Return a token for the backend of annotations.
    It uses the course id to retrieve a variable that contains the secret
    token found in inheritance.py. It also contains information of when
    the token was issued. This will be stored with the user along with
    the id for identification purposes in the backend.
    '''
    course_id = request.GET.get("course_id")
    course = course_from_id(course_id)
    dtnow = datetime.datetime.now()
    dtutcnow = datetime.datetime.utcnow()
    delta = dtnow - dtutcnow
    newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
    newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin)
    secret = course.annotation_token_secret
    custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": request.user.email, "ttl": 86400}
    newtoken = create_token(secret, custom_data)
    response = HttpResponse(newtoken, mimetype="text/plain")
    return response