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()
Related
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.
I have an application in django 1.11 where I use django-tenant-schemas (https://github.com/bernardopires/django-tenant-schemas) to create an account for the user.
After creating a client and schema and domain_url, the user is not redirected to the address given in domain_url.
For example: I have domain_url = test.localhost in the form.
After creating an account, I am still on localhost instead of test.localhost.
When I go to test.localhost I get a login panel. I log in with the data I provided when creating, but I get a message to enter the correct data. I check the database using shell - the user exists.
The user is connected to the Company using ForeignKey.
accounts/view.py
def signup(request):
if request.method == 'POST':
company_form = CompanyForm(request.POST, prefix='company')
user_form = SignUpForm(request.POST, prefix='user')
if company_form.is_valid() and user_form.is_valid():
company_form.instance.name = company_form.cleaned_data['name']
company_form.instance.domain_url = company_form.cleaned_data['name'] + '.localhost'
company_form.instance.schema_name = company_form.cleaned_data['name']
company = company_form.save()
user_form.instance.company = company
user = user_form.save()
auth_login(request, user)
return HttpResponseRedirect(reverse('post:post_list'))
else:
company_form = CompanyForm(prefix='company')
user_form = SignUpForm(prefix='user')
args = {}
args.update(csrf(request))
args['company_form'] = company_form
args['user_form'] = user_form
return render(request, 'accounts/signup.html', args)
Forms to create company and user:
class CompanyForm(forms.ModelForm):
name = forms.CharField(label='Company', widget=forms.TextInput(attrs={'autofocus': 'autofocus'}))
class Meta:
model = Company
fields = ('name',)
class SignUpForm(UserCreationForm):
email = forms.EmailField(max_length=254, required=True, widget=forms.EmailInput())
class Meta:
model = User
exclude = ('company', )
fields = ('email', 'password1', 'password2', )
re: User doesn't exist error -
According to your signup view function, the user record is getting created in the public schema. But when you try to login to test.localhost, test schema is being used for all the DB queries. You can switch schemas like -
from tenant_schemas.utils import schema_context
with schema_context(customer.schema_name):
# create your user here.
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 writing a Django website. I have a form like this :
class LoginForm(forms.Form):
user_name = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
and I like to do something like this :
def worker_login(request):
form = LoginForm(request)
if !form.is_valid():
form['user_name'].errors.append(u'This username is not registered.')
return render(request, 'WorkerLogin.html',{'form':form})
but no errors are added to the errorDict of the form, how should I append an error to an errorDict?
You can add validation error to a form in Django 1.5/1.6 like this:
form._errors["user_name"] = form.error_class([u'This username is registered.'])
You can find an example in Django docs using validation in practice section.
I had password change form like below
forms.py
class PasswordChangeForm(forms.Form):
old_password = forms.CharField(widget=forms.PasswordInput())
new_password = forms.CharField(widget=forms.PasswordInput())
confirm_password = forms.CharField(widget=forms.PasswordInput())
def clean(self):
if self.cleaned_data['new_password'] != self.cleaned_data['confirm_password']:
raise forms.ValidationError(_('The new passwords must be same'))
else:
return self.cleaned_data
template.html
<form action="/save/data/" method="post">
<div>
<p>{{form.old_password}}</p>
<span>{{form.old_password.errors}}</span>
</div>
<div>
<p>{{form.new_password}}</p>
<span>{{form.new_password.errors}}</span>
</div>
<div>
<p>{{form.confirm_password}}</p>
<span>{{form.confirm_password.errors}}</span>
</div>
</form>
views.py
#login_required
def change_password(request):
user_obj = User.objects.get(id=request.user.id)
form = PasswordChangeForm()
if request.method=="POST":
form = PasswordChangeForm(reques.POST)
#########
Here in this part i need to check if the user given old passoword
matched the already saved password in the database, create a password
with the user given new password
#########
new_password = form.cleaned_data['new_password']
......
user_obj.password = new_password
..........
return render_to_response('template.html',{'form':form})
So in the above code, how can we check the password saved in the database with the old password given by the user ?, Also how can we create the new password and sve in to the database ?
After that send an email to user, that ur password has been changed successfully
You have the user object. So you can just call it's set_password method.
request.user.set_password(password)
Also, you don't need to get the user again from the database. You're making an unnecessary DB request. request.user is the user.
I would rewrite the entire view like so,
from django.shortcuts import render
#login_required
def change_password(request):
form = PasswordChangeForm(request.POST or None)
if form.is_valid()
if request.user.check_password(form.cleaned_data['old_password']):
request.user.set_password(form.cleaned_data['new_password'])
request.user.save()
return render(request, 'success.html')
return render(request, 'template.html', {'form':form})
This means that if there is POST data you initialise the form with it. Otherwise it gets None. If the form is valid (which an empty form never will be) then you do the password change and send them to a success page. Otherwise you return the empty form (or the form with the validation errors) to the user.
you can check via check_password method.
if request.user.check_password(form.cleaned_data['old_password']):
request.user.set_password(form.cleaned_data['new_password'])
request.user.save()