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.
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 have a SQLALchemy table:
class User(db.Model, UserMixin):
user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), nullable=False, unique=True)
password_hash = db.Column(db.String(40), nullable=False)
But I want users to be able login and register using either by creating a new account with username and password, or using a social media account (google/twitter/vk.com). My idea is to have a base User table that will have abstract methods like get_name() that will have to be defined in the derived tables for each social media.
Pseudocode for this:
Base user
class User(db.Model, UserMixin):
user_id = db.Column(db.Integer, primary_key=True)
# Overridden method from UserMixin
def get_id(self):
return self.user_id
def get_name(self): raise NotImplementedError
Google user
class GoogleUser(User):
first_name = db.Columnn(db.String(40), nullable=False)
last_name = db.Columnn(db.String(40), nullable=False)
def get_name(self):
return '{} {}'.format(self.first_name, self.last_name)
E-mail user
class EmailUser(User):
username = db.Column(db.String(100), nullable=False, unique=True)
password_hash = db.Column(db.String(40), nullable=False)
def get_name(self):
return self.username
The question is what is the best way to implement it with SQLAlchemy & UserMixin?
Thanks for any help in advance.
What I usually do is create a User model with all the information that is needed by the application e.g.
class User(db.Model, UserMixin):
user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), nullable=False, unique=True)
password_hash = db.Column(db.String(40), nullable=True)
Then for third party services authentication, I create a table which stores token and other relevant data regarding that service that the application requirese.g.
class GoogleUser(db.Model):
first_name = db.Column(db.String(40), nullable=False)
last_name = db.Column(db.String(40), nullable=False)
google_token = db.Column(db.String(200), nullable=False)
I connect this new table to the User model via foreign key:
google_user_id = db.Column(db.Integer, db.ForeignKey("google_user.id"))
This way the user can easily connect their third party accounts and have an account on your application.
Facebook Login's documentation also recommends this
Hope it clears things up!
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.
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)
I have two user models for flask_login.:
class Admin(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30))
password_hash = db.Column(db.String(200))
class Merchant(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30))
password_hash = db.Column(db.String(200))
Now I want to load user in session:
#login_manager.user_loader
def load_user(user_id):
pass
I want to know how to load user from two models.
Here's a solution I've been using so far; I don't know it's flaws, but it's the answer you're looking for.
Assuming you have multiple account types, the key is to use the session to store that account type upon login, and use it like this:
#login_manager.user_loader
def load_user(user_id):
if session['account_type'] == 'Admin':
return Admin.query.get(int(user_id))
elif session['account_type'] == 'Merchant':
return Merchant.query.get(int(user_id))
else:
return None
Providing routes and html is not necessary; you could implement them as you wish, either by:
Creating different routes for different user types.
Adding a select field in the login form with one route for all user types.
This thread provides further information about sessions and how to secure them; you should check it out.
I understand that your choice to keep your classes separated, but consider merging the common attributes together in one parent class leaving only the id to prevent foreign keys problems, like this:
class Person(db.Model):
__abstract__ = True
name = db.Column(db.String(30))
password_hash = db.Column(db.String(200))
class Admin(Person, UserMixin):
id = db.Column(db.Integer, primary_key=True)
class Merchant(Person, UserMixin):
id = db.Column(db.Integer, primary_key=True)
As the parent table is abstract it won't be created, but its children will.
#You can create a permission
admin_permission = Permission(RoleNeed('admin'))
#protect a view with a principal for that need
#app.route('/admin')
#admin_permission.required()
def do_admin_index():
return Response('Only if you are an admin)