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.
Related
Is there a way to select the db bind based on user selection? In my case, I want to change the db bind based on user selected country. If US, connect to db_us, and if UK, select db_uk.
However, as soon as the application loads, I call db.init_app() which already selects db_us and the models are set, the blueprints are configured and then the login form with country selection is loaded. The user selects UK and enters username and password, but despite having the country selected in session I am not able to reset the models with the new bind_key.
Here is some code:
# app.py
def create_app(config=None, app_name=None, blueprints=None):
db.init_app(app)
admin.add_view(UsersAdmin(db.session))
admin.init_app(app)
...
# /partners/model.py
class Partners(db.Model):
__tablename__ = 'partners'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
# /user/model.py
class Users(db.Model, UserMixin):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
email = db.Column(db.String(STRING_LEN), unique=True)
partner = db.Column(db.Integer, db.ForeignKey("PARTNERS.id"))
partners = db.relationship("Partners", uselist=True, backref="user")
# Customized User model admin
class UsersAdmin(ModelView):
# set the form fields to use
form_columns = ('name', 'email', 'partners')
column_list = ('id', 'name', 'email', 'stores')
# Override query methods to add database switchers
def _run_view(self, fn, *args, **kwargs):
selectedCountry = session['selectedcountry']
with db.context(bind=selectedCountry):
return super()._run_view(fn, *args, **kwargs)
It still queries against US db.
SQLAlchemy with multiple binds - Dynamically choose bind to query helped me, but I still have an issue with using db bind with a flask-admin ModelView. I am now able to use the correct db to login but in Flask Admin page User Create form, the form field Partners drop down list always loads US relevant options. How can I set bind in ModelView?
# /frontend/views.py
def login():
...
user, authenticated = Users.authenticate(form.login.data, form.password.data, country=form.country.data)
if user and authenticated:
...
session['selectedcountry'] = form.country.data
...
# /user/model.py
#classmethod
def authenticate(cls, login, password, country):
with db.context(bind=country):
user = cls.query.filter(Users.email.ilike(login), Users.country.ilike(country)).first()
...
After applying the suggestion from Aaron regarding overriding UserAdmin._run_view(), I still find view does not query against the db in context. The select list for partners is still loaded from the default db.
Additional issues that I saw was that, even though authentication works fine for each db, the logged in user and email displayed post login is from the other db. For example, I logged in with UK Admin User of db_uk but the name and email is from db_us. See images below. The first image is from the application and second one is from db_uk and db_us respectively. The number of users listed is correct.
Specify the context in lms_signallingSession.execute() for it to work globally, and then you can remove with db.context(bind=selectedCountry): everywhere except Users.authenticate().
class lms_signallingSession(SignallingSession):
...
def execute(self, *args, **kwargs):
from flask import session
selectedCountry = session['selectedcountry']
with self.db.context(bind=selectedCountry):
return super().execute(*args, **kwargs)
It's called whenever you access the DB.
Otherwise, you need to specify the context explicitly in all places that are relevant for it to not be "unreliable". In particular, you are missing it in:
UsersAdmin._run_view()
def load_user(id):
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.
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 am having a lot of trouble implementing Joined Table Inheritance as laid out in the SQLAlchemy documentation here!.
I am building a web app using Flask and therefore using the Flask-SQLAlchemy library. I have been able to successfully replicate the inheritance when using just SQLAlchemy. My issue arises when I start implementing it with Flask-SQLAlchemy. It seems as if Flask-SQLAlchemy makes some fundamental changes that inhibit this kind of inheritance out of the box.
To be more specific, when I query a subclass using Flask-SQLAlchemy, an InstrumentedList does indeed get returned. The problem is that it never casts to the correct subclass.
I have searched for a long time reading all the info I could possibly find but to no avail. I have tried using the with_polymorphic function from sqlalchemy but, again, to no avail.
If someone could point me in the correct direction I would greatly, greatly appreciate it. Ideally, if someone could post a minimalist, working code snippet for Joined Table Inheritance using Flask-SQLAlchemy that would be much appreciated.
Thanks in advance
Daniel
The concept of joined table inheritance is so neat from a database design perspective. Let us take this example below:
# Base class
class Employee(UserMixin, 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)
password_hash = db.Column(db.String(128))
type = db.Column(db.String(64))
__mapper_args__ = {
'polymorphic_identity': 'employee',
'polymorphic_on': 'type'
}
def __repr__(self):
return f'Employee: {self.username}'
# Child1
class Manager(Employee):
id = db.Column(db.Integer, db.ForeignKey('employee.id'), primary_key=True)
department = db.Column(db.String(64))
__mapper_args__ = {
'polymorphic_identity': 'manager',
'polymorphic_load': 'inline'
}
def __init__(self, department):
super().__init__(username, email, department)
self.department = department
def __repr__(self):
return f'Manager: {self.department}'
# Child2
class Engineer(Employee):
id = db.Column(db.Integer, db.ForeignKey('employee.id'), primary_key=True)
stack = db.Column(db.String(64))
__mapper_args__ = {
'polymorphic_identity': 'engineer',
'polymorphic_load': 'inline'
}
def __init__(self, stack):
super().__init__(username, email, stack)
self.stack = stack
def __repr__(self):
return f'Engineer: {self.stack}'
There is a parent table called Employee that is polymorphically inherited by two children, Manager and Engineer. The base class defines common fields. The child models define fields that are specific to them.
Each model has an identity mapped to the discriminator column called type. This discriminator column typically stores strings such as in the case above, "employee", "manager" and "engineer".
To load inheritance hierarchies, polymorphic loading usually comes with an additional problem of which subclass attributes are to be queried upfront and which are to be loaded later. When an attribute of a particular subclass is queried up front, it can be used in the query as something to filter on, and it will be loaded when we get our objects back. On the other hand, if it is not queried up front, it gets loaded later when we first need to access it.
Thankfully, there are ways we can handle this problem. In the sample models shown above, I have specified that the children models should individually participate in the polymorphic loading by default using the mapper.polymorphic_load parameter.
If I query a particular subclass, I should be able to access all its information, which includes the data stored in the Employee table.
Updating The Models
A simple route to update a manager would look like this:
#app.route('/register/user/general-info', methods=['GET', 'POST'])
def register_user():
"""Registration URL"""
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
session['username'] = form.username.data
session['email'] = form.email.data
session['identity'] = form.identity.data
session['password'] = form.password.data
if session['identity'].lower() == 'manager':
flash('Complete your registration as manager')
return redirect(url_for('register_manager'))
if session['identity'].lower() == 'engineer':
flash('Complete your registration as engineer')
return redirect(url_for('register_engineer'))
return render_template('register.html', title='General User Data', form=form)
#app.route('/register/user/manager', methods=['GET', 'POST'])
def register_manager():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = ManagerForm()
if form.validate_on_submit():
manager = Manager(
username = session['username'],
email = session['email'],
department = form.department.data
)
manager.set_password(session['password'])
db.session.add(manager)
db.session.commit()
del session['username']
del session['email']
del session['identity']
del session['password']
flash(f'Registration successful. Login to continue')
return redirect(url_for('login'))
return render_template('register_manager.html', title='Manager Details', form=form)
Here, I am using two view functions, one to capture general user data, then the other for manager-specific details. In the end, I've added a manager object with all the info the Manager model would need.
Querying
In an active Python prompt, we can do the following:
>>> managers = Manager.query.all()
>>> for m in managers:
... m.username, m.type, m.department
# Output
('manager_username', 'manager', 'manager_department')
All fields about the manager, including the department which is specific to the Manager model, are accessible.
I'm messing around with Flask and the Flask-SQLAlchemy extension to create a simple registration form. In my User class, I have the attribute "email" set to nullable=False, but when I test the form on the site without including an email, it saves the new user to the db instead of throwing an exception as I expected. Any idea why that's happening? Thanks for looking!
Here's the code:
from flask import Flask, url_for, render_template, request, redirect
from flaskext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/kloubi.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(80), unique=True, nullable=False)
fname = db.Column(db.String(80))
lname = db.Column(db.String(80))
def __init__(self, username, email, fname, lname):
self.username = username
self.email = email
self.fname = fname
self.lname = lname
#app.route('/')
def index():
return render_template('index.html')
#app.route('/register/', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
new_user = User(fname = request.form['name'],
lname = request.form['surname'],
email = request.form['email'],
username = request.form['username'])
db.session.add(new_user)
db.session.commit()
return redirect(url_for('index'))
return render_template('register.html')
if __name__ == '__main__':
app.debug = True
app.run(host='0.0.0.0')
The problem is when you submit the webform without entering an email it will contain an empty string "" .. not None... and an empty string is not the same als null and it is ok to store it in the field.
I suggest using something like wtforms to validate the input of the user.
You can add a judgment if email != ''
Option1: If you are defining form by yourself in HTML
Just by using the required attribute in input HTML tag will prompt the user that an input field must be filled out before submitting the form.
<form action="#">
<input type="email" name="email" required>
<input type="submit">
</form>
Option2: If you are using WTforms to define the form
Then by using validators.required() while defining form class you can achieve the same result
class MyForm(Form):
email = EmailField(u'Email', [validators.required()])
you can use html5 attribute email directly in html. See this.