0
votes

Edit (trying to clarify the problem):
The custom validator I created for my flask app to check for duplicate usernames and emails does not work for admin users to modify other user accounts. When an admin user edits another user's username or email, the validator indicates it is a duplicate because it is trying to compare the current (admin) user's username and email, not the username and email of the user being edited. How do I write a custom validator to use the user object passed to the form in the view function below to compare username and email for the user being edited and not the logged in user?

Original post:
I am working on a Flask app that includes the ability for users with admin privileges to modify user account information. Currently, I am using Flask-WTF and WTForms to create a form on a user profile page that the account owner or a user with admin privileges (through role assignment in the user model) can edit user information. In the view function, I pass the user information to the form to pre-populate the form fields with the user information. When I made the profile editing form for the account owner, when the form is submitted I have a custom validator that compares the submitted data to the data for the currently logged in user to detect if there were any fields edited if then check if some of those fields (like username, email, etc.) are already in the database to avoid duplicates. For the form that admin users can edit other users, this validator keeps preventing writing new values to the database because the validator is comparing the logged in user information to the database and not the user information belonging to the account that is being edited. I want to know how to pass the user object that I use to populate the form fields to the form validation when the form is submitted? The Flask app is using a blueprint structure. I am pretty new to this so I hope the question makes sense. Here is some code that I think is helpful.

Here is the view function:

# Edit account view function for admin users
@users.route('/edit-account/<int:user_id>', methods=['GET', 'POST'])
@login_required
def edit_account(user_id):
    # Get the info for the user being edited
    user = User.query.get_or_404(user_id)
    # Check to be sure user has admin privileges
    if current_user.role != 'admin' and current_user.role != 'agent':
        abort(403)
    form = AccountEditForm()
    if form.validate_on_submit():
        # Update the profile picture, if a file is submitted
        if form.profile_pic.data:
            picture_file = save_picture(form.profile_pic.data)
            user.profile_pic = picture_file
        # Update the database entries for the user
        user.username = form.username.data
        user.first_name = form.first_name.data
        user.last_name = form.last_name.data
        user.email = form.email.data
        user.role = form.role.data
        db.session.commit()
        flash(f'The account for {user.first_name} {user.last_name} has been updated.', 'success')
        return redirect(url_for('users.users_admin'))
    # Pre-populate the form with existing data
    elif request.method == 'GET':
        form.username.data = user.username
        form.first_name.data = user.first_name
        form.last_name.data = user.last_name
        form.email.data = user.email
        form.role.data = user.role
    image_file = url_for('static', filename=f'profile_pics/{user.profile_pic}')
    return render_template('account.html', title='account',
                           image_file=image_file, form=form, user=user)

Here is the form class that doesn’t work for the validator when an admin user is trying to edit another user’s account:

class AccountEditForm(FlaskForm):
    username = StringField('Username',
                           validators=[DataRequired(), Length(min=2, max=20)])
    first_name = StringField('First Name',
                             validators=[DataRequired(), Length(min=2, max=32)])
    last_name = StringField('Last Name',
                            validators=[DataRequired(), Length(min=2, max=32)])
    email = StringField('Email',
                        validators=[DataRequired(), Email()])
    profile_pic = FileField('Update Profile Picture',
                            validators=[FileAllowed(['jpg', 'png'])])
    role = SelectField('Role', validators=[DataRequired()],
                       choices=[('admin', 'Admin'), ('agent', 'Agent'), ('faculty', 'Faculty'),
                                ('staff', 'Staff'), ('student', 'Student')])
    submit = SubmitField('Update')

    # Custom validation to check for duplicate usernames
    def validate_username(self, username):
        # Check to see if the form data is different than the current db entry
        if username.data != username:
            # Query db for existing username matching the one submitted on the form
            user = User.query.filter_by(username=username.data).first()
            if user:
                raise ValidationError('Username is taken, please choose another.')

    # Custom validation to check for duplicate email
    def validate_email(self, email):
        # Check to see if the form data is different than the current db entry
        if email.data != user.email:
            # Query db for existing email matching the one submitted on the form
            user = User.query.filter_by(email=email.data).first()
            if user:
                raise ValidationError('Email is already used, please choose another or login.')

Here is the custom validator that works when the user is editing their own account from a different form class:

# Custom validation to check for duplicate usernames
def validate_username(self, username):
    # Check to see if the form data is different than the current db entry
    if username.data != current_user.username:
        # Query db for existing username matching the one submitted on the form
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('Username is taken, please choose another.')

# Custom validation to check for duplicate email
def validate_email(self, email):
    # Check to see if the form data is different than the current db entry
    if email.data != current_user.email:
        # Query db for existing email matching the one submitted on the form
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('Email is already used, please choose another or login.')
1
try to add a paragraph stating the problem you're facing. - Vishal Singh
Sure thing, I tried to edit to clarify the problem. - covrebo

1 Answers

0
votes

here is an example of passing object to the custom validator, and the scenario is exactly like yours:

class EditProfileAdminForm(EditProfileForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 254), Email()])
    role = SelectField('Role', coerce=int)
    active = BooleanField('Active')
    confirmed = BooleanField('Confirmed')
    submit = SubmitField()

    def __init__(self, user, *args, **kwargs):  # accept the object
        super(EditProfileAdminForm, self).__init__(*args, **kwargs)
        self.role.choices = [(role.id, role.name)
                             for role in Role.query.order_by(Role.name).all()]
        self.user = user  # set the object as class attr

    def validate_username(self, field):
        # use self.user.username to get the user's username
        if field.data != self.user.username and User.query.filter_by(username=field.data).first():
            raise ValidationError('The username is already in use.')

    def validate_email(self, field):
        if field.data != self.user.email and User.query.filter_by(email=field.data.lower()).first():
            raise ValidationError('The email is already in use.')


# view function
@admin_bp.route('/profile/<int:user_id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_profile_admin(user_id):
    user = User.query.get_or_404(user_id)
    form = EditProfileAdminForm(user=user)  # pass the object
    # ...

Example codes come from a photo-sharing application.