Flask Admin One to One Relationship and Edit Form - flask

I am building an admin dashboard for my web app using Flask-Admin. For the user/address relationship, I am using a one to one relationship. On the user edit form, I'd like to be able to edit the individual components of the address (i.e. street address, city or zip) similar to what inline_models provides. Instead, flask-admin generates a select field and only allows me to select a different addresses.
I tried using inline_models = ['address'] in the UserModelView definition. However, I got the address object not iterable error due to the user/address relationship being configured to uselist=False. Switching uselist to True would affect other parts of my code, so I'd prefer to leave it as False.
From looking in flask-admin/contrib/sqla/forms, within the function get_forms, its being assigned a one to many tag which is what drives the use of a select field.
Before diving in further, I figured it best to see if anyone else has come across this or has a recommended fix/workaround.
models.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64))
address = db.relationship("Address", backref="user",
cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
class Address(db.Model):
id = db.Column(db.Integer, primary_key=True)
line1 = db.Column(db.String(128))
zip = db.Column(db.String(20), index=True)
city = db.Column(db.String(64), index=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id",
ondelete="CASCADE"))
admin.py
class UserModelView(ModelView):
column_list = [User.username, 'address']
form_columns = (User.username, 'address')
admin = Admin(name='Ask', template_mode='bootstrap3')
admin.add_view(UserModelView(User, db.session))

You can create 2 relations
# Relation for flask admin inline model
address_cms_relationsip = db.relationship(
"Address", backref="user", cascade="all, delete-orphan", lazy=False,
uselist=True, passive_deletes=True)
address_relationship = db.relationship(
"Address", cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
#property
def address(self):
return self.address_relationship
In your code you can use property address
user: User # some User object
user.address.city

Related

Relative Primary Key in Flask_SQLAlchemy

Context/Minimal Example: I'm relatively new to Database design and trying to design a small app in Flask/Flask_SQLAlchemy that tracks inventory.
I have a User table:
class Users(db.Model):
user_id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(25))
items = db.relationship('Item', lazy="dynamic")
and a Item table:
class Item(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'), index=True)
sku = db.Column(db.Integer, index=True, autoincrement=True)
name = db.String(10)
I would like to create a system where the Item.sku is unique... but only based on the user.id.
E.g. two users can have items of the same Sku, but one user may not have multiple items of the same sku. (And preferably have the sku automatically increment itself).
To me, this is a constraint that makes sense- sku+user_id should always be a unique combination, so I can save space and simplicity by using it as a primary key, as well as increasing the ?normalization? of the database.
However, I've spent a fair amount of time now reading and trying to figure out how to do this and I keep running into problems. Is there an easy way of accomplishing this, or is there something wrong with my logic that has lead to this design? Are there downsides to this I'm missing?
So far I've tried:
Setting both user_id and sku to primary_key=true
Setting them both to index=True (as you can see here)
Adding a table_args = db.PrimaryKeyConstraint (As discussed here https://www.reddit.com/r/flask/comments/g3tje5/composite_key_for_flasksqlalchemy/)
From what I've read the term of what I'm trying to accomplish here is a compound primary key, and that flask_sqlalchemy does support it, but with all of these I get exceptions that a constraint is failing or a parameter is missing.
Thanks for any help or advice you can provide.
Yes, a composite PK on (user_id, sku) will work, as in this example using vanilla SQLAlchemy ORM:
import sqlalchemy as db
from sqlalchemy.orm import declarative_base, relationship, Session
engine = db.create_engine("sqlite://")
Base = declarative_base()
class Users(Base):
__tablename__ = "users"
user_id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(25))
items = relationship('Item', lazy="dynamic")
class Item(Base):
__tablename__ = "item"
user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'), primary_key=True)
sku = db.Column(db.Integer, index=True, primary_key=True)
name = db.String(10)
Base.metadata.create_all(engine)
with Session(engine) as sess:
gord = Users(first_name="Gord", items=[Item(sku=1)])
anne = Users(first_name="Anne", items=[Item(sku=1), Item(sku=2)])
sess.add_all([gord, anne])
sess.commit()
# okay so far
# now try to add a duplicate
gord.items.append(Item(sku=1))
sess.flush()
"""
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: item.user_id, item.sku
[SQL: INSERT INTO item (user_id, sku) VALUES (?, ?)]
[parameters: (1, 1)]
"""

Many to Many Relationship - how to query all children

I am using flask-sqlalchemy to implement many to many relationship. Tried many options but unable to query all children for a parent.
class Participant(db.Model):
__tablename__='participant'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True)
name = db.Column(db.String(120), unique=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship("User")
#staticmethod
def get_by_user(user):
return Participant.query.filter_by(user=user).first()
def __repr__(self):
return "(Participant '{}')".format(self.email)
competitions = db.relationship("CompetitionParticipant", back_populates="participant")
class Competition(db.Model):
__tablename__='competition'
__table_args__={'extend_existing':True}
id=db.Column(db.Integer,primary_key=True)
name=db.Column(db.String(100),unique=True)
description=db.Column(db.String(255),unique=True)
# type=db.Column(db.String(50))#Team,Individual,Practice,etc.
start_date=db.Column(db.DateTime)
end_date=db.Column(db.DateTime)
result_date=db.Column(db.DateTime)
participants = db.relationship('CompetitionParticipant', back_populates='competition')
def __repr__(self):
return "(Competition '{}')".format(self.name)
class CompetitionParticipant(db.Model):
__tablename__='competition_participant'
__table_args__={'extend_existing':True}
id=db.Column(db.Integer,primary_key=True)
participant_id=db.Column(db.Integer, db.ForeignKey('participant.id'))
competition_id=db.Column(db.Integer, db.ForeignKey('competition.id'))
competition=db.relationship("Competition",back_populates="participants")
participant=db.relationship("Participant",back_populates="competitions")
def __repr__(self):
return "(competition_id, participant_id '{},{}')".format(self.competition_id,self.participant_id)
In this many to many relationship example how will I query all the competitions a participant is enrolled in?
I have tried following:
participant.competitions # returns relationship object
everything else I tried was syntax error. What is right ORM way to extract all competitions a participant is enrolled in?
If you, for example, want to get competitions data for a specific user:
participants = Participant.query.filter_by(id=1).all()
you can iterate through participants and get data :
for participant in participants:
for competition in participant.competitions:
print(competition.competition.name)
Shouldn't you be able to do something like this if you wanted to find out about the participant with id=1:
participant = Participant.query.get(1)
for item in participant.competitions:
print(item.name)

Django Inlines in admin for multiple foreign keys

I have 3 models: Document, MetaData, MetaDataValue, and a "connector table" DocumentMetaDataValue. I want to add a document and all the associated metadata values on one admin page.
models.py:
# MetaData
class MetaData(models.Model):
metadata_id = models.AutoField(primary_key = True)
name = models.CharField('metadata name', max_length=200, unique=True)
description = models.TextField('description')
def __str__(self):
return self.name
# MetaData Value
class MetaDataValue(models.Model):
metadata_id = models.ForeignKey(MetaData, on_delete=models.CASCADE,)
value = models.CharField('value', max_length=200, unique=True)
def __str__(self):
return self.value
# Document
class Document(models.Model):
document_id = models.AutoField(primary_key=True)
metadata = models.ManyToManyField('MetaData', through='DocumentMetaDataValue', through_fields=('document', 'metadata'))
metadatavalue = models.ManyToManyField('MetaDataValue', through='DocumentMetaDataValue', through_fields=('document', 'metadataValue'))
class DocumentMetaDataValue(models.Model):
document = models.ForeignKey(Document, on_delete=models.CASCADE)
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
metadataValue = models.ForeignKey(MetaDataValue, on_delete=models.CASCADE)
admin.py:
class Fred(forms.ModelForm):
""" something newer
"""
class Meta:
model = DocumentMetaDataValue
fields = '__all__'
class DocumentMetaDataValueInline(admin.TabularInline):
model = DocumentMetaDataValue
form = Fred
class DocumentAdmin(admin.ModelAdmin):
form = DocumentForm
list_display = ('get_document_type', 'document_state', 'title', 'description', 'original_file_name', 'created', 'storage_file_name', 'get_thumb', )
ordering = ('created',)
readonly_field = ('document_state',)
filter_horizontal = ('metadata', 'metadatavalue', )
inlines = (DocumentMetaDataValueInline, )
# other code not related to this problem
admin.site.register(Document, DocumentAdmin)
There are 16+ metadata names, and each one has one or more metadata values. For example, the metadata name Person has 23 values ('Bob', 'Sam', 'Sally', etc), and the metadata name Decade has 12+ values (1890, 1900, 1910, 1920, etc). A Document can have one or more metadata names associated with it, and one or more metadata values for that metadata name can be associated with that Document. For example, a Document can be a photograph with two people in it. The metadata value for Decade could be 1910, and the metadata values for Person could be Sam and Sally.
On each line of the inline, I want to show the metadata name opposite a drop down with the associated metadata values. One should be able to pick more than one metadata value for a particular metadata name.
What happens in the code above is each line of the inline has two drop down lists - the first is all the metadata names and the second is all the metadata values.
I want to change the first drop down to be a single string for a metadata name, and the second drop down to still be a drop down, but only the values for that metadata name. I don't see how to make these changes.
Thanks!
Mark

Flask-Admin Cannot find reverse relation

I already have an application using my SQLAlchemy model, and I'm now trying to add a flask admin website on top to manage some of the data in the DB. This model is already setup and working throughout the existing application so I know the relationships are configured correctly.
In my DB the data_package table has a FK to the supplier table, and this has been added as a relationship. However, when I create a flask model view to allow editing of the data_package I'm receiving the error:
Exception: Cannot find reverse relation for model
I've added a subset of the code below, there are a lot more columns and logic but this is the bare minimum I have to try and test a fix to the issue.The issue is happening because I am adding inline_models = (Supplier,) which I need as I want to have a select to allow the supplier to be changed for a data package.
The line of code throwing the exception is https://github.com/flask-admin/flask-admin/blob/master/flask_admin/contrib/sqla/form.py#L558. The reason for this is the relationship direction is defined as ONETOMANY, and there is a check to ensure the direction is MANYTOONE or MANYTOMANY
Base = declarative_base()
class Supplier(Base):
__tablename__ = 'supplier'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
class DataPackage(Base):
__tablename__ = 'data_package'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
supplier_id = Column(Integer, ForeignKey('supplier.id'), index=True, nullable=False)
supplier = relationship("Supplier", backref='packages')
class DataPackageAdminView(ModelViewOnly):
form_columns = ['name', 'supplier']
inline_models = (Supplier,)
def create_app():
settings.configure_orm()
app = Flask(__name__)
app.config.from_pyfile('config.py')
#app.route('/')
def index():
return 'Click me to get to Admin!'
admin = Admin(app, name='Test Editor', template_mode='bootstrap3')
admin.add_view(DataPackageAdminView(DataPackage, settings.Session, category='Data Sources'))
return app
if __name__ == '__main__':
app = create_app()
app.run(debug=True)

calling wtforms from sqlalchemy in flask

I'm building a website that has several survey forms.
class A(Form):
name = Text('Name')
class B(Form):
name = Text('Name')
class C(Form):
name = Text('Name')
etc...
I have a model that holds information on all these forms:
class forms(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(64), unique=True)
description = db.Column(db.String(255))
wtform = ???db.Column(db.String(255))??? # How to store a wtform [A, B, C, etc...]?
From a menu a user would select a particular form and be directed to:
#app.route('/new/<int:id>', methods = ['GET', 'POST'])
#login_required
def new(id):
data = forms.query.get(id)
form = data.???wtform??? # How to call the particular wtform here?
I'm not sure how to store and call a wtform class from sqlalchemy. What is the best method to deal with this sort of situation? Should I instead use multiple #app.route('/A'), #app.route('/B'), #app.route('/C') calls for each form, which would increase redundancy, or could I store the entire wtform class structure in the database?
You already define the forms in your code, so there's no reason to store them in the database as well. Store a reference to which form to use, then just instantiate the correct form based on the reference.
class FormDef(Base):
__tablename__ = 'form_def'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False, unique=True)
description = Column(String, nullable=False, default='')
form_ref = Column(String, nullable=False) # will be the name of the form class in this example
def get_form(instance):
# given an instance of FormDef, get the form class with the name stored in form_ref
from my_package import forms # or wherever your forms are stored
return getattr(forms, instance.form_ref)
On a side note, storing form class information in rows does not seem like the right way to solve whatever it is you're trying to solve.