I can't get validation error to be displayed, only IntegrityError from SQLAlchemy
(sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: Booking.username).
I have two tables in DB, one is a list of registered users, another one is a list of logged in users where they can book time thru FlaskForm with RadioFields. I think I have mistake in this function def validate_booking (self)
I need to check if the current_user already booked time then he cannot do another booking
I moved validation function into LoginForm instead and this seems to
be working. It validates before the user jumps into next booking page.
Not exactly how I wanted having the validation in booking page though.
models.py
class User(db.Model, UserMixin):
__tablename__ = 'Employees'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
code = db.Column(db.String(20), nullable=False)
def __repr__(self):
return f"User('{self.username}', '{self.code}')"
class Book(db.Model, UserMixin):
__tablename__ = 'Booking'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
choice = db.Column(db.String(30), nullable=False)
def __repr__(self):
return f"Book('{self.username}, '{self.choice}')"
forms.py
class LoginForm(FlaskForm):
username = StringField('Name', validators=[DataRequired(), Length(min=2, max=20)])
code = StringField('Code', validators=[DataRequired()])
submit = SubmitField('Book time')
def validate_username(self, username):
user = Book.query.filter_by(username=username.data).first()
if user:
raise ValidationError('You have registered your car today')
class BookingForm(FlaskForm):
book = RadioField('Label', choices=[('Station_1_morning', '07:00-11:00'), ('Station_1_afternoon', '11:00-15:00'),
('Station_2_morning', '07:00-11:00'), ('Station_2_afternoon', '11:00-15:00'),
('Station_3_morning', '07:00-11:00'), ('Station_3_afternoon', '11:00-15:00')],
coerce=str, validators=[InputRequired()])
submit = SubmitField('Register time')
routes.py
#app.route("/booking", methods=['POST', 'GET'])
#login_required
def booking():
session.permanent = True
app.permanent_session_lifetime = timedelta(seconds=5)
form = BookingForm()
if form.validate_on_submit():
book = Book(username=current_user.username, choice=form.book.data)
db.session.add(book)
db.session.commit()
flash('Your time is registered', 'success')
return render_template('booking.html', title='Booking', form=form)
I don't see the error. You could add a print (user) in the validation function to see what's in there.
Anyway this is still open to a race condition: if the same user books in another request between the check ("validation")and the commit. As a general rule, I'd rather try to commit and catch the integrity error. It can be a bit tricky to build a meaningful message from an integrity error exception object (I mean get the name of the offending field(s) from the object). Of course if you know for sure only one constraint applies, you may hardcode the message.
Related
This is my models.py where I made my User table.
from datetime import datetime
from flaskblog import db, login_manager
from flask_login import UserMixin
#login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
posts = db.relationship('PostIntmath', backref='author', lazy=True)
image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
password = db.Column(db.String(60), nullable=False)
def __repr__(self):
return f"User('{self.username}', '{self.email}', '{self.image_file}')"
class PostIntmath(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
content = db.Column(db.Text, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def __repr__(self):
return f"Post('{self.title}', '{self.date_posted}')"
I don't see what is wrong.
I have made forms to take inputs for the username and email.
My routes.py seems fine as well but whenever I try to log in it gives me an error
#app.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first() #error on this line
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('home'))
else:
flash('Login Unsuccessful. Please check email and password', 'danger')
return render_template('login.html', title='Login', form=form)
The error I get is:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: user
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.image_file AS user_image_file, user.password AS user_password
FROM user
WHERE user.email = ?
LIMIT ? OFFSET ?]
[parameters: ('abc456#gmail.com', 1, 0)]
(Background on this error at: http://sqlalche.me/e/e3q8)
Please help me out. I am new to flask and SQLAlchemy
write this in your code:
#app.before_first_request
def create_tables():
db.create_all()
Make sure to check the following 3 things:
Table is created with proper name.
Table imported from model to routes.py
from app.models import User
Table name added in 'init' file or file having shell_content_processor. You should have following code present.
from app import app, db
from app.models import User
#app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User}
Change the name of app with your app name.
My site is essentially a blog site -- a user uploads a post and each post has tags that categorize it. I build the site using a SQlite db and when I switched to Postgres I started getting this error when uploading a new post:
sqlalchemy.exc.DataError: (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(psycopg2.errors.StringDataRightTruncation) value too long for type character varying(20)
#posts.route('/post/new', methods=['GET', 'POST'])
#login_required
def new_post():
form = PostForm()
if form.validate_on_submit():
post = Post(title=form.title.data, description=form.description.data, author=current_user)
if form.notebook.data:
picture_file = save_notebook(form.notebook.data)#set user profile picture
post.notebook_file = picture_file
#Save tag data into database
for tag in form.tags.data:
post_tag = add_tags(tag)
post.tags.append(post_tag)
# ADDING NOTEBOOK HTML TO POST AS STRING
notebook_path_str = url_for('static',
filename='notebooks/' + picture_file) # STRING (src="{{ notebook }}")
notebook_html_str = open('/Users/colestriler/coding/websites/Flask_Blog/flaskapp' + notebook_path_str)
soup = BeautifulSoup(notebook_html_str, 'html.parser')
post.notebook_html = str(soup.body.contents[1]) # findChildren() removes body tags
db.session.add(post)
db.session.commit()
print(post.tags)
flash('Your post has been created!', 'success')
return redirect(url_for('main.home'))
return render_template('create_post.html', title='New Post', form=form, legend='New Post')
def add_tags(tag):
existing_tag = Tags.query.filter_by(name = tag.lower()).one_or_none()
if existing_tag is not None:
return existing_tag
else:
new_tag = Tags(name=tag.lower())
return new_tag
I suspect the problem might be in add_tags() or in db.session.commit().
Here is the Post & Tags model for reference:
class Post(db.Model): #one-to-many relationship because 1 user can have multiple posts, but post can have 1 author
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=False)
date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) #pass in function as argument (utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
notebook_file = db.Column(db.String(20), nullable=False, default='default.ipynb') # hash unique image files each 20 chars long
notebook_type = db.Column(db.String(20), nullable=False, default='Jupyter Notebook')
notebook_html = db.Column(db.Text, nullable=False, default='No Notebook File')
tags = db.relationship('Tags', secondary=relationship_table, backref=db.backref('posts', lazy='dynamic'))
def __repr__(self):
return f"Post('{self.title}', '{self.date_posted}')"
class Tags(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True, nullable=False)
description = db.Column(db.Text)
I cannot figure out why my code is giving me this DataError. Any pointers would be greatly appreciated!
You may need to update your notebook_file and notebook_type to be of type db.Text, unless you really need the constraint (in which case you can add a CHECK constraint to your database. Also, varchar(N) is often not recommended (there are many other similar blog articles). Also, in SQLite, varchar(N) is not really enforced, which may explain why you were able to get away with no errors previously.
Otherwise, please update your original post with proof that you are getting the error message while not attempting to enter a notebook_file or notebook_type with greater than 20 chars.
Disclosure: I work for EnterpriseDB (EDB)
hope all is well. I'm having difficulty trying to figure out how to write this db functionality with flask and SQLALCHEMY.
I want to be able to register a user with a site that will already be existing in the database.
When registering them I want the route to be able to assign that user to the site model in the db. Reason I want to do this is so I can later send a message to all users connected to a particular site, or a message to all users for all sites.
This is my User and Site models currently:
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
site = db.Column(db.String())
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(100), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
adminstatus = db.Column(db.Boolean)
user_data = db.relationship('Rma', backref='userdata', lazy=True)
# sites = db.relationship('Sites', secondary=usertosite, backref=db.backref('sites', lazy='dynamic'))
def __repr__(self):
return f"User('{self.username}, '{self.email}')"
class Sites(db.Model):
id = db.Column(db.Integer, primary_key=True)
sitename = db.Column(db.String(), nullable=False)
contractstart = db.Column(db.String(), nullable=False)
contractend = db.Column(db.String(), nullable = False)
hwkey = db.Column(db.String(), nullable=False)
stations = db.Column(db.String(), nullable=False)
printers = db.Column(db.String(), nullable = False)
remprinters = db.Column(db.String(), nullable = False)
bof = db.Column(db.Boolean())
processor = db.Column(db.String(), nullable = False)
giftopt = db.Column(db.String(), nullable = False)
Here is my register form
# REGISTER NEW USER
#app.route('/register', methods=['POST', 'GET'])
#login_required
def register():
form = RegistrationForm()
if form.validate_on_submit():
hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user = User(site = form.site.data, username = form.username.data, email = form.email.data, password = hashed_pw, adminstatus= form.admin_status.data)
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=form)
I'm not sure how I can do this considering usually when I add a form to a database I add all of the form elements to the specific model. If I try a one to many relationship I would have to assign the Sites model with all of its elements along with the particular user which is not Ideal because the site will already be created in db. I'm a super noob and I'm probably missing some steps but please assist if possible. Thanks guys.
You need to use a foreign key.
class User(db.Model, UserMixin):
...
site = db.Column(db.Integer, db.ForeignKey('sites.id'))
Then when you enter User information into the database you supply the primary key id of the site that exists as a Sites. You can either do this in your route or provide an initialisation override function that does this based on the keyword, for example:
class User(..):
...
def __init__(self, **kwargs):
if 'site' in kwargs:
site_id = db.session.query(Sites).filter(Sites.sitename == kwargs['site']).one().id
kwargs['site'] = site_id
super().__init__(**kwargs)
Note that I don't think mySQL or SQLite enforces foreign key consistency by default, which personally I find annoying meaning you can get NULL entries in yoru database when you might not expect them, however POSTGRES does enforce it I believe. There are things you can do in sqlalchemy to enforce foreign key consistency in mySQL and SQLlite, however.
I am stumped on testing a POST to add a category to the database where I've used Flask_WTF for validation and CSRF protection. For the CRUD operations pm my website. I've used Flask, Flask_WTF and Flask-SQLAlchemy. It is my first independent project, and I find myself a little at a lost on how to test the Flask-WTForm validate_on_submit function.
Here's are the models:
class Users(db.Model):
id = db.Column(db.Integer, primary_key=True, unique=True)
name = db.Column(db.String(80), nullable=False)
email = db.Column(db.String(250), unique=True)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True, unique=True)
name = db.Column(db.String(250), nullable=False, unique=True)
users_id = db.Column(db.Integer, db.ForeignKey('users.id'))
Here's the form:
class CategoryForm(Form):
name = StringField(
'Name', [validators.Length(min=4, max=250, message="name problem")])
And here's the controller:
#category.route('/category/add', methods=['GET', 'POST'])
#login_required
def addCategory():
""" Add a new category.
Returns: Redirect Home.
"""
# Initiate the form.
form = CategoryForm()
# On POST of a valid form, add the new category.
if form.validate_on_submit():
category = Category(
form.name.data, login_session['users_id'])
db.session.add(category)
db.session.commit()
flash('New Category %s Successfully Created' % category.name)
return redirect(url_for('category.showHome'))
else:
# Render the form to add the category.
return render_template('newCategory.html', form=form)
How do I write a test for the if statement with the validate_on_submit function?
You should have different configurations for your app, depending if you are local / in production / executing unit tests. One configuration you can set is
WTF_CSRF_ENABLED = False
See flask-wtforms documentation.
Using py.test and a conftest.py recommended by Delightful testing with pytest and SQLAlchemy, here's a test that confirms the added category.
def test_add_category_post(app, session):
"""Does add category post a new category?"""
TESTEMAIL = "test#test.org"
TESTUSER = "Joe Test"
user = Users.query.filter(Users.email==TESTEMAIL).first()
category = Category(name="Added Category", users_id=user.id)
form = CategoryForm(formdata=None, obj=category)
with app.test_client() as c:
with c.session_transaction() as sess:
sess['email'] = TESTEMAIL
sess['username'] = TESTUSER
sess['users_id'] = user.id
response = c.post(
'/category/add', data=form.data, follow_redirects=True)
assert response.status_code == 200
added_category = Category.query.filter(
Category.name=="Added Category").first()
assert added_category
session.delete(added_category)
session.commit()
Note that the new category is assigned to a variable and then used to create a form. The form's data is used in the post.
Working on the comments of #mas I got to this solution which worked for me:
topic_name = "test_topic"
response = fixt_client_logged_in.post('/create', data={"value":topic_name}, follow_redirects=True)
I am using this form class:
class SimpleSubmitForm(FlaskForm):
value = StringField(validators=[DataRequired()])
submit = SubmitField()
In this html file:
{{form.hidden_tag()}}
{{form.value.label("Topic", class="form-label")}}
{{form.value(value=topic_name, class="form-control")}}
<br/>
{{form.submit(value="submit", class="btn btn-primary")}}
Note that I am using the hidden_tag for the CSRF security, however when testing I have this extra line that de-activates it:
app.config['WTF_CSRF_ENABLED']=False
I have no idea how it actually works under the hood but my hypothesis is this: The wtform FlaskForm object looks at the "data" attribute of the request, which should be a dict. It then looks for keys in that dict that have the same name as its attributes. If it finds a key with the same name then it assigns that value to its attribute.
My app fails whenever I call login_user with the error NotImplementedError: No 'id' attribute - override 'get_id'. My user has an id attribute. Why does this fail?
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
print(user.user_id)
login_user(user, False)
return jsonify({'response': user.user_id})
class User(UserMixin, db.Model):
__tablename__ = 'users'
user_id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
You just need to add a get_id() function in order to override the default properties of get_id() under the User class in the models.py file where your database schema is defined.
class User(#...):
# ...
def get_id(self):
return (self.user_id)
# ...
login_user calls get_id on the user instance. UserMixin provides a get_id method that returns the id attribute or raises an exception. You did not define an id attribute, you named it (redundantly) user_id. Name your attribute id (preferably), or override get_id to return user_id.
I have no idea how you make return User.get_id(id) to work. In my case User model is defined as:
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
but load_user would still get error:
AttributeError: type object 'User' has no attribute 'get'
Quite a few tutorials online including RealPython implementation deviates from documentation, but worked for me:
#login_manager.user_loader
def load_user(id):
return User.query.get(int(id))