Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ModelFieldList for one-page forms #37

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ejorritsma
Copy link

ModelFieldList for one-page forms

Problem

In WTForms-SQLAlchemy, there is currently no way of modeling nested forms based on from database models. Thus, in a One to Many database relationship, each of these relationship entries need their own form. Instead, it might be much more user friendly to be able to edit this relationship in a single form, with fields for each of the parent attributes, and a nested form containing entries of the database relationship.

Solution proposal

By creating a new field, ModelFieldList, I made it possible to render one to many relationships in a single form, where children are rendered as entries in nested forms. The nested forms are rendered with an 'add' and 'delete' buttons for each entry, which makes it easy to delete and/or add rows. On submitting the form, changes are updated in the database session. This field is now included in the converter for model_form().

Design

I created the following ModelFieldList class, which inherits from wtforms.fields.core.FieldList:

class wtforms_sqlalchemy.fields.ModelFieldList(unbound_field, horizontal_layout=False, model=None, label=None, validators=None, min_entries=0, max_entries=None, default=tuple(), /, *, label=None, validators=None, min_entries=0, max_entries=None, default=tuple(), filters=tuple(), description='', id=None, widget=None, render_kw=None, _form=None, _name=None, _prefix='', _translations=None, _meta=None)

  • Attributes unique to ModelFieldList:
    • model - The SQLAlchemy database model that serves as the base for the entries in the fieldlist.

    • horizontal_layout - When set to True, ModelFieldList will be rendered with a table that contains in the first row the fieldnames and in the second row the fields. When set to the default (False), fieldnames will appear in the table head, and fields appear below their proper fieldname for each entry.

I also created the following widget to work with the ModelFieldList class:

wtforms_sqlalchemy.fields._ModelFieldListTableWiget(horizontal_layout, prefix_label=True, with_table_tag=True)

  • Attributes:
    • horizontal_layout - See the horizontal_layout attribute for ModelFieldList.
    • prefix_label - If True, this renders the label before the field when the layout is horizontal.
    • with_table_tag - If True, the widget is surrounded by a <table> tag.

I edited the following method:

wtforms_sqlalchemy.orm.model_form(model, db_session=None, base_class=<class 'wtforms.form.Form'>, only=None, exclude=None, field_args=None, converter=None, exclude_pk=True, exclude_fk=True, type_name=None, embed=False)

  • New constructor parameters:
    • embed - An optional boolean or dictionary specifying whether and/or how to embed relations in model. If set to True, fields for all related models are generated. If set to a dictionary, all specified fields will be embedded (Example: embed={'student': {courses: False}}, where 'student' will get fields but 'courses' will not)

Sample usage

I created a demo of the implemented feature on repl.it, which you can check out here: https://modelfieldlist.pnehkan.repl.co/

Usage of the feature is demonstrated below.

from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
from wtforms_sqlalchemy.orm import model_form

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite3"

db = SQLAlchemy(app)

class Car(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    manufacturer = db.Column(db.String(50))
    model = db.Column(db.String(50))
    part_suppliers = db.relationship('PartSupplier', order_by='PartSupplier.name', backref='car', lazy='joined', cascade='all, delete-orphan')

class PartSupplier(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    parts = db.relationship('Part', order_by='Part.name', backref='part_supplier', lazy='joined', cascade='all, delete-orphan')
    car_id = db.Column(db.Integer, db.ForeignKey('car.id'))
    
class Part(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    in_stock = db.Column(db.Boolean)
    part_supplier_id = db.Column(db.Integer, db.ForeignKey('part_supplier.id'))


@app.route('/', methods=['GET', 'POST'])
def show_form():
  car = Car.query.get(1)
  CarForm = model_form(Car, db_session=db.session, embed={'part_suppliers': {'parts': True}})

  if request.method == 'POST':
    form = CarForm(request.form, obj=car)
    if form.validate():
      form.populate_obj(car)
      db.session.commit()
      form = CarForm(obj=car)

  else:
    form = CarForm(obj=car)

  return render_template('index.html', form=form)


if __name__ == '__main__':
  db.drop_all()
  db.create_all()
  db.session.add(Car(manufacturer='Ford', model='Fiesta'))
  db.session.add(PartSupplier(name='WinParts', car_id=1))
  db.session.add(Part(name='Steering Wheel', in_stock=True, part_supplier_id=1))
  db.session.add(Part(name='Tires', in_stock=False, part_supplier_id=1))
  db.session.commit()

  app.run(host='0.0.0.0', port="8080")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant