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.')
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.
Related
I am a beginner in Django an am using version 2.2 .I created a user form to sign a user in the site but it cant add other field information to the database
I have tried adding other fields in the fields list add adding fields but nothing works`
forms.py
from django import forms
from django.contrib.auth import (
authenticate,
get_user_model
)
User = get_user_model()
class UserRegisterForm(forms.ModelForm):
username = forms.CharField(label='PUsername')
email = forms.EmailField(label='Email address')
email2 = forms.EmailField(label='Confirm Email')
password = forms.CharField(widget=forms.PasswordInput,label='Password')
password2 = forms.CharField(widget=forms.PasswordInput,label='ConfirmPassword')
age = forms.CharField(label='your age')
info = forms.CharField(label='info about you')
class Meta:
model = User
fields = [
'username',
'email',
'email2',
'password',
'password2',
'age'
'info'
]
def clean(self, *args, **kwargs):
username = self.cleaned_data.get('email')
email = self.cleaned_data.get('email')
email2 = self.cleaned_data.get('email2')
password = self.cleaned_data.get('password')
password2 = self.cleaned_data.get('password2')
if email != email2:
raise forms.ValidationError("Emails must match")
email_qs = User.objects.filter(email=email)
if password != password2:
raise forms.ValidationError("Passwords must match")
email_qs = User.objects.filter(email=email)
if email_qs.exists():
raise forms.ValidationError(
"This email has already been registered")
username_ex = User.objects.filter(username=username)
if username_ex.exists():
raise forms.ValidationError("This username is taken")
return super(UserRegisterForm, self).clean(*args, **kwargs)
views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate,get_user_model,login,logout
from .forms import CreateUserForms
import random
import string
def register_view(request):
if request.method=='POST':
frm=CreateUserForm(request.POST)
if frm.is_valid():
username, email, password = frm.cleaned_data['username'], frm.cleaned_data['email'], frm.cleaned_data['password1']
new_user = User.objects.create_user(username, email, password)
new_user.is_active = True # if you want to set active
new_user.save()
else:
frm=CreateUserForm()
return render(request,'registration/signup.html',{'form':frm})
def logout_view(request):
logout(request)
return redirect('/')
Expected results are the working form,but actual results are only saving the username, password and email
read the documentation on python super . you are using it in wrong way. calling super at last of the clean method mean it it execute the modelform clean method and code before super get modified .
The title of your question is
How can i create custom users with only forms?
The answer to that is: That's not possible. Whatever you want to save to the database, you have to have a corresponding model for it. Forms only determine what data is submitted by the clients and how that data is processed.
So if you want to save columns age and info for your users, you'll have to create a Model and the corresponding table and columns in your database to hold these values.
Now, the default Django User model only takes username, email and password (plus some booleans like is_staff, is_superuser, is_active etc...). If you want to save additional information, you have a few options that are well described in the official docs, either by extending the default User model or by substituting it.
The recommendation when starting a new Django project is to always at least substitute with your own User model (see this). If you will only ever have one type of user, then add the age and info fields on your custom User model. But if there's any chance that you might have different types of users where age and info might not be relevant in the future, extend your custom User model with a profile.
I'm creating a part of my application in which an admin can register a user. I have a registration form in which one of the elements of the form is a QuerySelectField that gets a list of locations from db that the user can be linked too. I'm able to display the QuerySelectField with the correct information but upon submitting the form I get an error
Sqlalchemy.exc.ArgumentError: Object "Site" is not legal as a SQL literal value
This error populates when trying to query the database in my form Sites Model to find the name of the selected option in the QuerySelectField so that I can then store the ID of the particular site within a variable for further use.
I'm not sure how to make the object a literal value that sqlalchemy can read. I tried wrapping it in a str() but that didn't do a thing. I'm a noob by the way so I'm still trying to get a grasp on certain elements of flask.
This is my Form for what I'm trying to accomplish
def site():
return Sites.query.all()
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(),
Length(min=2, max=20)])
email = StringField('Email', validators=
[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
confirm_pass = PasswordField('Confirm Password', validators=
[DataRequired(), EqualTo('password')])
admin_status = BooleanField('Check for Admin Status')
sitechoices = QuerySelectField(query_factory=site, allow_blank=False,
get_label='sitename')
Here is the route info in my routes.py
#app.route('/register', methods=['POST', 'GET'])
#login_required
def register():
forms = RegistrationForm()
if forms.validate_on_submit():
hashed_pw = bcrypt.generate_password_hash(forms.password.data).decode('utf-8')
siteid = Sites.query.filter_by(sitename=forms.sitechoices.data).first().id
user = User(site = forms.sitechoices.data, username = forms.username.data, email = forms.email.data,
password = hashed_pw, adminstatus= forms.admin_status.data, sitelink=siteid)
db.create_all()
db.session.add(user)
db.session.commit()
flash(f"{form.username.data} has been added!")
return redirect(url_for('dash'))
return render_template('register.html', name = 'login', form=forms)
Here is the template info that is needed:
<div class="form-group">
{{form.sitechoices}}
</div>
I would like to retrieve site names from model within queryselectfield and be able to register a user with that selected option.
In a queryselectfield the object is returned, so what you need to use is form.sitechoices.data.sitename to save a submitted form
I use Flask, Blueprint, WTForms 3.0, SQLAlchemy, Bootstrap 4 - and the following syntax works for me ...
FORMS.py
class IndicatorForm(FlaskForm):
class Meta:
csrf = False
ind_id = IntegerField('indid', id='indid')
topic = QuerySelectField('Topic',query_factory=lambda: Indicator.query.group_by("topic"), allow_blank=True, blank_text=u'Topic', get_pk=lambda a: a.id, get_label='topic')
subtopic1 = QuerySelectField('SubTopic',query_factory=lambda: Indicator.query.group_by("subtopic1"), allow_blank=True, blank_text=u'SubTopic', get_pk=lambda a: a.id, get_label='subtopic1')
routes.py
#blueprint.route('/newind_edit', methods=['GET','POST'])
#login_required
def newind_edit():
form = IndicatorForm(request.form)
if request.method == "POST" and form.validate():
indid = request.form['ind_id']
Indicator.query.filter_by(id=indid).update(dict(
id=ind_id,
topic = form.topic.name.topic, # <== this is the selected value
subtopic1 = form.subtopic1.name.subtopic1 # <== add as many pull-downs as you like
))
db.session.commit()
This will be an easy question for someone out there. I searched around quite a bit and if there's a thread that addresses this perfectly, please direct me and close this out. I'm building a very simple web form within our organization's website using Django.
In forms.py I've got a class for the login page:
class userLogin(forms.Form):
user = forms.CharField(label = 'Username ', max_length = 25)
pwd = forms.CharField(label = 'Password ', widget = forms.PasswordInput)
def clean(self):
cleaned_data = super(userLogin, self).clean()
user = cleaned_data.get("user")
pwd = cleaned_data.get("pwd")
if not fs.authenticateUser(user, pwd):
raise forms.ValidationError("Invalid password!")
I have to authenticate against our website's API, which is what the fs.authenticate bit is about; it just returns True or False. In views.py I do this:
if request.method == 'POST':
user_form = userLogin(data = request.POST)
if user_form.is_valid():
return redirect('PickupAuthorization/main')
else:
pass
The authentication and redirection to the main page works splendidly, and the login form throws an invalid password message as expected when the credential is incorrect. What I want to know is how to make "user" available to views.py. If I try to reference user_form.user in the view, Python tells me that user is not an attribute of userLogin!
I haven't done much with OOP, so I'm imagining that this has a simple answer, but I can't freakin' find it. Thanks!
EDIT: I have had to shelve this project for the moment in favor of more pressing matters, but I will update it when I have a solution that works properly.
You need to use authenticate method provided by Django in order to get the user. The authenticate method returns a user if it finds one.
To authenticate a given username and password, use authenticate(). It takes two keyword arguments, username and password, and it returns a User object if the password is valid for the given username. If the password is invalid, authenticate() returns None:
Generally the flow using authenticate goes like this:
forms.py
class userLogin(forms.Form):
user = forms.CharField(label = 'Username ', max_length = 25)
pwd = forms.CharField(label = 'Password ', widget = forms.PasswordInput)
def __init__(self, *args, **kwargs):
self.user_cache = None # You need to make the user as None initially
super(LoginForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(userLogin, self).clean()
user = cleaned_data.get("user")
pwd = cleaned_data.get("pwd")
# Now you get the user
self.user_cache = authenticate(username=user, password=pwd)
# Do other stuff
return self.cleaned_data
# Function to return user in views
def get_user(self):
return self.user_cache
Views.py
if request.method == 'POST':
user_form = userLogin(data = request.POST)
# You can now get user using the get_user method
user = user_form.get_user()
# Do other stuff
Another thing I'd like to add is if your user has already logged in you can simply do request.user.username in your views.py to get the current user.
I am currently learning Flask through Miguel Grinberg's book. If you are familiar you might know Flasky (An application that Miguel use during the book)
I am currently in section 8, dealing with Password Reset, here the original code (you can also find it on the repo. it's tag 8g):
models.py
class User(UserMixin, db.Model):
__tablename__ = 'users'
...
def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id})
def reset_password(self, token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('reset') != self.id:
return False
self.password = new_password
db.session.add(self)
return True
auth/views.py
#auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
if not current_user.is_anonymous:
return redirect(url_for('main.index'))
form = PasswordResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None:
return redirect(url_for('main.index'))
if user.reset_password(token, form.password.data):
flash('Your password has been updated.')
return redirect(url_for('auth.login'))
else:
return redirect(url_for('main.index'))
return render_template('auth/reset_password.html', form=form)
auth/forms.py
class PasswordResetForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
password = PasswordField('New Password', validators=[
Required(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Reset Password')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first() is None:
raise ValidationError('Unknown email address.')
My question:
I don't want to ask the user for their email again since they are changing the password through an email they have received. Is there a way to get the user or user's email from that token?
For 1— On a security level, it can be nice advantage to your users to hide who has accounts with your site. for instance, lets say it was a Addicts Anonymous site, if I wanted to see if alice#example.com was a member, I could simply try a password reset to confirm she's a member.
Alternatively if you had a large collection of email addresses, you could use that password reset form to narrow down the list to active members for use in a more targeted social engineering attack, or at least narrow the list down if you're aiming to brute-force them.
Alright so in case this could be useful for someone else. The user info is already in the token, in the {'reset': user_id}.
The problem is that token management logic is in User model. So having an email field in the form for later finding that user in the view does the trick in the current version
Since you get the token in this view we can move that logic to the view:
auth/views.py
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
#auth.route('/reset/<token>', methods=['GET', 'POST'])
def reset_password(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
raise ValidationError()
user_id = data['reset']
....
And in User model, we need to modify the reset_password() method:
models.py
class User(UserMixin, db.Model):
__tablename__ = 'users'
...
def reset_password(self, new_password):
self.password = new_password
db.session.add(self)
return True
I'm creating a registration form for my site.
I'm going with just the standard field entries of username, email, password in my User object.
The database tables are already created.
Models.py
class RegistrationForm(ModelForm):
class Meta:
model = User
views.py
def Registration(request):
RegForm = RegistrationForm(request.POST or None)
if request.method == 'POST':
if RegForm.is_valid():
newUser = User.objects.create_user(username, email, password)
RegForm.save()
try:
return HttpResponseRedirect('/Newuser/?userNm=' + clearUserName)
except:
raise ValidationError(('Invalid request'), code='300') ## [ TODO ]: add a custom error page here.
My question is, how do I represent these fields:
username, email, password
in this line (from the view):
newUser = User.objects.create_user(username, email, password)
that are part of the RegistrationForm() in models.py ?
Additionally: How do I properly represent this default model (User) in the modelform in forms.py?
To answer your second question:
Use UserCreationForm to represent registration process for User class, it handles validation and saving for you and you can always extend it (i.e. to add email field)
User class has a lot of related forms, depending of the use case: authentication, registration, password change etc. read the docs on authentication to find more.
https://docs.djangoproject.com/en/1.5/topics/auth/default/#module-django.contrib.auth.forms
You can get the validated data from the form using cleaned_date:
username = form.cleaned_data['username']
email = form.cleaned_data['email']
password = form.cleaned_data['password']
newUser = User.objects.create_user(username, email, password)
Now I think your approach has serious problems since you are saving the form again after creating the user object, also the User object expects the password field to be a hash of the password and some meta data, etc.
I highly recommend you use django-registration (or at least have a look at it) which handles all this stuff for you in addition to email validation, all you need is to provide the templates(and also you can find some templates for it like this )