Search code examples
formspython-3.xflaskmany-to-manyflask-admin

Secondary Table Attributes in Create / Edit Views for primary models


I want to be able to create and edit the secondary table attributes (the relational table) of a many-to-many relationship during the creation or editing of either of the primary tables. So, when I edit one of the primary tables and add a relation to another model (implicitly using the secondary table), I want to be able to access / edit the attributes of that secondary relationship.

More specifically:

Models

# "Primary" table
class Paper(db.Model):
    __tablename__ = 'papers'
    ...
    chapters = db.relationship(Chapter, secondary="chapter_paper")
    ...

# "Primary" table
class Chapter(db.Model):
    ...
    papers = db.relationship('Paper', secondary="chapter_paper")
    ...

# "Secondary" table
class ChapterPaper(db.Model):
    __tablename__ = 'chapter_paper'
    paper_id = db.Column(db.Integer,
                         db.ForeignKey('papers.id'),
                         primary_key=True)
    chapter_id = db.Column(db.Integer,
                           db.ForeignKey('chapters.id'),
                           primary_key=True)

    ### WANT TO EDIT
    printed = db.Column(db.Boolean, default=False)
    note = db.Column(db.Text, nullable=True)
    ### WANT TO EDIT


    paper = db.relationship('Paper',
                            backref=db.backref("chapter_paper_assoc",
                                               lazy='joined'),
                            lazy='joined')
    chapter = db.relationship(Chapter,
                              backref=db.backref("chapter_paper_assoc",
                                                 lazy='joined'),
                              lazy='joined')

So, for this example, I want to be able to edit the "printed" and "note" attribute of ChapterPaper from the create / edit forms of Paper and Chapter in flask admin.

ModelViews

# MainModelView subclasses flask_admin.contrib.sqla.ModelView
class PaperModelView(MainModelView):
    ...
    form_columns = (
        'title',
        'abstract',
        'doi',
        'pubmed_id',
        'link',
        'journals',
        'keywords',
        'authors',
        'chapters',
    )
    # Using form_columns allows CRUD for the many to many
    # relation itself, but does not allow access to secondary attributes
    ...

So, I honestly have very little idea of how to do this. If I added the form fields as extras and then manually validated them...? (I don't know how to do this)

Even then, adding extra fields to the form doesn't really cover multiple models. Can anyone show me how to do this, or point me to a tutorial / even a relevant example from code that's part of some random project?

Thanks!


Solution

  • Alrighty, this was a lot of work and required a lot of RTFM, but it was pretty straightforward once I got going.

    The way to do this without a neat API is to extend the model view and replace the create / edit form with a form of your own.

    Here is my form class:

    class ExtendedPaperForm(FlaskForm):
        title = StringField()
        abstract = TextAreaField()
        doi = StringField()
        pubmed_id = StringField()
        link = StringField()
        journals = QuerySelectMultipleField(
            query_factory=_get_model(Journal),
            allow_blank=False,
        )
        issue = StringField()
        volume = StringField()
        pages = StringField()
        authors = QuerySelectMultipleField(
            query_factory=_get_model(Author),
            allow_blank=False,
        )
        keywords = QuerySelectMultipleField(
            query_factory=_get_model(Keyword),
            allow_blank=True,
        )
        chapters_printed = QuerySelectMultipleField(
            query_factory=_get_model(Chapter),
            allow_blank=True,
            label="Chapters (Printed)",
        )
        chapters = QuerySelectMultipleField(
            query_factory=_get_model(Chapter),
            allow_blank=True,
            label="Chapters (All)",
        )
    

    The important part for making this functionality happen is the on_model_change method, which performs an action before a model is saved.

    ...
        def on_model_change(self, form, model, is_created):
            """
            Perform some actions before a model is created or updated.
            Called from create_model and update_model in the same transaction (if it has any meaning for a store backend).
            By default does nothing.
    
            Parameters:
            form – Form used to create/update model
            model – Model that will be created/updated
            is_created – Will be set to True if model was created and to False if edited
            """
    
            all_chapters = list(set(form.chapters.data + form.chapters_printed.data))
            for chapter in all_chapters:
    
                if chapter in form.chapters_printed.data:  # if chapter in both, printed takes priority
                    chapter_paper = ChapterPaper.query.filter_by(chapter_id=chapter.id, paper_id=model.id).first()
    
                    if not chapter_paper:
                        chapter_paper = ChapterPaper(chapter_id=chapter.id, paper_id=model.id)
    
                    chapter_paper.printed = True
                    db.session.add(chapter_paper)
    
            journal = None
            if form.journals.data:
                journal = form.journals.data[0]
    
            if journal:  # Assumes only 1 journal if there are any journals in this field
                issue = form.issue.data
                volume = form.volume.data
                pages = form.pages.data
                journal_paper = JournalPaper.query.filter_by(journal_id=journal.id, paper_id=model.id).first()
    
                if not journal_paper:
                    journal_paper = JournalPaper(journal_id=journal.id, paper_id=model.id)
    
                journal_paper.issue = issue
                journal_paper.volume = volume
                journal_paper.pages = pages
                db.session.add(journal_paper)
    ...