I want to allow anonymous users to post their comments in a blog without requiring them to login or sign up. One anonymous user can post several comments using the same credentials during subsequent visits. However, I get SQLAlchemy IntegrityError during a second attempt.
I have a simple form, a simple model and a basic route.
comment.py
class CommentForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
comment = TextAreaField('Comment', validators=[DataRequired()])
submit = SubmitField('Post')
models.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
posts = db.relationship('Post', backref='author', lazy='dynamic')
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(500))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
route.py
#app.route('/personal_blog', methods = ['GET', 'POST'])
def personal_blog():
form = CommentForm()
if form.validate_on_submit():
user = User(username = form.username.data, email = form.email.data)
post = Post(body = form.comment.data, author = user)
db.session.add(user)
db.session.add(post)
db.session.commit()
flash('Your comment is now live!')
return redirect(url_for('personal_blog', _anchor='translate-hover'))
posts = Post.query.order_by(Post.timestamp.desc()).all()
return render_template('personal_blog.html', form = form, posts = posts)
I have tried several suggested solutions and the closet I have come to was to add validation to the form, such as this:
class CommentForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
comment = TextAreaField('Comment', validators=[DataRequired()])
submit = SubmitField('Post')
def __init__(self, original_username, *args, **kwargs):
super(CommentForm, self).__init__(*args, **kwargs)
self.original_username = original_username
def validate_username(self, username):
if username.data != self.original_username:
user = User.query.filter_by(username=self.username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
This, however, needs the addition of #app.route('/personal_blog/<username>') which essentially does not work for my case. I want to have this anonymous user post subsequent comments without the need of changing either username or email.
Personally, I have tried to add validation within the route (if user.username is not None: flash('Use different credentials!')) to notify the user that the email or username they are trying to use has already been used, and it works, but this is not what I want the app to do. The user can simply use the very same credentials as before. How can this be achieved?
The solution to this is to remove the unique constraint from the User table.
Instead of:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True) <----------here
email = db.Column(db.String(120), index=True, unique=True) <----------here
posts = db.relationship('Post', backref='author', lazy='dynamic')
I have this:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True) <----------here
email = db.Column(db.String(120), index=True) <----------here
posts = db.relationship('Post', backref='author', lazy='dynamic')
As soon as I update the database, the changes take effect and I am able to allow a single anonymous user to post more than one comment using the same credentials.
Related
I have created a web app using Flask where you can leave a note as text. Ig goes well you leave a text and it got saved in our database. But it only happens when you are an authorized user and can't be done if you are an unauthorized (anonymous user) and if you attempt as such this error message pops up: "'AnonymousUserMixin' object has no attribute 'id'".
Here the code goes:
#home.py
def home():
if request.method == 'POST':
note = request.form.get('note')
if len(note) < 1:
flash('Note is too short!', category='error')
else:
new_note = Note(data=note, user_id=current_user.id)
db.session.add(new_note)
db.session.commit()
flash('Note added!', category='success')
return render_template("home.html", user=current_user)
And the DB models:
#models.py
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
data = db.Column(db.String(10000))
date = db.Column(db.DateTime(timezone=True), default=func.now())
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(150), unique=True)
password = db.Column(db.String(150))
first_name = db.Column(db.String(150))
confirmed = db.Column(db.Boolean, nullable=False, default=False)
notes = db.relationship('Note')
You are setting a user_id value when creating a Note:
new_note = Note(data=note, user_id=current_user.id)
Evidently anonymous users don't have an id attribute, so you must either set user_id=None when creating notes for anonymous users or create a default user to represent anonymous users and user that user's id when creating notes.
The def clean_clean email isn't working. This function should do two things as shown by two ifs. The first checks if no changes have been made to the field and if no changes have been made validation doesn't start, this currently work. The second if should check if the email entered already exists and if so present a validation error message. As the second if doesn't if an exisitng email is entered this leads to SQL IntegrityError.
Model.py - table
class User(db.Model, TimestampMixin, UserMixin):
__tablename__ = 'user'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), nullable=False, unique=True)
email = db.Column(db.String(80), nullable=False, unique=True)
password_hash = db.Column(db.String(128))
first_name = db.Column(db.String(20), nullable=False)
last_name = db.Column(db.String(20), nullable=False)
forms.py
class EditUserForm(FlaskForm):
first_name = StringField('First Name', validators=[DataRequired()])
last_name = StringField('Last Name', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
update = SubmitField('Update')
cancel = SubmitField('Cancel')
def clean_email(self):
cd = self.cleaned_data
email = cd(email=email.data)
# if email is not modified, so don't start validation flow
if self.instance.pk is not None and self.instance.email == email:
return cd
# check email is unique or not
if User.objects.filter(email=email).exists():
raise ValidationError("Email address already exists!".format(email))
return cd
SQL error when a existing email is entered
sqlite3.IntegrityError: UNIQUE constraint failed: user.email
In reference to this error listed:
sqlite3.IntegrityError: UNIQUE constraint failed: user.email
This means that you're trying to save a new entry into your 'user' table with an email that already exist. You must have the constraint in your models.py file that says something like
email = db.Column(db.String(120), unique=True, nullable=False)
If you want users to be able to save an email that's already being used by a different user, then you need to change this to...
email = db.Column(db.String(120), unique=False, nullable=False)
It's not clear to me what you're trying to do, but if you want to do a query to find if the email is in use by someone besides the current user, you could do something like this:
email_already_in_use_boolean = bool(db.session.query(User).filter(User.email==email.data).filter(User.id!=current_user_id).first())
I am using Flask as a Rest API for my WebApp.
In the frontend i use the User object quite often, which is why i need it from the backend to work with the user data.
My concern is, that the user object has an attribute password, which is obviously also sent to the frontend, when i make a request for a user object.
Should i define another class like UserPublic to send to the frontend and just strip out the password or is there a better way to do this with Flask, SQLAlchemy, Marshmallow?
I'm not sure if it's even a problem sending the password hash+salt to the frontend. I mean, i don't need it there, so why send it? Password check for login purposes is done in the backend anyway.
This is my User class:
class User(db.Model):
__tablename__ = 'user'
user_id = db.Column(db.Integer, primary_key=True,autoincrement=True, nullable=False)
public_id = db.Column(db.String, nullable=False)
fname = db.Column(db.String, nullable=False)
lname= db.Column(db.String, nullable=False)
bday = db.Column(db.Date, nullable=False)
street = db.Column(db.String, nullable=False)
zip = db.Column(db.String, nullable=False) #Zip used
city = db.Column(db.String, nullable=False)
country = db.Column(db.String, nullable=False, default='Germany')
password = db.Column(db.String, nullable=False)
admin = db.Column(db.Boolean, default=False)
email = db.Column(db.String, nullable=False)
iban = db.Column(db.String)
bic = db.Column(db.String)
gender = db.Column(db.CHAR, default='m', nullable=False)
created_by = db.Column(db.String)
updated_by = db.Column(db.String)
membership_status_id = db.Column(db.Integer, db.ForeignKey('membership_status.membership_status_id'))
member_since = db.Column(db.Date)
bookings = db.relationship('Booking', backref="User", lazy='select')
Marshmallow Schema:
class UserSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = User
include_fk = True
This is the endpoint to get a user object:
#app.route('/users/<public_id>', methods=['GET'])
#jwt_required
def get_user(public_id):
logger.info('Getting user with id: '+str(public_id))
current_user = User.query.filter_by(public_id=get_jwt_identity()).first()
if not current_user.admin:
return jsonify({'message' : 'Not privileged for this action'})
user = User.query.filter_by(public_id=public_id).first()
if not user:
return jsonify({'message' : 'No user found with id '+str(public_id)})
user_schema = UserSchema()
return user_schema.jsonify(user), 200
See doc about overriding generated fields.
Here's how to exclude the field from the auto-generated schema:
class UserSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = User
include_fk = True
exclude = ("password", )
# You may want to only exclude id on dump but keep it on load
# In this case, add it here by calling `auto_field` yourself
password = ma.auto_field(load_only=True)
I am trying to use flask-Security to change the password. Basically, there are no errors being thrown but the hash in my database is not updating. When creating the initial password, everything works fine.
However, when I try to update using the code in update.py the password isn't updating. I appreciate your insight. Thank you.
update.py
buyer = Buyers.query.filter_by(id=buyer_id).first()
buyer.password = generate_password_hash(form.password.data)
db.session.commit()
flash('Password has been updated')
models.py
class Buyers(db.Model, UserMixin):
__tablename__ = 'buyers'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True, nullable=False)
firstname = db.Column(db.String(64), index=True, nullable=False)
lastname = db.Column(db.String(64), index=True, nullable=False)
company = db.Column(db.Integer, db.ForeignKey('company.id'))
password_hash = db.Column(db.String(128))
def __init__(
self, email, firstname, lastname, company, password
):
self.email = email
self.firstname = firstname
self.lastname = lastname
self.company = company
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Not totally sure what you are trying to accomplish - but the code snippets have your User model with a field password_hash - not password. Flask-Security requires certain defined model names to work.
Not sure where you are getting check_password_hash() and generate_password_hash()....
The way Flask-Security updates password is:
user.password = hash_password(password)
_datastore.put(user)
_datastore.commit()
Oh - and I assume you are Flask-Security-Too (which is a maintained fork).....
I'm working on an edit user page for my flask app, and I can't seem to figure out how to render the user's current roles like I do other variables, such as email.
Here's my models, form, and view:
#Flask-Principal Role Model
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
def __repr__(self):
return '<Role %r>' % (self.name)
#SQLALchemy User Model
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
first_name = db.Column(db.String(128))
last_name = db.Column(db.String(128))
business_name = db.Column(db.String(128))
active = db.Column(db.Boolean())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
#WTForms User Form
class UserForm(Form):
first_name = StringField('first name', validators= [Required()])
last_name = StringField('last name', validators= [Required()])
business_name = StringField('business name', validators= [Required()])
email = StringField('email', validators = [Required(), Email()])
active = BooleanField('active')
roles = MultiCheckboxField('roles', coerce=int)
#Edit User View
#app.route('/admin/users/<id>/edit/', methods = ['GET', 'POST'])
#roles_required('admin')
def edit_user(id):
user = User.query.filter_by(id = id).first()
editform = UserForm()
# This is how I've assigned choices for other MultiCheckboxField forms, but I haven't
# needed to populate the MultiCheckboxField from a user model before with role objects.
editform.roles.choices = [(x.id,x.name) for x in Role.query.all()]
if editform.validate_on_submit():
pass
editform.first_name.data = user.first_name
editform.last_name.data = user.last_name
editform.business_name.data = user.business_name
editform.email.data = user.email
editform.active.data = user.active
#The below doesn't show all the available roles, just the current roles assigned.
editform.roles.data = user.roles
return render_template("user_edit.html",
title = "Edit User",
user = user,
editform = editform)
So then, does anyone know how to have WTForms display all the Roles available, and have checked the ones which are currently in the user.roles list?
data for a multi-select field is a list of form_data after it has been coerced by your coerce callable - so what you provide needs to match what's available as the first entry in each tuple you provide to choices - in this case, an integer. That said, if you change your code to:
editform.roles.data = [role.id for role in user.roles]
you should see all the appropriate checkboxes checked.