How to populate lists of objects with WTForms?
-
21-12-2019 - |
Вопрос
Use case: using a form to enter grades for each course a student is enrolled in.
Model:
Using SQLAlchemy, I defined a Student
object, a Course
object, and a StudentCourse
association object that stores each student's grade for each course.
class Student(Base):
__tablename__ = 'students'
id = Column(Integer, primary_key=True)
name = Column(Text)
courses = association_proxy('student_courses', 'grade',
creator=lambda k, v: StudentCourse(course_title=k, grade=v))
...
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
title = Column(Text, unique=True)
...
# Link students to courses and store grades
class StudentCourse(Base):
__tablename__ = 'student_courses'
student_id = Column(Integer, ForeignKey(Student.id), primary_key=True)
course_id = Column(Integer, ForeignKey(Course.id), primary_key=True)
grade = Column(Integer)
student = relationship(Student,backref=backref(
'student_courses',
collection_class=attribute_mapped_collection('course_title'),
cascade='all, delete-orphan'))
course = relationship(Course)
@property
def course_title(self):
if self.course is not None:
return self.course.title
else:
return self._course_title
...
View:
I can query the StudentCourses model and construct a relevant form, but I can't figure out how to pass/retrieve the data from the query as an object.
def view(request):
student = Student.from_request(request)
student_courses = DBSession.query(StudentCourse).\
filter(StudentCourse.student_id == student.id).\
all()
class GradesForm(Form):
pass
# Add form field for each course the student is enrolled in
for c in student_courses:
setattr(GradesForm,
c.course_title,
IntegerField()
)
form = GradesForm(request.POST, obj=student_courses) # this doesn't populate the form
return {'form': form}
This produces a blank form, so I obviously can't populate the form with data directly from the Query
object. But I've been unsuccessful populating the form with any kind of object, even when creating a form with a FormField
type for each course:
class StudentCourseForm(Form):
course_title = StringField()
grade = IntegerField()
def view(request):
...
class GradesForm(Form):
pass
# Add form field for each course
for c in student_courses:
setattr(GradesForm,
c.course_title,
FormField(StudentCourseForm)
)
form = GradesForm(request.POST, obj=student_courses)
return {'form': form}
Using a query, if possible, would be the easiest. Per the SQLAlchemy docs, using the query()
method on a session creates a Query
object. When iterated like I did in my controller, this object is a list of StudentCourse
objects.
[<app.models.StudentCourse object at 0x10875bd50>, <app.models.StudentCourse object at 0x10875bed0>]
...and my progress ends here. Any help appreciated!
Решение
The only way I've been able to populate these dynamically-created forms is by passing **kwargs, so I'll post this method as an answer until someone else can figure out an object-based solution.
To populate the form:
def view(request):
...
data = {}
for c in student_courses:
data[c.course_title] = c.grade
# Populate form
form = GradesForm(request.POST, **data)
...
In this way, I can render a form in a template by iterating over the fields, and when submitted, I'll have a list of dicts which I can then validate and use to update my database records.
Form validation requires the same method:
def view(request):
...
# Validate and persist form data
if request.method == 'POST' and form.validate():
for c in student_courses:
student.courses[c.title] = form[c.title].data
This works, but it'd be great if I could use the WTForms populate_obj()
method:
def view(request):
...
if request.method == 'POST' and form.validate():
form.populate_obj(student_courses)