4

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!

bfin
  • 511
  • 6
  • 11
  • possible duplicate of [WTForms create variable number of fields](http://stackoverflow.com/questions/11622592/wtforms-create-variable-number-of-fields) – Sean Vieira Jan 10 '14 at 21:25
  • `class GradesForm(Form): pass` then `for course in courses: setattr(GradesForm, course.name, TextField(course.name, validators=[Required()]))` for an example :-) – Sean Vieira Jan 10 '14 at 21:27
  • @SeanVieira: Thanks for the input. I read that post (as well as the [section in the docs](http://wtforms.readthedocs.org/en/1.0.4/specific_problems.html#dynamic-form-composition) demonstrating that technique), but I can't set the course _grades_ in that way...at least not so easily. I can use this technique to populate the correct _number_ of fields, and even use WTForms validation, but I'd still have to loop through the courses again in the controller to assign the form data to the correct objects in the DB (instead of using `form.populate_obj(user)`). – bfin Jan 11 '14 at 18:27
  • I am have a [related issue](http://stackoverflow.com/questions/23251470/how-to-send-query-results-to-a-wtform-field) with no resolution. In short, how do I pass a string variable to the form so I can use the data pre-poplulate a field. An example use case: a comma separated list of tags stored in a many-to-many database. I think the proper way to do it is to run the query pass the values from the view to the form, but every example I found uses populate_obj(post) which pass the entire post object to the form. What if I need to do something special to one of post's attributes before passing. – jwogrady Apr 23 '14 at 23:10
  • 1
    @bfin agghhh... Can you **please** post your solution. The original question was posted months ago so I assume you have figured this out. – jwogrady Apr 23 '14 at 23:13
  • @jwogrady: I wasn't able to populate/validate/persist form data with objects, but I posted the kwargs-based solution I ended up using in case it helps. – bfin Apr 24 '14 at 13:59
  • @bfin, thank you so much for doing that. I really appreciate it. – jwogrady Apr 24 '14 at 22:45

1 Answers1

1

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)
bfin
  • 511
  • 6
  • 11
  • I'm not clear on how close our issues are related, but I think we are/were looking at this the wrong way. The populate_obj means we are passing data to another object. We are not populating the object with another object, hence the name populate object... In my case, my PostForm maps to my model. That means the value is attached to the field. No need to assign the value. The value is already there. Here is my [answered question](http://stackoverflow.com/questions/23251470/how-to-send-query-results-to-a-wtform-field) in case it helps. – jwogrady Apr 24 '14 at 22:58
  • @jwogrady: The challenge for my use case is that I have no object to pass to the populate_obj method, since this particular form is generated dynamically. If each student was enrolled in the same set of classes, then I could create a static form and set the grades for those specific courses as attributes of a User class and pass an instance of that class for data population/validation/persistence. Unfortunately, that's not my case... – bfin Apr 24 '14 at 23:31