Writing my first web app using flask / SQLAlchemy. I have a many to many relationship between 'persons' and 'facilities.' When I successfully add a person using the registration form, the association table does not get a row added. Do I have to insert that row manually?
Here is the pertinent part of the model:
# app/models.py
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login_manager
# [START model]
# Build secondary table for many to many between facilities and persons
workers = db.Table('workers',
db.Column('facility_id', db.Integer, db.ForeignKey('facilities.id')),
db.Column('person_id', db.Integer, db.ForeignKey('persons.id'))
)
class Facility(db.Model):
__tablename__='facilities'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60))
description = db.Column(db.String(128))
persons = db.relationship('Person', secondary='workers', backref='facilities', lazy = 'dynamic')
def __repr__(self):
return "<Facility name='%s')" % (self.name)
class Person(UserMixin, db.Model):
__tablename__ = 'persons'
id = db.Column(db.Integer, primary_key=True)
last_name = db.Column(db.String(60), index=True)
username = db.Column(db.String(60), index=True, unique=True)
email = db.Column(db.String(80), index=True)
password_hash = db.Column(db.String(128))
first_name = db.Column(db.String(60), index=True)
role = db.Column(db.Integer, db.ForeignKey('roles.id'))
is_person_active = db.Column(db.Boolean, index=True)
is_admin = db.Column(db.Boolean, default=False)
comments = db.Column(db.String(255))
animals = db.relationship('Animal', secondary='permissions', backref='persons', lazy = 'dynamic'))
#property
def password(self):
"""
Prevent password from being accessed
"""
raise AttributeError('password is not a readable attribute.')
#password.setter
def password(self, password):
"""
Set password to a hashed password
"""
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
"""
Check if hashed password matches actual password
"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return "<Person name='%s', '%s', '%s')" % (self.first_name, self.last_name, self.username)
# Set up user_loader
#login_manager.user_loader
def load_user(user_id):
return Person.query.get(int(user_id))
And here is the view:
# app/auth/views.py
from flask import flash, redirect, render_template, url_for
from flask_login import login_required, login_user, logout_user
from . import auth
from .forms import LoginForm, RegistrationForm
from .. import db
from ..models import Person, Facility
#auth.route('/register', methods=['GET', 'POST'])
def register():
"""
Handle requests to the /register route
Add a person to the database through the registration form
"""
form = RegistrationForm()
form.facility_id.choices = [(f.id, f.name) for f in Facility.query.order_by('name')]
if form.validate_on_submit():
person = Person(facility=form.facility_id.data,
email=form.email.data,
username=form.username.data,
first_name=form.first_name.data,
last_name=form.last_name.data,
password=form.password.data)
# add person to the database
db.session.add(person)
db.session.commit()
flash('You have successfully registered! You may now login.')
# redirect to the login page
return redirect(url_for('auth.login'))
# load registration template
return render_template('auth/register.html', form=form, title='Register')
Thanks for the support #Michael. You were close enough that I found the problem; it was that I was not adding the person to the persons collection for the facility, so no row was inserted into the workers table. I added
facility = Facility.query.filter_by(id=form.facility_id.data).first()
facility.persons.append(person)
db.session.commit()
after the existing code
db.session.add(person)
db.session.commit()
in the registration view and it is correctly inserting rows in the workers table now.
If the above https://stackoverflow.com/a/60100671/1449799 doesn't work, I wonder if it's as simple as a spelling issue? You've said that the back ref from Facility to Person should be called facilities. perhaps in your call to the Person constructor in your register function you should change:
person = Person(facility=form.facility_id.data,
to
person = Person(facilities=[form.facility_id.data],
Perhaps this question is a duplicate of https://stackoverflow.com/a/25669256/1449799 ? It seems that the issue you're having is that in your register() function, there's no mention of facilities.
Without changing your model classes (e.g. to have the Person model know about its connected facilities in addition to the reverse of what you do have now in Facility for Person), I think you may be able to do something in register() like:
#this should maybe come after db.session.add(person), but before db.session.commit()
selected_facility = Facility.query.get(form.facility_id.data)
selected_facility.persons.append(person)
or alternatively
#this should maybe come after db.session.add(person), but before db.session.commit()
selected_facility = Facility.query.get(form.facility_id.data)
person.facilities.append(selected_facility)
Related
I am just beginning with python and flask and am looking to create an app. The app allows users to enter their favourite artists in one page. In the next page then allows the user to enter favourite tracks using SelectField where the artist names are given as the options.
I have the following
models:
from application import db
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, BooleanField, IntegerField, SelectField
class Artist(db.Model):
id = db.Column(db.Integer, primary_key=True)
artist_name = db.Column(db.String(45), nullable=False)
music_artist = db.relationship('Music', backref='musicbr')
def __repr__(self):
return 'Choose {}'.format(self.artist_name)
class Music(db.Model):
id = db.Column(db.Integer, primary_key=True)
track_name = db.Column(db.String(45), nullable=False)
artist_id = db.Column(db.Integer, db.ForeignKey('artist.id'), nullable=False)
class AddArtistForm(FlaskForm):
artist_name = StringField('Artist Name')
submit = SubmitField('Add Artist!')
class AddMusicForm(FlaskForm):
track_name = StringField('Track Name')
artist_name = SelectField('Artist Name', coerce=int)
submit = SubmitField('Add Track!')enter code here
and the following routes
#app.route('/')
def home():
return render_template('index.html')
#app.route('/add_artist', methods = ['GET', 'POST'])
def add_artist():
form = AddArtistForm()
if form.validate_on_submit():
new_artist = Artist(artist_name =form.artist_name.data)
db.session.add(new_artist)
db.session.commit()
return render_template('index.html', message="Artist Added!")
else:
return render_template('add_artist.html', form=form)
#app.route('/add_music', methods = ['GET', 'POST'])
def add_music():
form = AddMusicForm()
if form.validate_on_submit():
new_track = Music(track_name =form.track_name.data)
artist_choice = Artist.query.all(artist_name=form.artist_name.data)
db.session.add(new_track)
db.session.commit()
return render_template('index.html', message="Track Added!")
else:
return render_template('add_music.html', form=form)
Is someone able to help me to understand what code I need to implement here?
I'm combining django-invitations with django-allauth for user invitation and signup.
I'd like the Administrator (when creating an invitation through the Django Admin) to provide extra data (here a foreign key to Patient object). This is archieved by adding an extra field to the (custom) invitation model:
class PatientInvitation (AbstractBaseInvitation):
email = models.EmailField(unique=True, verbose_name=_('e-mail address'),
max_length=app_settings.EMAIL_MAX_LENGTH)
created = models.DateTimeField(verbose_name=_('created'),
default=timezone.now)
patient = models.ForeignKey(Patient, blank=True, null=True, on_delete=models.CASCADE)
#classmethod
def create(cls, email, inviter=None, patient=None, **kwargs):
key = get_random_string(64).lower()
instance = cls._default_manager.create(
email=email,
key=key,
inviter=inviter,
patient=patient,
**kwargs)
return instance
def key_expired(self):
expiration_date = (
self.sent + datetime.timedelta(
days=app_settings.INVITATION_EXPIRY))
return expiration_date <= timezone.now()
def send_invitation(self, request, **kwargs):
current_site = kwargs.pop('site', Site.objects.get_current())
invite_url = reverse('invitations:accept-invite',
args=[self.key])
invite_url = request.build_absolute_uri(invite_url)
ctx = kwargs
ctx.update({
'invite_url': invite_url,
'site_name': current_site.name,
'email': self.email,
'key': self.key,
'inviter': self.inviter,
})
When the invited user signs up, I would like this data to end up in the Custom user model:
class customUser(AbstractUser):
username_validator = MyValidator()
is_patient = models.BooleanField(default=False)
patient = models.ForeignKey(Patient, null=True, blank=True, on_delete=models.CASCADE)
username = models.CharField(
_('username'),
max_length=150,
unique=True,
)
I've looked into the signals to pass the data, but couldn't find how exactly to do this.
Another option seems to add the PK of the foreign key to a hidden field on the signup form (this seems unsafe though).
I'm a bit stuck on this one, so if anyone could point me in the right direction, It would be greatly appreciated :)
Regards,
Joris
Use allauth's signals instead; you'll have much more options and you can still accomplish exactly what you want.
I would create signals.py in your app directory.
Register your signals file under your apps.py like so:
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'app_name'
def ready(self): ###<-----------
import app_name.signals ###<-----------
Use the user_signed_up signal to update the user with the invitation's data:
signals.py
from allauth.account.signals import user_signed_up
from invitations.utils import get_invitation_model
def user_signed_up(request, user, **kwargs):
try:
Invitation = get_invitation_model() ### Get the Invitation model
invite = Invitation.objects.get(email=user.email) ### Grab the Invitation instance
user.patient = invite.patient ### Pass your invitation's patient to the related user
user.save()
except Invitation.DoesNotExist:
print("this was probably not an invited user.")
I'm building a relatively simple Flask application with a not so simple database. I've a lot of models to handle and relate all the needed information in the DB.
For some reason I'm getting the title error.
I'm starting to think is some how related with my DB models/relationships.
So, the implementation of the models goes as the follow:
from app import db, login
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
import datetime
class User(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 = db.Column(db.String(128))
def set_password(self, password):
self.password = generate_password_hash(password)
def check_password(self, passwd):
return check_password_hash(self.password, passwd)
def __repr__(self):
return '<User {}>'.format(self.username)
class airFeedVariator(db.Model):
variatorID = db.Column(db.Integer, db.ForeignKey('variator.id',onupdate='RESTRICT',ondelete='RESTRICT'), primary_key=True)
airFeedID = db.Column(db.Integer, db.ForeignKey('air_feed.id',onupdate='RESTRICT',ondelete='RESTRICT'), primary_key=True)
variatorRel = db.relationship('Variator', backref='variators',lazy='joined')
freq = db.Column(db.Integer)
class Variator(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30))
machineID = db.Column(db.Integer)
baudrate = db.Column(db.Integer)
addedAt = db.Column(db.DateTime, default=datetime.datetime.utcnow) # the current timestamp
class Air_feed(db.Model):
id = db.Column(db.Integer, primary_key=True)
timeOn = db.Column(db.Integer)
timeOff = db.Column(db.Integer)
desc = db.Column(db.String)
digital = db.relationship('Pin_function', backref='analog_or_digital', lazy=True)
airFeedTypeId = db.relationship('Air_feed_type', backref='air_feed_type_id', lazy=True, uselist=False) #This allow the usage for a single element. 1-1 Rel.
#variators = db.relationship('Variator', secondary=airFeedVariator, lazy='subquery',backref=backref('airfeedvariators', lazy=True))
variators = db.relationship('airFeedVariator', lazy='dynamic')
And the forms goes as:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, IntegerField, DateTimeField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from flask_babel import lazy_gettext as _l #compile in runtime only.
#Utilizado para converter directamente os modelos em forms.
#from wtforms_alchemy import ModelForm
from app import models
from wtforms_alchemy import model_form_factory
BaseModelForm = model_form_factory(FlaskForm)
class ModelForm(BaseModelForm):
#classmethod
def get_session(self):
print('Session: ', db.session)
return db.session
class LoginForm(FlaskForm):
username = StringField(_l('Utilizador'), validators=[DataRequired()])
password = PasswordField(_l('Password'), validators=[DataRequired()])
remember_me = BooleanField(_l('Lembrar-me'))
submit = SubmitField(_l('Submeter'))
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repetir Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField(l_('Registo'))
#Validate on submit check for this. The rise allow to write a message directly on the webpage!
def validate_username(self, username):
user = models.User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError(_l('Utilizador já existente!'))
def validate_email(self, email):
user = models.User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError(_l('Email já existente!'))
class VariatorForm(FlaskForm):
name = StringField(_l('Nome'), validators=[DataRequired()])
machineID = IntegerField(_l('Identificador'), validators=[DataRequired()])
baudrate = IntegerField(_l('Valocidade Comm'), validators=[DataRequired()])
addedAt = DateTimeField(_l('Preenchimento automatico')) # the current timestamp
def validate_machineID(self, machineID):
user = models.Variator.query.filter_by(machineID=machineID.data).first()
if user is not None:
raise ValidationError(_l('Já existente um variador registado neste endereço!'))
I do realize that I have a FlaskForm form and ModelForm on the others, I was just testing.
My problem is the following:
File "c:\app\routes.py", line 49, in SaveVariator
db.session.add(var) File "c:\gburner\lib\site-packages\sqlalchemy\orm\scoping.py", line 153, in do
return getattr(self.registry(), name)(*args, **kwargs) File "c:\gburner\lib\site-packages\sqlalchemy\orm\session.py", line 1833, in add
raise exc.UnmappedInstanceError(instance) sqlalchemy.orm.exc.UnmappedInstanceError: Class 'app.forms.VariatorForm' is not mapped
* Added Problatic route.py part *
#app.route('/savevariator', methods=['POST'])
def SaveVariator():
var = VariatorForm(request.form)
if var.validate_on_submit():
print('Variator form submit accepted!')
### I was doing this ### Trying to add the FORM to the session!
db.session.add(var)
db.session.commit()
#############################
### INSTEAD I Should be doing this: ######
variator = Variator(name=var.name, machineID=var.machineID, baudrate=var.baudrate)
db.session.add(variator)
db.session.commit()
##############################
resp = jsonify(success=True)
else:
print('Variator form submit NOT accepted!')
resp = jsonify(success=False)
return resp
I think html code would not be necessary since everything works fine until the moment where the call to save information on database happens.
Am I missing something regarding the mapping relationship in database because I have 3 tables for a many to many relationship?
Any help solve the problem would greatly appreciated.
Thank you.
* EDIT *
Thanks to Joost for point me this simple yet difficult to realize point! (I probably should pause more often).
The problem was in the route section:
#app.route('/savevariator', methods=['POST'])
def SaveVariator():
var = VariatorForm(request.form)
if var.validate_on_submit():
print('Variator form submit accepted!')
### I was doing this ### Trying to add the FORM to the session!
db.session.add(var)
db.session.commit()
#############################
### INSTEAD I Should be doing this: ######
variator = Variator(name=var.name, machineID=var.machineID, baudrate=var.baudrate)
db.session.add(variator)
db.session.commit()
##############################
resp = jsonify(success=True)
else:
print('Variator form submit NOT accepted!')
resp = jsonify(success=False)
return resp
Thanks to Joost I was able to understand where the problem was. Thanks again!
I realize it's a simple mistake, but I couldn't find much information regarding the error and solution. However if the admins decide it's not worth to keep, feel free to remove it.
Thank you.
I am attempting to create Admin-user changeable drop down boxes that will then be used in another model to track transactions. The db.relationship approach doesn't work very well as the validation list will be changed almost daily. Think of a delivery tracking app (class DeliveryLog is where I store every delivery, class Driver is where I store names of drivers, class Product is where I store what they delivered).
imports and Models:
import os
from flask import Flask, url_for, redirect, render_template, request, abort
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin, login_required, current_user
import flask_admin
from flask_admin.contrib import sqla
import sys
# Create Flask application
app = Flask(__name__)
app.config.from_pyfile('config.py')
db = SQLAlchemy(app)
# Define models
roles_users = db.Table(
'roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
)
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 __str__(self):
return self.name
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))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
def __str__(self):
return self.email
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
class Driver(db.Model):
id = db.Column(db.Integer, primary_key=True)
driver = db.Column(db.String(255), unique=True)
def __str__(self):
return self.driver
def __repr__(self):
return self.driver
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
product = db.Column(db.String(255), unique=True)
def __str__(self):
return self.product
def __repr__(self):
return self.product
class DeliveryLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DATE,index=True)
operator = db.Column(db.String(127))
on_premises = db.Column(db.Boolean())
cargo = db.Column(db.String(127))
def __str__(self):
return self.operator + ' on ' + str(self.date)
Views, security context and main
class MyAdminView(sqla.ModelView):
def is_accessible(self):
if not current_user.is_active or not current_user.is_authenticated:
return False
if current_user.has_role('superuser'):
return True
return False
def _handle_view(self, name, **kwargs):
"""
Override builtin _handle_view in order to redirect users when a view is not accessible.
"""
if not self.is_accessible():
if current_user.is_authenticated:
# permission denied
abort(403)
else:
# login
return redirect(url_for('security.login', next=request.url))
class MyLogView(MyAdminView):
form_extra_fields = {
'cargo': sqla.fields.QuerySelectField(label='Custom Cargo',
query_factory=db.session.query(Product).all)
}
#Product.query.all
#db.session.query(Product).all
#app.route('/')
#login_required
def index():
return render_template('index.html')
# Create admin
admin = flask_admin.Admin(
app,
'Fruit Tracker',
base_template='my_master.html',
template_mode='bootstrap3',
)
# Add model views
admin.add_view(MyAdminView(Role, db.session))
admin.add_view(MyAdminView(User, db.session))
admin.add_view(MyAdminView(Driver, db.session))
admin.add_view(MyAdminView(Product, db.session))
admin.add_view(MyLogView(DeliveryLog, db.session))
# define a context processor for merging flask-admin's template context into the
#security.context_processor
def security_context_processor():
return dict(
admin_base_template=admin.base_template,
admin_view=admin.index_view,
h=flask_admin.helpers,
get_url=url_for
)
if __name__ == '__main__':
db.create_all()
if 'debug' in sys.argv:
app.run(host='0.0.0.0', port=5001, debug=True)
else:
app.run(host='0.0.0.0', port=80)
While the form generates fine and has the latest values are in the drop down, the representation of the fields in the model are incorrect to SQL Alchemy and I get :
sqlalchemy.exc.InterfaceError
sqlalchemy.exc.InterfaceError: (sqlite3.InterfaceError) Error binding
parameter 3 - probably unsupported type. [SQL: 'INSERT INTO
delivery_log (date, operator, on_premises, cargo) VALUES (?, ?, ?,
?)'] [parameters: ('2016-08-18', 'Frank', 1, kiwi)]
Please keep in mind that this is a simplified example that may teach others how to sub class views in Flask-Admin, the real application will have large forms (20 fields + at least five drop down boxes), so single-field to single-class overrides don't work here. I think that this pattern of allowing admin-users change how the application behaves is great way to offload maintenance of the application onto non-developers. Thank you
It looks like SQLlite doesn't like your on_premises data type. Try converting the '1' to a real Python boolean value, True. Or double check that the '1' is truly an int, and not a string.
I want to create an authentication backend, because my usernames and passwords stores in existing DB table. In this table, there is more information than just usernames and passwords, that's why I created an extended user.
class MyUser(AbstractUser):
user = models.OneToOneField(User)
user_id = models.IntegerField()
code = models.BigIntegerField()
telefon = models.CharField(50)
remark = models.CharField(250)
fio = models.CharField(50)
As I understand, I don't have to include username and password in this model, because it's already included due to user = models.OneToOneField(User).
OK, then I create the backend:
from login.models import MyUser
class AuthBackend:
def authenticate(self, username=None, password=None):
try:
user = MyUser.user.objects.filter(username=username)
except MyUser.DoesNotExist:
return None
if user.is_pass_valid(password):
return user
else:
return None
def get_user(self, user_id):
try:
user = MyUser.user.objects.get(id=user_id)
except MyUser.DoesNotExist:
return None
return user
Is it all correct?
And main question: how my backend is going to return User object from DB table( not auth_user table, but existing table). I have to create models of users before authentication or what? Or I have an idea just to create such user if exists in method authenticate? And where should I call get_user method?
My idea:
def authenticate(self, username=None, password=None):
password = hashlib.md5(password).hexdigest()
cursor = connection.cursor()
cursor.execute("""
SELECT * FROM zusers
WHERE login = %s AND userpass = %s""", [username, password])
row = cursor.fetchone()
if row:
user = MyUser(username = row[0], password = row[1], code = row[2], telefon = row[3], remark = row[4], fio = row[5])
return user
else:
return None
I don't really get what you are trying...you are using md5 instead of something that actually works for encrypting passwords...
If you want to extend an user it is simple since django 1.5:
class MyUser(AbstractUser):
myCustomField = models.CharField()
#You don't need ids, oneToOne or anything weird...just extra fields for your user
Now in settings.py or whatever your settings module is you set:
AUTH_USER_MODEL = 'myapp.MyUser'
You can find the information in the documentation.
And...thats it, you don't need anything else to make it work. Have in mind that when you are going to use your users in other models you have 2 options:
from django.conf import settings
from django.db import models
class Article(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL)
Or:
from myapp.models import MyUser
from django.db import models
class Article(models.Model):
author = models.ForeignKey(MyUser)
Using this method you don't have to worry about anything else, just forget about the default...use MyUser every time.