Simple Many-to-Many issue in Flask-Admin - python

I'm adding Flask-Admin to an existing Flask app (using Python 3, and MySQL with SQLAlchemy), and I simply cannot figure out how to get a many-to-many relationship to render correctly. I've read a number of questions about this here, and it looks like I am following the right practices.
I have a Quotation table, a Subject table, and a QuotationSubject table, which has to be an actual class rather than an association table, but I don't care about the extra columns in the association table for this purpose; they're things like last_modified that I don't need to display or edit. The relationships seem to work in the rest of the application.
Trimming out the fields and definitions that don't matter here, I have:
class Quotation(db.Model):
__tablename__ = 'quotation'
id = db.Column(db.Integer, primary_key=True)
word = db.Column(db.String(50))
description = db.Column(db.Text)
created = db.Column(db.TIMESTAMP, default=db.func.now())
last_modified = db.Column(db.DateTime, server_default=db.func.now())
subject = db.relationship("QuotationSubject", back_populates="quotation")
def __str__(self):
return self.word
class Subject(db.Model):
__tablename__ = 'subject'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
created = db.Column(db.TIMESTAMP, default=db.func.now())
last_modified = db.Column(db.DateTime, server_default=db.func.now())
quotation = db.relationship("QuotationSubject", back_populates="subject")
def __str__(self):
return self.name
class QuotationSubject(db.Model):
__tablename__ = 'quotation_subject'
id = db.Column(db.Integer, primary_key=True)
quotation_id = db.Column(db.Integer, db.ForeignKey('quotation.id'), default=0, nullable=False)
subject_id = db.Column(db.Integer, db.ForeignKey('subject.id'), default=0, nullable=False)
created = db.Column(db.TIMESTAMP, default=db.func.now())
last_modified = db.Column(db.DateTime, server_default=db.func.now())
quotation = db.relationship('Quotation', back_populates='subject', lazy='joined')
subject = db.relationship('Subject', back_populates='quotation', lazy='joined')
In my admin.py, I simply have:
class QuotationModelView(ModelView):
column_searchable_list = ['word', 'description']
form_excluded_columns = ['created', 'last_modified']
column_list = ('word', 'subject')
admin.add_view(QuotationModelView(Quotation, db.session))
And that's it.
In my list view, instead of seeing subject values, I get the associated entry in the QuotationSubject table, e.g.
test <QuotationSubject 1>, <QuotationSubject 17>, <QuotationSubject 18>
book <QuotationSubject 2>
Similarly, in my create view, instead of getting a list of a dozen or so subjects, I get an enormous list of everything from the QuotationSubject table.
I've looked at some of the inline_models stuff, suggested by some posts here, which also hasn't worked, but in any case there are other posts (e.g. Flask-Admin view with many to many relationship model) which suggest that what I'm doing should work. I'd be grateful if someone could point out what I'm doing wrong.

First of all, I fear there's something missing from your question because I don't see the Citation class defined. But that doesn't seem to be the problem.
The most classic example of many-to-many relationships in Flask is roles to users. Here is what a working role to user M2M relationship can look like:
class RolesUsers(Base):
__tablename__ = 'roles_users'
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
role_id = db.Column(db.Integer(), db.ForeignKey('role.id'))
class Role(RoleMixin, Base):
__tablename__ = 'role'
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
def __repr__(self):
return self.name
class User(UserMixin, Base):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), index=True, unique=True)
roles = db.relationship('Role', secondary='roles_users',
backref=db.backref('users', lazy='dynamic'))
And in Flask-Admin:
from user.models import User, Role
admin.add_view(PVCUserView(User, db.session))
admin.add_view(PVCModelView(Role, db.session))
Note that the relationship is only declared once, with a backref so it's two-way. It looks like you're using back_populates for this, which I believe is equivalent.
For the case you're describing, it looks like your code declares relationships directly to the M2M table. Is this really what you want? You say that you don't need access to the extra columns in the QuotationSubject table for Flask-Admin. But do you need them elsewhere? It seems very odd to me to have a call to quotation or subject actually return an instance of QuotationSubject. I believe this is why Flask-Admin is listing all the QuotationSubject rows in the create view.
So my recommendation would be to try setting your relationships to point directly to the target model class while putting the M2M table as the secondary.
If you want to access the association model in other places (and if it really can't be an Association Proxy for some reason) then create a second relationship in each model class which explicitly points to it. You will then likely need to exclude that relationship in Flask-Admin using form_excluded_columns and column_exclude_list.

Related

Flask-Sqlalchemy create views with postgres

I am using a Flask-Sqlalchemy postgres database model with the following style:
class User(db.Model) // Holds the users
id = db.Column(db.Integer, primary_key=True)
...
class Track(db.Model) // Holds racing tracks
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
class Record(db.Model) // Hold users records on tracks
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)
track_id = db.Column(db.Integer, db.ForeignKey("map.id"), primary_key=True)
time = db.Column(db.BigInteger, nullable=False)
My goal now would be to create a view which holds all Tracks completed by Users. In plain PostgresSQL it would look like this:
CREATE VIEW OR REPLACE user_tracks_finished AS
SELECT DISTINCT user_id, track_id
FROM record;
The User model would look something like this:
class User(db.Model) // Holds the users
id = db.Column(db.Integer, primary_key=True)
finished_tracks = db.relationship(...)
I can not any way to create views with the flask-sqlalchemy module. There is a sqlalchemy-views module, which I can not get to run in my flask app. Anybody got a good idea on how to model something like this? Any help ist appreciated!
I managed to get it done with SQLAlchemy-Utils: https://pypi.org/project/SQLAlchemy-Utils/
from sqlalchemy_utils import create_view
class UserFinishedMaps(db.Model):
__table__ = create_view(
name="user_finished_maps",
selectable=select([Record.user_id, Record.map_id]).group_by(
Record.user_id, Record.map_id
),
metadata=db.metadata,
)

Is it possible to have SQL Alchemy database models point to attributes of a different model, so that when one model's data changes, so does the other?

I'm making a very simple warehouse management system, and I'd like for users to be able to create templates for items. The template will show up on a list, and then can individually be used to create instances of an item that will also gain a quantity and warehouse attribute.
The goal is, if one of the item templates gets modified to specify a different size or price, the size or price attributes of the actual item instance gets changed as well.
Here is my code in case that helps you visualize what I'm trying to do. I'm not sure if this is possible or if there is a different solution I should consider. It's my first time working with Flask SQLAlchemy.
class ItemTemplate(db.model):
"""This template will simply store the information related to an item type.
Individual items that will be associated with the warehouse they're stored in
will inherit this information from the item templates."""
_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(15), unique=True, nullable=False)
price = db.column(db.Float, nullable=False)
cost = db.column(db.Float, nullable=False)
size = db.column(db.Integer, nullable=False)
lowThreshold = db.column(db.Integer, nullable=False)
# Actual items
class Item(db.model):
"""This template will be used to represent the actual items that are associated with a warehouse."""
_id = db.Column(db.Integer, primary_key=True)
quantity = db.Column(db.Integer, primary_key=True)
"""Here I want the Item attributes to be able to just point to attributes from the ItemTemplate class.
ItemTemplate(name='tape') <--- will be a template with the information for tape.
Item(name='tape') <--- will be an actually instance of tape that should inherit all the attributes from the tape template.
I want these attributes to be like pointers so that if the tape template has its name changed, for instance, to
'scotch tape', all the Item instances that point to the tape template will have their names changed."""
# Warehouse
class Warehouse(db.model):
_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(15), unique=True, nullable=False)
capacity = db.Column(db.column(db.Integer, nullable=False))
items = db.relationship("Item", backref="warehouse", lazy=True)```
As I understand, you just declare a One-Many relationship between ItemTemplate and Item, that one template will be used for many items.
Define Model
Just try to declare their relationship like this
class ItemTemplate(db.model):
_id = db.Column(db.Integer, primary_key=True)
... # Other attribute
instances = db.relationship('item', backref='item_template', lazy=True)
class Item(db.model):
_id = db.Column(db.Integer, primary_key=True)
quantity = db.Column(db.Integer, primary_key=True)
item_template_id = db.Column(db.Integer, db.ForeignKey('item_template._id'), nullable=False)
Docs for more information about relationship:
https://flask-sqlalchemy.palletsprojects.com/en/2.x/models/#one-to-many-relationships
Query
Next time querying, just join two tables and you can have your ItemTemplate.name
items_qr = db.session.query(Item, ItemTemplate.name).join(ItemTemplate)
for item, item_name in items_qr:
print(item.id, item_name)
SQLAlchemy Doc for query.join(): https://docs.sqlalchemy.org/en/14/orm/query.html#sqlalchemy.orm.Query.join
Some relative SO questions may help
flask Sqlalchemy One to Many getting parent attributes
One-to-many Flask | SQLAlchemy

flask-migrate doesn't work When I add models with ForeignKey

class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True)
# 是不是应该加密下,不能明文存储?应该设置多长的空间? 14.7.18 4:22 by lee
password = db.Column(db.String(100))
nickname = db.Column(db.String(64))
school = db.Column(db.String(20))
sex = db.Column(db.String(5))
status = db.Column(db.String(10))
grade = db.Column(db.String(18))
I have a database remains. Then I add model to models.py:
class PubSquare(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
author = db.relationship('User', backref=db.backref('publish'))
subject = db.Column(db.String(100))
timestamp = db.Column(db.DateTime, default=datetime.datetime.now)
Then I run migrate script, it call bug:
NoReferencedTableError: Foreign key associated with column 'pub_square.author_id' could not find table 'user' with which to generate a foreign key to target column 'id'
Befor this time, I can run migrate script successfully for serveral times.But this time, when it refer to foreignkey relationship, it doesn't work.
to prove my models code is right, I re-create the database, it works.
So, it's the flask-migrate calls to this bug.
#knight We have migrated for many times.'user' table is in the database. But I found that if I
code like
author_id = db.Column(db.Integer)
and migrate, It's nothing wrong. And than add the code like
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
and migrate again, It passed. It's strange. I don't know why exactly.
Our migrate code is
api.update_db_from_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, db.metadata)
From what I can see the user table is not being created (could not find table 'user' with which to generate a foreign key to target column 'id'). Try migrating the User first and, therefore, making the user table, and then doing the PubSquare.
EDIT: Have you tried reading the docs? http://sqlalchemy-migrate.readthedocs.org/en/v0.7.1/changeset.html#constraint seems to help.

sqlalchemy foreign key relationship attributes

I have a User table and a Friend table. The Friend table holds two foreign keys both to my User table as well as a status field. I am trying to be able to call attributes from my User table on a Friend object. For example, I would love to be able to do something like, friend.name, or friend.email.
class User(Base):
""" Holds user info """
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(25), unique=True)
email = Column(String(50), unique=True)
password = Column(String(25))
admin = Column(Boolean)
# relationships
friends = relationship('Friend', backref='Friend.friend_id',primaryjoin='User.id==Friend.user_id', lazy='dynamic')
class Friend(Base):
__tablename__ = 'friend'
user_id = Column(Integer, ForeignKey(User.id), primary_key=True)
friend_id = Column(Integer, ForeignKey(User.id), primary_key=True)
request_status = Column(Boolean)
When I get friend objects all I have is the 2 user_ids and i want to display all properties of each user so I can use that information in forms, etc. I am new to sqlalchemy - still trying to learn more advanced features. This is just a snippet from a larger Flask project and this feature is going to be for friend requests, etc. I've tried to look up association objects, etc, but I am having a hard with it.
Any help would be greatly appreciated.
First, if you're using flask-sqlalchemy, why are you using directly sqlalchemy instead of the Flask's db.Model?
I strongly reccomend to use flask-sqlalchemy extension since it leverages the sessions and some other neat things.
Creating a proxy convenience object is straightforward. Just add the relationship with it in the Friend class.
class Friend(Base):
__tablename__ = 'friend'
user_id = Column(Integer, ForeignKey(User.id), primary_key=True)
friend_id = Column(Integer, ForeignKey(User.id), primary_key=True)
request_status = Column(Boolean)
user = relationship('User', foreign_keys='Friend.user_id')
friend = relationship('User', foreign_keys='Friend.friend_id')
SQLAlchemy will take care of the rest and you can access the user object simply by:
name = friend.user.name
If you plan to use the user object every time you use the friend object specify lazy='joined' in the relationship. This way it loads both object in a single query.

How do I write a query to get sqlalchemy objects from relationship?

I am learning python and using the framework pyramid with sqlalchemy as the orm. I can not figure out how relationships work. I have 2 tables, offices and users. the foreign key is on the users table 'offices_id'. I am trying to do a query that will return to me what office a user is a part of.
This is how I have my models set up.
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(Unicode(255), unique=True, nullable=False)
trec_number = Column(Unicode(255), unique=True, nullable=False)
office_id = Column(Integer, ForeignKey('offices.id'))
class Office(Base):
__tablename__ = 'offices'
id = Column(Integer, primary_key=True)
name = Column(Unicode(255), unique=True, nullable=False)
address = Column(Unicode(255), unique=True, nullable=False)
members = relationship("User", backref='offices')
In my view how would I write a query that would return the the office information for a given user?
I am trying this:
for user in DBSession.query(User).join(Office).all():
print user.address
but I think I am misunderstanding how the queries work because I keep getting errors
AttributeError: 'User' object has no attribute 'address'
when I do this:
for user in DBSession.query(User).join(Office).all():
print user.name
it prints out the users name fine since name is an attribute of the User class.
I also can not get the inverse to work
for offices in DBSession.query(Office).join(User).all():
print offices.users.name
You need to use the name you used in the backref argument to access the Office model. Try user.offices.address

Categories