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)
Related
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.
I am working on a Flask app, using Flask-SQLAlchemy extension for database interactions. Since I have multiple apps writing on the same DB, I was getting concurrency issues with SQLite and I wanted to switch to PostgreSQL instead. I am able to create the tables on new database without a problem and pgAdmin displays the tables and columns.
# working
def createTables():
with app.app_context():
from models import User, Invoice
db.create_all()
But when it comes to adding a user, I am now getting an error: sqlalchemy.exc.NoForeignKeysError Although, I think, I declared one-to-many relationship in my models, based on the documentation, I get an error states that "there are no foreign keys linking these tables."
# not working
def create_test_user():
with app.app_context():
user = User(
username="Bob",
email="bob#email.com",
password="test"
)
db.session.add(user)
db.session.commit()
The full error message:
""" NoForeignKeysError: Could not determine join condition between parent/child tables on relationship User.invoices
- there are no foreign keys linking these tables.
Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression. """
I can't figure out what causes the error. What is missing with my models?
# models.py
class User(db.Model):
__tablename__ = "user"
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)
password = db.Column(db.String(60), nullable=False)
invoices = db.relationship('Invoice', backref='user', lazy=True)
class Invoice(db.Model):
__tablename__ = "invoice"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
amount = db.Column(db.Integer, nullable=False)
Solved
Your code works for me. Maybe you need to re-create your tables or something similar. To be sure that we have the identical code: I have tested the following code:
class User(db.Model):
__tablename__ = "user"
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)
password = db.Column(db.String(60), nullable=False)
invoices = db.relationship('Invoice', backref='user', lazy=True)
class Invoice(db.Model):
__tablename__ = "invoice"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
amount = db.Column(db.Integer, nullable=False)
In the route:
user = User(
username="Bob",
email="bob#email.com",
password="test"
)
db.session.add(user)
db.session.commit()
print(user)
I finally solved the problem and it was not where I was looking for. I was getting NoForeignKeysError due to importing a wrong model file during initializing the app. One of my imported modules was calling a wrong/old version of the model. It was causing the table relationship in the actual model to break I guess.
When I went through step by step create_test_user() I noticed that the error occurs actually during the class creation, before even it hits to db.session.add and I replicated the error even without a DB. I went through all my modules that are calling the models and caught wrong model import.
I'm building a flask application using SqlAlchemy and I'd like to retrieve the author from a Notification object from the Notification object itself, rather then parsing all Users to a template in order to capture the author's data.
Here's my models:
In app/models.py
from app import db
from datetime import datetime
#...
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(250), unique=True, index=True)
password_hash = db.Column(db.Binary(72))
notifications = db.relationship('Notification', backref='recipient', lazy='dynamic')
#...
class Notification(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
pending = db.Column(db.Boolean, default=True)
author = ?
In my routes, I always parse the notifications from a current_user as an argument, as I use it in my templates/base.html navbar (and all my views {% extends "base.html %})
Here's one route example:
In app/routes.py
#app.route('/')
#app.route('/index')
def index():
notifications = User.query.filter_by(id=current_user.id).first().notifications.all()
else:
notifications = []
return(render_template('index.html', title=_('Home'), notifications=notifications))
But, I don't want to parse all the Users from the database in order to get the author information from each notification, instead I want to access the author User object in my template from the Notification object itself, like so:
In app/templates/index.html
{{ notification.author.username }}
And that should return its username.
What kind of relationship or trick would I have to do to achieve that?
P.S: Would that be in any way insecure as a user (a notification recipient) could somehow exploit it to access unwanted data from the author, such as nothification.author.password_hash for example? If so, what would be the best way to implement what I'm trying to?
Edit
To clarify the question, as pointed by #noslenkwah, I'd like to make it clear that the author is a User and I hope that I can create a Notification object by assigning things as follows:
notification = Notification(body='example', user_id=some_recipient_id, author = User.query.filter_by(username=some_author_username).first())
I could then db.session.add(notification) and finally db.session.commit() it.
some_recipient_id is the id of a User, being an unique identifier and its primary key and some_author_username is also a unique identifier.
I could also filter_by(id=some_author_id), whereas some_author_id would be a User's primary key and also a unique identifier.
Thanks to #noslenkwah pointing to this question, I was able to find a way to accomplish what I was trying to.
Here's how:
in models.py
from app import db
from sqlalchemy.orm import backref
from datetime import datetime
#...
class Notification(db.Model):
__tablename__ = 'notification'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
pending = db.Column(db.Boolean, default=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
recipient = db.relationship(User, foreign_keys=[user_id],
backref=backref('notifications', order_by=id, lazy='dynamic'))
author = db.relationship(User, foreign_keys=[author_id])
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.