Пример #1
0
def validate_secondary_email(account_recovery, new_email):
    """
    Enforce valid email addresses.
    """

    from openedx.core.djangoapps.user_api.accounts.api import get_email_validation_error, \
        get_secondary_email_validation_error

    if get_email_validation_error(new_email):
        raise ValueError(_('Valid e-mail address required.'))

    if new_email == account_recovery.secondary_email:
        raise ValueError(_('Old email is the same as the new email.'))

    # Make sure that secondary email address is not same as user's primary email.
    if new_email == account_recovery.user.email:
        raise ValueError(_('Cannot be same as your sign in email address.'))

    # Make sure that secondary email address is not same as any of the primary emails currently in use or retired
    if email_exists_or_retired(new_email):
        raise ValueError(
            _("It looks like {email} belongs to an existing account. Try again with a different email address.").format(
                email=new_email
            )
        )

    message = get_secondary_email_validation_error(new_email)
    if message:
        raise ValueError(message)
Пример #2
0
def _validate_email_change(user, data, field_errors):
    # If user has requested to change email, we must call the multi-step process to handle this.
    # It is not handled by the serializer (which considers email to be read-only).
    if "email" not in data:
        return

    if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']:
        raise AccountUpdateError(
            u"Email address changes have been disabled by the site operators.")

    new_email = data["email"]
    try:
        student_views.validate_new_email(user, new_email)
    except ValueError as err:
        field_errors["email"] = {
            "developer_message":
            u"Error thrown from validate_new_email: '{}'".format(
                text_type(err)),
            "user_message":
            text_type(err)
        }
        return

    # Don't process with sending email to given new email, if it is already associated with
    # an account. User must see same success message with no error.
    # This is so that this endpoint cannot be used to determine if an email is valid or not.
    if email_exists_or_retired(new_email):
        del data["email"]
Пример #3
0
def validate_secondary_email(account_recovery, new_email):
    """
    Enforce valid email addresses.
    """

    from openedx.core.djangoapps.user_api.accounts.api import get_email_validation_error, \
        get_secondary_email_validation_error

    if get_email_validation_error(new_email):
        raise ValueError(_('Valid e-mail address required.'))

    if new_email == account_recovery.secondary_email:
        raise ValueError(_('Old email is the same as the new email.'))

    # Make sure that secondary email address is not same as user's primary email.
    if new_email == account_recovery.user.email:
        raise ValueError(_('Cannot be same as your sign in email address.'))

    # Make sure that secondary email address is not same as any of the primary emails currently in use or retired
    if email_exists_or_retired(new_email):
        raise ValueError(
            _("It looks like {email} belongs to an existing account. Try again with a different email address."
              ).format(email=new_email))

    message = get_secondary_email_validation_error(new_email)
    if message:
        raise ValueError(message)
Пример #4
0
def do_create_account_no_registration(data):
    """
    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.
    """
    # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation

    proposed_username = data["username"]
    user = User(username=proposed_username,
                email=data["phone_number"] +
                settings.DEFAULT_EMAIL_ACCOUNT_DOMAIN,
                is_active=True)
    log.warning("phone: " + data["phone_number"])
    password = normalize_password(data["password"])
    user.set_password(password)
    registration = Registration()
    try:
        with transaction.atomic():
            user.save()
    except IntegrityError:
        # Figure out the cause of the integrity error
        # TODO duplicate email is already handled by form.errors above as a ValidationError.
        # The checks for duplicate email/username should occur in the same place with an
        # AccountValidationError and a consistent user message returned (i.e. both should
        # return "It looks like {username} belongs to an existing account. Try again with a
        # different username.")
        if username_exists_or_retired(user.username):
            raise AccountValidationError(
                USERNAME_EXISTS_MSG_FMT.format(username=proposed_username),
                field="username")
        elif email_exists_or_retired(user.email):
            raise AccountValidationError(_(
                "An account with the Email '{email}' already exists.").format(
                    email=user.email),
                                         field="email")
        else:
            raise

    registration.register(user)
    profile_fields = [
        "name", "level_of_education", "gender", "mailing_address", "city",
        "country", "goals", "year_of_birth", "phone_number",
        "web_accelerator_name", "web_accelerator_link"
    ]
    profile = UserProfile(user=user,
                          **{key: data.get(key)
                             for key in profile_fields})
    profile.save()
    # except Exception:
    #     log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
    #     raise
    log.warning("Testing the process to register {id}".format(id=user.id))
    return user, profile
Пример #5
0
def _validate_email_doesnt_exist(email):
    """Validate that the email is not associated with an existing user.

    :param email: The proposed email (unicode).
    :return: None
    :raises: errors.AccountEmailAlreadyExists
    """
    if email is not None and email_exists_or_retired(email):
        raise errors.AccountEmailAlreadyExists(_(accounts.EMAIL_CONFLICT_MSG).format(email_address=email))
Пример #6
0
def _validate_email_doesnt_exist(email):
    """Validate that the email is not associated with an existing user.

    :param email: The proposed email (unicode).
    :return: None
    :raises: errors.AccountEmailAlreadyExists
    """
    if email is not None and email_exists_or_retired(email):
        raise errors.AccountEmailAlreadyExists(_(accounts.EMAIL_CONFLICT_MSG).format(email_address=email))
Пример #7
0
def check_edxapp_account_conflicts(email, username):
    """
    Exposed function to check conflicts
    """
    conflicts = []

    if username and username_exists_or_retired(username):
        conflicts.append("username")

    if email and email_exists_or_retired(email):
        conflicts.append("email")

    return conflicts
Пример #8
0
def validate_new_email(user, new_email):
    """
    Given a new email for a user, does some basic verification of the new address If any issues are encountered
    with verification a ValueError will be thrown.
    """
    try:
        validate_email(new_email)
    except ValidationError:
        raise ValueError(_('Valid e-mail address required.'))

    if new_email == user.email:
        raise ValueError(_('Old email is the same as the new email.'))

    if email_exists_or_retired(new_email):
        raise ValueError(_('An account with this e-mail already exists.'))
Пример #9
0
    def _handle_duplicate_email_username(self, request, data):
        # pylint: disable=no-member
        # TODO Verify whether this check is needed here - it may be duplicated in user_api.
        email = data.get('email')
        username = data.get('username')
        errors = {}

        if email is not None and email_exists_or_retired(email):
            errors["email"] = [{"user_message": accounts_settings.EMAIL_CONFLICT_MSG.format(email_address=email)}]

        if username is not None and username_exists_or_retired(username):
            errors["username"] = [{"user_message": accounts_settings.USERNAME_CONFLICT_MSG.format(username=username)}]

        if errors:
            return self._create_response(request, errors, status_code=409)
Пример #10
0
def _validate_secondary_email(user, data, field_errors):
    if "secondary_email" not in data:
        return

    secondary_email = data["secondary_email"]

    try:
        student_views.validate_secondary_email(user, secondary_email)
    except ValueError as err:
        field_errors["secondary_email"] = {
            "developer_message": u"Error thrown from validate_secondary_email: '{}'".format(text_type(err)),
            "user_message": text_type(err)
        }
    else:
        # Don't process with sending email to given new email, if it is already associated with
        # an account. User must see same success message with no error.
        # This is so that this endpoint cannot be used to determine if an email is valid or not.
        if email_exists_or_retired(secondary_email):
            del data["secondary_email"]
Пример #11
0
 def clean_email(self):
     """ Enforce email restrictions (if applicable) """
     email = self.cleaned_data["email"]
     if settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED is not None:
         # This Open edX instance has restrictions on what email addresses are allowed.
         allowed_patterns = settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED
         # We append a '$' to the regexs to prevent the common mistake of using a
         # pattern like '.*@edx\\.org' which would match '*****@*****.**'
         if not any(re.match(pattern + "$", email) for pattern in allowed_patterns):
             # This email is not on the whitelist of allowed emails. Check if
             # they may have been manually invited by an instructor and if not,
             # reject the registration.
             if not CourseEnrollmentAllowed.objects.filter(email=email).exists():
                 raise ValidationError(_(u"Unauthorized email address."))
     if email_exists_or_retired(email):
         raise ValidationError(
             _(
                 u"It looks like {email} belongs to an existing account. Try again with a different email address."
             ).format(email=email)
         )
     return email
Пример #12
0
 def clean_email(self):
     """ Enforce email restrictions (if applicable) """
     email = self.cleaned_data["email"]
     if settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED is not None:
         # This Open edX instance has restrictions on what email addresses are allowed.
         allowed_patterns = settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED
         # We append a '$' to the regexs to prevent the common mistake of using a
         # pattern like '.*@edx\\.org' which would match '*****@*****.**'
         if not any(re.match(pattern + "$", email) for pattern in allowed_patterns):
             # This email is not on the whitelist of allowed emails. Check if
             # they may have been manually invited by an instructor and if not,
             # reject the registration.
             if not CourseEnrollmentAllowed.objects.filter(email=email).exists():
                 raise ValidationError(_("Unauthorized email address."))
     if email_exists_or_retired(email):
         raise ValidationError(
             _(
                 "It looks like {email} belongs to an existing account. Try again with a different email address."
             ).format(email=email)
         )
     return email
Пример #13
0
def _validate_email_change(user, data, field_errors):
    # If user has requested to change email, we must call the multi-step process to handle this.
    # It is not handled by the serializer (which considers email to be read-only).
    if "email" not in data:
        return

    if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']:
        raise AccountUpdateError(u"Email address changes have been disabled by the site operators.")

    new_email = data["email"]
    try:
        student_views.validate_new_email(user, new_email)
    except ValueError as err:
        field_errors["email"] = {
            "developer_message": u"Error thrown from validate_new_email: '{}'".format(text_type(err)),
            "user_message": text_type(err)
        }
        return

    # Don't process with sending email to given new email, if it is already associated with
    # an account. User must see same success message with no error.
    # This is so that this endpoint cannot be used to determine if an email is valid or not.
    if email_exists_or_retired(new_email):
        del data["email"]
Пример #14
0
    def handle(self, *args, **options):

        while True:
            uname = raw_input('username: '******'Create MIT ExternalAuth? [n] ').lower() == 'y':
            email = '*****@*****.**' % uname
            if not email.endswith('@MIT.EDU'):
                print "Failed - email must be @MIT.EDU"
                sys.exit(-1)
            mit_domain = 'ssl:MIT'
            if ExternalAuthMap.objects.filter(external_id=email, external_domain=mit_domain):
                print "Failed - email %s already exists as external_id" % email
                sys.exit(-1)
            make_eamap = True
            password = GenPasswd(12)

            # get name from kerberos
            try:
                kname = os.popen("finger %s | grep 'name:'" % email).read().strip().split('name: ')[1].strip()
            except:
                kname = ''
            name = raw_input('Full name: [%s] ' % kname).strip()
            if name == '':
                name = kname
            print "name = %s" % name
        else:
            while True:
                password = getpass()
                password2 = getpass()
                if password == password2:
                    break
                print "Oops, passwords do not match, please retry"

            while True:
                email = raw_input('email: ')
                if email_exists_or_retired(email):
                    print "email %s already taken" % email
                else:
                    break

            name = raw_input('Full name: ')

        user = User(username=uname, email=email, is_active=True)
        user.set_password(password)
        try:
            user.save()
        except IntegrityError:
            print "Oops, failed to create user %s, IntegrityError" % user
            raise

        r = Registration()
        r.register(user)

        up = UserProfile(user=user)
        up.name = name
        up.save()

        if make_eamap:
            credentials = "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN=%s/emailAddress=%s" % (name, email)
            eamap = ExternalAuthMap(
                external_id=email,
                external_email=email,
                external_domain=mit_domain,
                external_name=name,
                internal_password=password,
                external_credentials=json.dumps(credentials),
            )
            eamap.user = user
            eamap.dtsignup = datetime.datetime.now(UTC)
            eamap.save()

        print "User %s created successfully!" % user

        if not raw_input('Add user %s to any groups? [n] ' % user).lower() == 'y':
            sys.exit(0)

        print "Here are the groups available:"

        groups = [str(g.name) for g in Group.objects.all()]
        print groups

        completer = MyCompleter(groups)
        readline.set_completer(completer.complete)
        readline.parse_and_bind('tab: complete')

        while True:
            gname = raw_input("Add group (tab to autocomplete, empty line to end): ")
            if not gname:
                break
            if gname not in groups:
                print "Unknown group %s" % gname
                continue
            g = Group.objects.get(name=gname)
            user.groups.add(g)
            print "Added %s to group %s" % (user, g)

        print "Done!"
Пример #15
0
def do_create_account(form, custom_form=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.
    """
    # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation
    if not configuration_helpers.get_value(
            'ALLOW_PUBLIC_ACCOUNT_CREATION',
            settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)
    ):
        raise PermissionDenied()

    errors = {}
    errors.update(form.errors)
    if custom_form:
        errors.update(custom_form.errors)

    if errors:
        raise ValidationError(errors)

    proposed_username = form.cleaned_data["username"]
    user = User(
        username=proposed_username,
        email=form.cleaned_data["email"],
        is_active=False
    )
    password = normalize_password(form.cleaned_data["password"])
    user.set_password(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:
        with transaction.atomic():
            user.save()
            if custom_form:
                custom_model = custom_form.save(commit=False)
                custom_model.user = user
                custom_model.save()
    except IntegrityError:
        # Figure out the cause of the integrity error
        # TODO duplicate email is already handled by form.errors above as a ValidationError.
        # The checks for duplicate email/username should occur in the same place with an
        # AccountValidationError and a consistent user message returned (i.e. both should
        # return "It looks like {username} belongs to an existing account. Try again with a
        # different username.")
        if username_exists_or_retired(user.username):
            raise AccountValidationError(
                USERNAME_EXISTS_MSG_FMT.format(username=proposed_username),
                field="username"
            )
        elif email_exists_or_retired(user.email):
            raise AccountValidationError(
                _("An account with the Email '{email}' already exists.").format(email=user.email),
                field="email"
            )
        else:
            raise

    registration.register(user)

    profile_fields = [
        "name", "level_of_education", "gender", "mailing_address", "city", "country", "goals",
        "year_of_birth"
    ]
    profile = UserProfile(
        user=user,
        **{key: form.cleaned_data.get(key) for key in profile_fields}
    )
    extended_profile = form.cleaned_extended_profile
    if extended_profile:
        profile.meta = json.dumps(extended_profile)
    try:
        profile.save()
    except Exception:
        log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
        raise

    return user, profile, registration
Пример #16
0
def update_account_settings(requesting_user, update, username=None):
    """Update user account information.

    Note:
        It is up to the caller of this method to enforce the contract that this method is only called
        with the user who made the request.

    Arguments:
        requesting_user (User): The user requesting to modify account information. Only the user with username
            'username' has permissions to modify account information.
        update (dict): The updated account field values.
        username (str): Optional username specifying which account should be updated. If not specified,
            `requesting_user.username` is assumed.

    Raises:
        errors.UserNotFound: no user with username `username` exists (or `requesting_user.username` if
            `username` is not specified)
        errors.UserNotAuthorized: the requesting_user does not have access to change the account
            associated with `username`
        errors.AccountValidationError: the update was not attempted because validation errors were found with
            the supplied update
        errors.AccountUpdateError: the update could not be completed. Note that if multiple fields are updated at the
            same time, some parts of the update may have been successful, even if an errors.AccountUpdateError is
            returned; in particular, the user account (not including e-mail address) may have successfully been updated,
            but then the e-mail change request, which is processed last, may throw an error.
        errors.UserAPIInternalError: the operation failed due to an unexpected error.

    """
    if username is None:
        username = requesting_user.username

    existing_user, existing_user_profile = _get_user_and_profile(username)
    account_recovery = _get_account_recovery(existing_user)

    if requesting_user.username != username:
        raise errors.UserNotAuthorized()

    # If user has requested to change email, we must call the multi-step process to handle this.
    # It is not handled by the serializer (which considers email to be read-only).
    changing_email = False
    if "email" in update:
        changing_email = True
        new_email = update["email"]
        del update["email"]

    # If user has requested to change name, store old name because we must update associated metadata
    # after the save process is complete.
    changing_full_name = False
    old_name = None
    if "name" in update:
        changing_full_name = True
        old_name = existing_user_profile.name

    changing_secondary_email = False
    if "secondary_email" in update:
        changing_secondary_email = True

    # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
    read_only_fields = set(update.keys()).intersection(
        AccountUserSerializer.get_read_only_fields() +
        AccountLegacyProfileSerializer.get_read_only_fields())

    # Build up all field errors, whether read-only, validation, or email errors.
    field_errors = {}

    if read_only_fields:
        for read_only_field in read_only_fields:
            field_errors[read_only_field] = {
                "developer_message":
                u"This field is not editable via this API",
                "user_message":
                _(u"The '{field_name}' field cannot be edited.").format(
                    field_name=read_only_field)
            }
            del update[read_only_field]

    user_serializer = AccountUserSerializer(existing_user, data=update)
    legacy_profile_serializer = AccountLegacyProfileSerializer(
        existing_user_profile, data=update)

    for serializer in user_serializer, legacy_profile_serializer:
        field_errors = add_serializer_errors(serializer, update, field_errors)

    # If the user asked to change email, validate it.
    if changing_email:
        try:
            student_views.validate_new_email(existing_user, new_email)
        except ValueError as err:
            field_errors["email"] = {
                "developer_message":
                u"Error thrown from validate_new_email: '{}'".format(
                    text_type(err)),
                "user_message":
                text_type(err)
            }

        # Don't process with sending email to given new email, if it is already associated with
        # an account. User must see same success message with no error.
        # This is so that this endpoint cannot be used to determine if an email is valid or not.
        changing_email = new_email and not email_exists_or_retired(new_email)

    if changing_secondary_email:
        try:
            student_views.validate_secondary_email(account_recovery,
                                                   update["secondary_email"])
        except ValueError as err:
            field_errors["secondary_email"] = {
                "developer_message":
                u"Error thrown from validate_secondary_email: '{}'".format(
                    text_type(err)),
                "user_message":
                text_type(err)
            }
        else:
            account_recovery.update_recovery_email(update["secondary_email"])

    # If the user asked to change full name, validate it
    if changing_full_name:
        try:
            student_forms.validate_name(update['name'])
        except ValidationError as err:
            field_errors["name"] = {
                "developer_message":
                u"Error thrown from validate_name: '{}'".format(err.message),
                "user_message":
                err.message
            }

    # If we have encountered any validation errors, return them to the user.
    if field_errors:
        raise errors.AccountValidationError(field_errors)

    try:
        # If everything validated, go ahead and save the serializers.

        # We have not found a way using signals to get the language proficiency changes (grouped by user).
        # As a workaround, store old and new values here and emit them after save is complete.
        if "language_proficiencies" in update:
            old_language_proficiencies = list(
                existing_user_profile.language_proficiencies.values('code'))

        for serializer in user_serializer, legacy_profile_serializer:
            serializer.save()

        # if any exception is raised for user preference (i.e. account_privacy), the entire transaction for user account
        # patch is rolled back and the data is not saved
        if 'account_privacy' in update:
            update_user_preferences(
                requesting_user,
                {'account_privacy': update["account_privacy"]}, existing_user)

        if "language_proficiencies" in update:
            new_language_proficiencies = update["language_proficiencies"]
            emit_setting_changed_event(
                user=existing_user,
                db_table=existing_user_profile.language_proficiencies.model.
                _meta.db_table,
                setting_name="language_proficiencies",
                old_value=old_language_proficiencies,
                new_value=new_language_proficiencies,
            )

        # If the name was changed, store information about the change operation. This is outside of the
        # serializer so that we can store who requested the change.
        if old_name:
            meta = existing_user_profile.get_meta()
            if 'old_names' not in meta:
                meta['old_names'] = []
            meta['old_names'].append([
                old_name,
                u"Name change requested through account API by {0}".format(
                    requesting_user.username),
                datetime.datetime.now(UTC).isoformat()
            ])
            existing_user_profile.set_meta(meta)
            existing_user_profile.save()

        # updating extended user profile info
        if 'extended_profile' in update:
            meta = existing_user_profile.get_meta()
            new_extended_profile = update['extended_profile']
            for field in new_extended_profile:
                field_name = field['field_name']
                new_value = field['field_value']
                meta[field_name] = new_value
            existing_user_profile.set_meta(meta)
            existing_user_profile.save()

    except PreferenceValidationError as err:
        raise AccountValidationError(err.preference_errors)
    except (AccountUpdateError, AccountValidationError) as err:
        raise err
    except Exception as err:
        raise AccountUpdateError(
            u"Error thrown when saving account updates: '{}'".format(
                text_type(err)))

    # And try to send the email change request if necessary.
    if changing_email:
        if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']:
            raise AccountUpdateError(
                u"Email address changes have been disabled by the site operators."
            )
        try:
            student_views.do_email_change_request(existing_user, new_email)
        except ValueError as err:
            raise AccountUpdateError(
                u"Error thrown from do_email_change_request: '{}'".format(
                    text_type(err)),
                user_message=text_type(err))
    if changing_secondary_email:
        try:
            student_views.do_email_change_request(
                user=existing_user,
                new_email=update["secondary_email"],
                secondary_email_change_request=True,
            )
        except ValueError as err:
            raise AccountUpdateError(
                u"Error thrown from do_email_change_request: '{}'".format(
                    text_type(err)),
                user_message=text_type(err))
Пример #17
0
def do_create_account(form, custom_form=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.
    """
    # Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation
    if not configuration_helpers.get_value(
            'ALLOW_PUBLIC_ACCOUNT_CREATION',
            settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION', True)
    ):
        raise PermissionDenied()

    errors = {}
    errors.update(form.errors)
    if custom_form:
        errors.update(custom_form.errors)

    if errors:
        raise ValidationError(errors)

    proposed_username = form.cleaned_data["username"]
    user = User(
        username=proposed_username,
        email=form.cleaned_data["email"],
        is_active=False
    )
    password = normalize_password(form.cleaned_data["password"])
    user.set_password(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:
        with transaction.atomic():
            user.save()
            if custom_form:
                custom_model = custom_form.save(commit=False)
                custom_model.user = user
                custom_model.save()
    except IntegrityError:
        # Figure out the cause of the integrity error
        # TODO duplicate email is already handled by form.errors above as a ValidationError.
        # The checks for duplicate email/username should occur in the same place with an
        # AccountValidationError and a consistent user message returned (i.e. both should
        # return "It looks like {username} belongs to an existing account. Try again with a
        # different username.")
        if username_exists_or_retired(user.username):
            raise AccountValidationError(
                USERNAME_EXISTS_MSG_FMT.format(username=proposed_username),
                field="username"
            )
        elif email_exists_or_retired(user.email):
            raise AccountValidationError(
                _("An account with the Email '{email}' already exists.").format(email=user.email),
                field="email"
            )
        else:
            raise

    registration.register(user)

    profile_fields = [
        "name", "level_of_education", "gender", "mailing_address", "city", "country", "goals",
        "year_of_birth"
    ]
    profile = UserProfile(
        user=user,
        **{key: form.cleaned_data.get(key) for key in profile_fields}
    )
    extended_profile = form.cleaned_extended_profile
    if extended_profile:
        profile.meta = json.dumps(extended_profile)
    try:
        profile.save()
    except Exception:
        log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
        raise

    return user, profile, registration
Пример #18
0
def update_account_settings(requesting_user, update, username=None):
    """Update user account information.

    Note:
        It is up to the caller of this method to enforce the contract that this method is only called
        with the user who made the request.

    Arguments:
        requesting_user (User): The user requesting to modify account information. Only the user with username
            'username' has permissions to modify account information.
        update (dict): The updated account field values.
        username (str): Optional username specifying which account should be updated. If not specified,
            `requesting_user.username` is assumed.

    Raises:
        errors.UserNotFound: no user with username `username` exists (or `requesting_user.username` if
            `username` is not specified)
        errors.UserNotAuthorized: the requesting_user does not have access to change the account
            associated with `username`
        errors.AccountValidationError: the update was not attempted because validation errors were found with
            the supplied update
        errors.AccountUpdateError: the update could not be completed. Note that if multiple fields are updated at the
            same time, some parts of the update may have been successful, even if an errors.AccountUpdateError is
            returned; in particular, the user account (not including e-mail address) may have successfully been updated,
            but then the e-mail change request, which is processed last, may throw an error.
        errors.UserAPIInternalError: the operation failed due to an unexpected error.

    """
    if username is None:
        username = requesting_user.username

    existing_user, existing_user_profile = _get_user_and_profile(username)
    account_recovery = _get_account_recovery(existing_user)

    if requesting_user.username != username:
        raise errors.UserNotAuthorized()

    # If user has requested to change email, we must call the multi-step process to handle this.
    # It is not handled by the serializer (which considers email to be read-only).
    changing_email = False
    if "email" in update:
        changing_email = True
        new_email = update["email"]
        del update["email"]

    # If user has requested to change name, store old name because we must update associated metadata
    # after the save process is complete.
    changing_full_name = False
    old_name = None
    if "name" in update:
        changing_full_name = True
        old_name = existing_user_profile.name

    changing_secondary_email = False
    if "secondary_email" in update:
        changing_secondary_email = True

    # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
    read_only_fields = set(update.keys()).intersection(
        AccountUserSerializer.get_read_only_fields() + AccountLegacyProfileSerializer.get_read_only_fields()
    )

    # Build up all field errors, whether read-only, validation, or email errors.
    field_errors = {}

    if read_only_fields:
        for read_only_field in read_only_fields:
            field_errors[read_only_field] = {
                "developer_message": u"This field is not editable via this API",
                "user_message": _(u"The '{field_name}' field cannot be edited.").format(field_name=read_only_field)
            }
            del update[read_only_field]

    user_serializer = AccountUserSerializer(existing_user, data=update)
    legacy_profile_serializer = AccountLegacyProfileSerializer(existing_user_profile, data=update)

    for serializer in user_serializer, legacy_profile_serializer:
        field_errors = add_serializer_errors(serializer, update, field_errors)

    # If the user asked to change email, validate it.
    if changing_email:
        try:
            student_views.validate_new_email(existing_user, new_email)
        except ValueError as err:
            field_errors["email"] = {
                "developer_message": u"Error thrown from validate_new_email: '{}'".format(text_type(err)),
                "user_message": text_type(err)
            }

        # Don't process with sending email to given new email, if it is already associated with
        # an account. User must see same success message with no error.
        # This is so that this endpoint cannot be used to determine if an email is valid or not.
        changing_email = new_email and not email_exists_or_retired(new_email)

    if changing_secondary_email:
        try:
            student_views.validate_secondary_email(account_recovery, update["secondary_email"])
        except ValueError as err:
            field_errors["secondary_email"] = {
                "developer_message": u"Error thrown from validate_secondary_email: '{}'".format(text_type(err)),
                "user_message": text_type(err)
            }
        else:
            account_recovery.update_recovery_email(update["secondary_email"])

    # If the user asked to change full name, validate it
    if changing_full_name:
        try:
            student_forms.validate_name(update['name'])
        except ValidationError as err:
            field_errors["name"] = {
                "developer_message": u"Error thrown from validate_name: '{}'".format(err.message),
                "user_message": err.message
            }

    # If we have encountered any validation errors, return them to the user.
    if field_errors:
        raise errors.AccountValidationError(field_errors)

    try:
        # If everything validated, go ahead and save the serializers.

        # We have not found a way using signals to get the language proficiency changes (grouped by user).
        # As a workaround, store old and new values here and emit them after save is complete.
        if "language_proficiencies" in update:
            old_language_proficiencies = list(existing_user_profile.language_proficiencies.values('code'))

        for serializer in user_serializer, legacy_profile_serializer:
            serializer.save()

        # if any exception is raised for user preference (i.e. account_privacy), the entire transaction for user account
        # patch is rolled back and the data is not saved
        if 'account_privacy' in update:
            update_user_preferences(
                requesting_user, {'account_privacy': update["account_privacy"]}, existing_user
            )

        if "language_proficiencies" in update:
            new_language_proficiencies = update["language_proficiencies"]
            emit_setting_changed_event(
                user=existing_user,
                db_table=existing_user_profile.language_proficiencies.model._meta.db_table,
                setting_name="language_proficiencies",
                old_value=old_language_proficiencies,
                new_value=new_language_proficiencies,
            )

        # If the name was changed, store information about the change operation. This is outside of the
        # serializer so that we can store who requested the change.
        if old_name:
            meta = existing_user_profile.get_meta()
            if 'old_names' not in meta:
                meta['old_names'] = []
            meta['old_names'].append([
                old_name,
                u"Name change requested through account API by {0}".format(requesting_user.username),
                datetime.datetime.now(UTC).isoformat()
            ])
            existing_user_profile.set_meta(meta)
            existing_user_profile.save()

        # updating extended user profile info
        if 'extended_profile' in update:
            meta = existing_user_profile.get_meta()
            new_extended_profile = update['extended_profile']
            for field in new_extended_profile:
                field_name = field['field_name']
                new_value = field['field_value']
                meta[field_name] = new_value
            existing_user_profile.set_meta(meta)
            existing_user_profile.save()

    except PreferenceValidationError as err:
        raise AccountValidationError(err.preference_errors)
    except (AccountUpdateError, AccountValidationError) as err:
        raise err
    except Exception as err:
        raise AccountUpdateError(
            u"Error thrown when saving account updates: '{}'".format(text_type(err))
        )

    # And try to send the email change request if necessary.
    if changing_email:
        if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']:
            raise AccountUpdateError(u"Email address changes have been disabled by the site operators.")
        try:
            student_views.do_email_change_request(existing_user, new_email)
        except ValueError as err:
            raise AccountUpdateError(
                u"Error thrown from do_email_change_request: '{}'".format(text_type(err)),
                user_message=text_type(err)
            )
    if changing_secondary_email:
        try:
            student_views.do_email_change_request(
                user=existing_user,
                new_email=update["secondary_email"],
                secondary_email_change_request=True,
            )
        except ValueError as err:
            raise AccountUpdateError(
                u"Error thrown from do_email_change_request: '{}'".format(text_type(err)),
                user_message=text_type(err)
            )