Skip to content
Snippets Groups Projects
views.py 86.4 KiB
Newer Older
            user.id,
            "edx.bi.user.account.authenticated",
            {
                'category': "conversion",
Julia Hansbrough's avatar
Julia Hansbrough committed
                'label': registration_course_id,
                'provider': None
            },
            context={
                'Google Analytics': {
Sarina Canelake's avatar
Sarina Canelake committed
                    'clientId': tracking_context.get('client_id')
                }
            }
        )
        request.session['registration_course_id'] = None

Piotr Mitros's avatar
Piotr Mitros committed
    if user is not None and user.is_active:
            # We do not log here, because we have a handler registered
            # to perform logging on successful logins.
            login(request, user)
            if request.POST.get('remember') == 'true':
                log.debug("Setting user session to never expire")
            else:
                request.session.set_expiry(0)
Sarina Canelake's avatar
Sarina Canelake committed
        except Exception as exc:  # pylint: disable=broad-except
            AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?")
            log.critical("Login failed - Could not create session. Is memcached running?")
Sarina Canelake's avatar
Sarina Canelake committed
            log.exception(exc)
        redirect_url = try_change_enrollment(request)
        if third_party_auth_successful:
            redirect_url = pipeline.get_complete_url(backend_name)

David Baumgold's avatar
David Baumgold committed
        response = JsonResponse({
            "success": True,
            "redirect_url": redirect_url,
        })

        # 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)
David Baumgold's avatar
David Baumgold committed
        response.set_cookie(
            settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age,
            expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
            path='/', secure=None, httponly=None,
        )
    if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
        AUDIT_LOG.warning(u"Login failed - Account not active for user.id: {0}, resending activation".format(user.id))
    else:
        AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
David Baumgold's avatar
David Baumgold committed
    not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your e-mail for the activation instructions.")
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({
        "success": False,
        "value": not_activated_msg,
    })  # TODO: this should be status code 400  # pylint: disable=fixme
Piotr Mitros's avatar
Piotr Mitros committed

Piotr Mitros's avatar
Piotr Mitros committed
def logout_user(request):
    HTTP request to log out the user. Redirects to marketing page.
    Deletes both the CSRF and sessionid cookies so the marketing
    site can determine the logged in state of the user
    # We do not log here, because we have a handler registered
    # to perform logging on successful logouts.
Piotr Mitros's avatar
Piotr Mitros committed
    logout(request)
    if settings.FEATURES.get('AUTH_USE_CAS'):
        target = reverse('cas-logout')
    else:
        target = '/'
    response = redirect(target)
David Baumgold's avatar
David Baumgold committed
    response.delete_cookie(
        settings.EDXMKTG_COOKIE_NAME,
        path='/', domain=settings.SESSION_COOKIE_DOMAIN,
    )
Piotr Mitros's avatar
Piotr Mitros committed

@require_GET
@login_required
@ensure_csrf_cookie
def manage_user_standing(request):
    """
    Renders the view used to manage user standing. Also displays a table
    of user accounts that have been disabled and who disabled them.
    """
    if not request.user.is_staff:
        raise Http404
    all_disabled_accounts = UserStanding.objects.filter(
        account_status=UserStanding.ACCOUNT_DISABLED
    )

    all_disabled_users = [standing.user for standing in all_disabled_accounts]

    headers = ['username', 'account_changed_by']
    rows = []
    for user in all_disabled_users:
        row = [user.username, user.standing.all()[0].changed_by]
        rows.append(row)

    context = {'headers': headers, 'rows': rows}

    return render_to_response("manage_user_standing.html", context)

@require_POST
@login_required
@ensure_csrf_cookie
def disable_account_ajax(request):
    """
    Ajax call to change user standing. Endpoint of the form
    in manage_user_standing.html
    """
    if not request.user.is_staff:
        raise Http404
    username = request.POST.get('username')
    context = {}
    if username is None or username.strip() == '':
        context['message'] = _('Please enter a username')
        return JsonResponse(context, status=400)

    account_action = request.POST.get('account_action')
    if account_action is None:
        context['message'] = _('Please choose an option')
        return JsonResponse(context, status=400)

    username = username.strip()
    try:
        user = User.objects.get(username=username)
    except User.DoesNotExist:
        context['message'] = _("User with username {} does not exist").format(username)
        return JsonResponse(context, status=400)
    else:
        user_account, _success = UserStanding.objects.get_or_create(
            user=user, defaults={'changed_by': request.user},
        )
        if account_action == 'disable':
            user_account.account_status = UserStanding.ACCOUNT_DISABLED
            context['message'] = _("Successfully disabled {}'s account").format(username)
            log.info("{} disabled {}'s account".format(request.user, username))
        elif account_action == 'reenable':
            user_account.account_status = UserStanding.ACCOUNT_ENABLED
            context['message'] = _("Successfully reenabled {}'s account").format(username)
            log.info("{} reenabled {}'s account".format(request.user, username))
        else:
            context['message'] = _("Unexpected account status")
            return JsonResponse(context, status=400)
        user_account.changed_by = request.user
        user_account.standing_last_changed_at = datetime.datetime.now(UTC)
        user_account.save()

    return JsonResponse(context)

def change_setting(request):
    """JSON call to change a profile setting: Right now, location"""
    # TODO (vshnayder): location is no longer used
Sarina Canelake's avatar
Sarina Canelake committed
    u_prof = UserProfile.objects.get(user=request.user)  # request.user.profile_cache
Piotr Mitros's avatar
Piotr Mitros committed
    if 'location' in request.POST:
Sarina Canelake's avatar
Sarina Canelake committed
        u_prof.location = request.POST['location']
    u_prof.save()
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({
        "success": True,
Sarina Canelake's avatar
Sarina Canelake committed
        "location": u_prof.location,
Calen Pennington's avatar
Calen Pennington committed

class AccountValidationError(Exception):
    def __init__(self, message, field):
        super(AccountValidationError, self).__init__(message)
        self.field = field


@receiver(post_save, sender=User)
def user_signup_handler(sender, **kwargs):  # pylint: disable=W0613
    """
    handler that saves the user Signup Source
    when the user is created
    """
    if 'created' in kwargs and kwargs['created']:
        site = microsite.get_value('SITE_NAME')
        if site:
            user_signup_source = UserSignupSource(user=kwargs['instance'], site=site)
            user_signup_source.save()
            log.info(u'user {} originated from a white labeled "Microsite"'.format(kwargs['instance'].id))


def _do_create_account(post_vars, extended_profile=None):
    """
    Given cleaned post variables, create the User and UserProfile objects, as well as the
    registration for this user.

    Returns a tuple (User, UserProfile, Registration).

    Note: this function is also used for creating test users.
    """
    user = User(username=post_vars['username'],
                email=post_vars['email'],
                is_active=False)
    user.set_password(post_vars['password'])
    registration = Registration()
    # TODO: Rearrange so that if part of the process fails, the whole process fails.
    # Right now, we can have e.g. no registration e-mail sent out and a zombie account
    try:
    except IntegrityError:
        # Figure out the cause of the integrity error
        if len(User.objects.filter(username=post_vars['username'])) > 0:
            raise AccountValidationError(
                _("An account with the Public Username '{username}' already exists.").format(username=post_vars['username']),
                field="username"
Sarina Canelake's avatar
Sarina Canelake committed
            )
        elif len(User.objects.filter(email=post_vars['email'])) > 0:
            raise AccountValidationError(
                _("An account with the Email '{email}' already exists.").format(email=post_vars['email']),
                field="email"
Sarina Canelake's avatar
Sarina Canelake committed
            )
    # add this account creation to password history
    # NOTE, this will be a NOP unless the feature has been turned on in configuration
    password_history_entry = PasswordHistory()
    password_history_entry.create(user)

    registration.register(user)
    profile = UserProfile(user=user)
    profile.name = post_vars['name']
    profile.level_of_education = post_vars.get('level_of_education')
    profile.gender = post_vars.get('gender')
    profile.mailing_address = post_vars.get('mailing_address')
    profile.city = post_vars.get('city')
    profile.country = post_vars.get('country')
    profile.goals = post_vars.get('goals')
    # add any extended profile information in the denormalized 'meta' field in the profile
    if extended_profile:
        profile.meta = json.dumps(extended_profile)

        profile.year_of_birth = int(post_vars['year_of_birth'])
    except (ValueError, KeyError):
        # If they give us garbage, just ignore it instead
        # of asking them to put an integer.
        profile.year_of_birth = None
        profile.save()
Sarina Canelake's avatar
Sarina Canelake committed
    except Exception:  # pylint: disable=broad-except
David Baumgold's avatar
David Baumgold committed
        log.exception("UserProfile creation failed for user {id}.".format(id=user.id))

    UserPreference.set_preference(user, LANGUAGE_KEY, get_language())

    return (user, profile, registration)
def create_account(request, post_override=None):  # pylint: disable-msg=too-many-statements
ichuang's avatar
ichuang committed
    JSON call to create new edX account.
    Used by form in signup_modal.html, which is included into navigation.html
    js = {'success': False}  # pylint: disable-msg=invalid-name
    post_vars = post_override if post_override else request.POST

    # allow for microsites to define their own set of required/optional/hidden fields
    extra_fields = microsite.get_value(
        'REGISTRATION_EXTRA_FIELDS',
        getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})
    )
    if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')) and pipeline.running(request):
        post_vars = dict(post_vars.items())
        post_vars.update({'password': pipeline.make_random_password()})

ichuang's avatar
ichuang committed
    # if doing signup for an external authorization, then get email, password, name from the eamap
    # don't use the ones from the form, since the user could have hacked those
    # unless originally we didn't get a valid email or name from the external auth
Sarina Canelake's avatar
Sarina Canelake committed
    do_external_auth = 'ExternalAuthMap' in request.session
    if do_external_auth:
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
Sarina Canelake's avatar
Sarina Canelake committed
    for req_field in ['username', 'email', 'password', 'name']:
        if req_field not in post_vars:
            js['value'] = _("Error (401 {field}). E-mail us.").format(field=req_field)
            js['field'] = req_field
            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':
Sarina Canelake's avatar
Sarina Canelake committed
        js['value'] = _("To enroll, you must follow the honor code.")
        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
Sarina Canelake's avatar
Sarina Canelake committed
        not do_external_auth or
        not eamap.external_domain.startswith(
            external_auth.views.SHIBBOLETH_DOMAIN_PREFIX
        )
    )
        if post_vars.get('terms_of_service', 'false') != u'true':
Sarina Canelake's avatar
Sarina Canelake committed
            js['value'] = _("You must accept the terms of service.")
            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 field_name not in post_vars or len(post_vars[field_name]) < min_length:
                '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')
            }

            if field_name in error_str:
                js['value'] = error_str[field_name]
            else:
                js['value'] = _('You are missing one or more required fields')

            return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

        max_length = 75
        if field_name == 'username':
            max_length = 30

        if field_name in ('email', 'username') and len(post_vars[field_name]) > max_length:
            error_str = {
Sarina Canelake's avatar
Sarina Canelake committed
                'username': _('Username cannot be more than {num} characters long').format(num=max_length),
                'email': _('Email cannot be more than {num} characters long').format(num=max_length)
            }
            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:
Sarina Canelake's avatar
Sarina Canelake committed
        js['value'] = _("Valid e-mail is required.")
        js['field'] = 'email'
        return JsonResponse(js, status=400)
Piotr Mitros's avatar
Piotr Mitros committed

    try:
    except ValidationError:
Sarina Canelake's avatar
Sarina Canelake committed
        js['value'] = _("Username should only consist of A-Z and 0-9, with no spaces.")
        js['field'] = 'username'
        return JsonResponse(js, status=400)
    # enforce password complexity as an optional feature
    # but not if we're doing ext auth b/c those pws never get used and are auto-generated so might not pass validation
Sarina Canelake's avatar
Sarina Canelake committed
    if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False) and not do_external_auth:
        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)

    # allow microsites to define 'extended profile fields' which are
    # captured on user signup (for example via an overriden registration.html)
    # and then stored in the UserProfile
    extended_profile_fields = microsite.get_value('extended_profile_fields', [])
    extended_profile = None

    for field in extended_profile_fields:
        if field in post_vars:
            if not extended_profile:
                extended_profile = {}
            extended_profile[field] = post_vars[field]

    # Make sure that password and username fields do not match
    username = post_vars['username']
    password = post_vars['password']
    if username == password:
        js['value'] = _("Username and password fields cannot match")
        js['field'] = 'username'
        return JsonResponse(js, status=400)

    # Ok, looks like everything is legit.  Create the account.
    try:
        with transaction.commit_on_success():
            ret = _do_create_account(post_vars, extended_profile)
Sarina Canelake's avatar
Sarina Canelake committed
    except AccountValidationError as exc:
        return JsonResponse({'success': False, 'value': exc.message, 'field': exc.field}, status=400)
    (user, profile, registration) = ret
    dog_stats_api.increment("common.student.account_created")

    email = post_vars['email']

    # Track the user's registration
    if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
        tracking_context = tracker.get_tracker().resolve_context()
        analytics.identify(user.id, {
Julia Hansbrough's avatar
Julia Hansbrough committed
            'email': email,
            'username': username,
Julia Hansbrough's avatar
Julia Hansbrough committed
        # If the user is registering via 3rd party auth, track which provider they use
        provider_name = None
        if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request):
            running_pipeline = pipeline.get(request)
            current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
            provider_name = current_provider.NAME

        registration_course_id = request.session.get('registration_course_id')
        analytics.track(
            user.id,
Sarina Canelake's avatar
Sarina Canelake committed
            "edx.bi.user.account.registered",
Julia Hansbrough's avatar
Julia Hansbrough committed
                'category': 'conversion',
                'label': registration_course_id,
                'provider': provider_name
            },
            context={
                'Google Analytics': {
Sarina Canelake's avatar
Sarina Canelake committed
                    'clientId': tracking_context.get('client_id')
                }
            }
        )
        request.session['registration_course_id'] = None

    create_comments_service_user(user)

    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
    # or external auth with bypass activated
    send_email = (
        not settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING') and
        not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'))
    )
    if send_email:
        from_address = microsite.get_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.error('Unable to send activation email to user from "{from_address}"'.format(from_address=from_address), 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.
Sarina Canelake's avatar
Sarina Canelake committed
    new_user = authenticate(username=post_vars['username'], password=post_vars['password'])
    login(request, new_user)
    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.
Sarina Canelake's avatar
Sarina Canelake committed
    if new_user is not None:
        AUDIT_LOG.info(u"Login success on new account creation - {0}".format(new_user.username))
Sarina Canelake's avatar
Sarina Canelake committed
    if do_external_auth:
        eamap.user = new_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')
Sarina Canelake's avatar
Sarina Canelake committed
            new_user.is_active = True
            new_user.save()
            AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(new_user.username, new_user.email))
    dog_stats_api.increment("common.student.account_created")
    redirect_url = try_change_enrollment(request)

    # Resume the third-party-auth pipeline if necessary.
    if microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')) and pipeline.running(request):
        running_pipeline = pipeline.get(request)
        redirect_url = pipeline.get_complete_url(running_pipeline['backend'])

    response = JsonResponse({
        'success': True,
        'redirect_url': redirect_url,

    # 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`
    * `roles`: Comma-separated list of roles to grant 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)
    course_key = None
    if course_id:
        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]

    # 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
Sarina Canelake's avatar
Sarina Canelake committed
        user, _profile, reg = _do_create_account(post_data)
    except AccountValidationError:
        # Attempt to retrieve the existing user.
        user = User.objects.get(username=username)
        user.email = email
        user.set_password(password)
        user.save()
        reg = Registration.objects.get(user=user)

    # 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.activate()
    reg.save()
    # Enroll the user in a course
    if course_key is not None:
        CourseEnrollment.enroll(user, course_key)
    # Apply the roles
    for role_name in role_names:
        role = Role.objects.get(name=role_name, course_id=course_key)
        user.roles.add(role)

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

    create_comments_service_user(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} and user_id {3}".format(
        username, email, password, user.id
    )
    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"""
Sarina Canelake's avatar
Sarina Canelake committed
    regs = Registration.objects.filter(activation_key=key)
    if len(regs) == 1:
        user_logged_in = request.user.is_authenticated()
        already_active = True
Sarina Canelake's avatar
Sarina Canelake committed
        if not regs[0].user.is_active:
            regs[0].activate()
        # Enroll student in any pending courses he/she may have if auto_enroll flag is set
Sarina Canelake's avatar
Sarina Canelake committed
        student = User.objects.filter(id=regs[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
            }
        )
Sarina Canelake's avatar
Sarina Canelake committed
    if len(regs) == 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
    # Add some rate limiting here by re-using the RateLimitMixin as a helper class
    limiter = BadRequestRateLimiter()
    if limiter.is_rate_limit_exceeded(request):
        AUDIT_LOG.warning("Rate limit exceeded in password_reset")
        return HttpResponseForbidden()

    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())
    else:
        # bad user? tick the rate limiter counter
        AUDIT_LOG.info("Bad password_reset user passed in.")
        limiter.tick_bad_request_counter(request)

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
    # tie in password strength enforcement as an optional level of
    # security protection
    err_msg = None

    if request.method == 'POST':
        password = request.POST['new_password1']
        if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
            try:
                validate_password_length(password)
                validate_password_complexity(password)
                validate_password_dictionary(password)
            except ValidationError, err:
                err_msg = _('Password: ') + '; '.join(err.messages)

        # also, check the password reuse policy
        if not PasswordHistory.is_allowable_password_reuse(user, password):
            if user.is_staff:
                num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE']
            else:
                num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE']
            err_msg = ungettext(
                "You are re-using a password that you have used recently. You must have {num} distinct password before reusing a previous password.",
                "You are re-using a password that you have used recently. You must have {num} distinct passwords before reusing a previous password.",
                num_distinct
            ).format(num=num_distinct)

        # also, check to see if passwords are getting reset too frequent
        if PasswordHistory.is_password_reset_too_soon(user):
            num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
                "You are resetting passwords too frequently. Due to security policies, {num} day must elapse between password resets.",
                "You are resetting passwords too frequently. Due to security policies, {num} days must elapse between password resets.",
                num_days
            ).format(num=num_days)

    if err_msg:
        # We have an password reset attempt which violates some security policy, use the
        # existing Django template to communicate this back to the user
        context = {
            'validlink': True,
            'form': None,
            'title': _('Password reset unsuccessful'),
            'err_msg': err_msg,
        }
        return TemplateResponse(request, 'registration/password_reset_confirm.html', context)
    else:
        # we also want to pass settings.PLATFORM_NAME in as extra_context
        extra_context = {"platform_name": settings.PLATFORM_NAME}

        if request.method == 'POST':
            # remember what the old password hash is before we call down
            old_password_hash = user.password

            result = password_reset_confirm(
                request, uidb36=uidb36, token=token, extra_context=extra_context
            )

            # get the updated user
            updated_user = User.objects.get(id=uid_int)

            # did the password hash change, if so record it in the PasswordHistory
            if updated_user.password != old_password_hash:
                entry = PasswordHistory()
                entry.create(updated_user)

            return result
        else:
            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.error('Unable to send reactivation email from "{from_address}"'.format(from_address=settings.DEFAULT_FROM_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 = microsite.get_value(
        'email_from_address',
        settings.DEFAULT_FROM_EMAIL
    )
    try:
        send_mail(subject, message, from_address, [pec.new_email])
    except Exception:  # pylint: disable=broad-except
        log.error('Unable to send email activation link to user from "{from_address}"'.format(from_address=from_address), exc_info=True)
        return JsonResponse({
            "success": False,
            "error": _('Unable to send email activation link. Please try again later.')
        })
David Baumgold's avatar
David Baumgold committed
    return JsonResponse({"success": True})

@ensure_csrf_cookie
Sarina Canelake's avatar
Sarina Canelake committed
def confirm_email_change(request, key):  # pylint: disable=unused-argument
    """
    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)
Sarina Canelake's avatar
Sarina Canelake committed
        u_prof = UserProfile.objects.get(user=user)
        meta = u_prof.get_meta()
        if 'old_emails' not in meta:
            meta['old_emails'] = []
        meta['old_emails'].append([user.email, datetime.datetime.now(UTC).isoformat()])
Sarina Canelake's avatar
Sarina Canelake committed
        u_prof.set_meta(meta)
        u_prof.save()
        # Send it to the old email...
        try:
            user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
Sarina Canelake's avatar
Sarina Canelake committed
        except Exception:    # pylint: disable=broad-except
            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)
Sarina Canelake's avatar
Sarina Canelake committed
        except Exception:  # pylint: disable=broad-except
            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})