Skip to content
This repository has been archived by the owner on May 15, 2023. It is now read-only.

Commit

Permalink
working towards ajax-less nested forms
Browse files Browse the repository at this point in the history
  • Loading branch information
Brett committed Jun 3, 2016
1 parent 91ca8d3 commit 6308ba2
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 33 deletions.
76 changes: 70 additions & 6 deletions bauble/controllers/taxon.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
from flask import abort, request
from flask.ext.login import login_required
from wtforms_alchemy import ModelFieldList, ModelForm, ModelFormField, model_form_factory
from wtforms.fields import FormField, BooleanField, StringField
import wtforms.widgets as widgets
import sqlalchemy.orm as orm
from webargs import fields
from webargs.flaskparser import use_args

import bauble.db as db
from bauble.forms import form_factory
from bauble.models import Geography, Taxon
# from bauble.controllers.vernacular_name import Resource as VernacularNameResource
from bauble.forms import form_factory, form_class_factory, BaseModelForm
from bauble.models import Geography, Taxon, VernacularName, DefaultVernacularName
from bauble.resource import Resource
import bauble.utils as utils

resource = Resource('taxon', __name__)

class HiddenBooleanField(BooleanField):
widget = widgets.HiddenInput()

class RelationshipFormMixin(BaseModelForm):
destroy_ = HiddenBooleanField()

class OneToManyField(ModelFieldList):
def __init__(self, model, *args, **kwargs):
model_form = model_form_factory(RelationshipFormMixin)
form_cls = form_class_factory(model, model_form, include_primary_keys=True)
super().__init__(ModelFormField(form_cls), *args, **kwargs)


class OneToOneField(ModelFormField):
def __init__(self, model, *args, **kwargs):
model_form = model_form_factory(RelationshipFormMixin)
form_cls = form_class_factory(model, model_form, include_primary_keys=True)
super().__init__(form_cls, *args, **kwargs)


class TaxonForm(form_class_factory(Taxon)):
vernacular_names = OneToManyField(VernacularName)

@resource.index
@login_required
def index():
Expand Down Expand Up @@ -46,14 +73,49 @@ def new():
taxon = Taxon()
geographies = Geography.query.all()
return resource.render_html(taxon=taxon, geographies=geographies,
form=form_factory(taxon))
form=TaxonForm)
# form=form_factory(taxon))

def accept_nested(attribute, model):
# TODO: maybe instead of having a accept nested we can just subclass
# the form and use a wtfoms field enclosure: http://wtforms.readthedocs.io/en/latest/fields.html#field-enclosures
def decorator(f):
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
return decorator

# def accept_nested_resources(model_or_form_cls, *kwargs):
# form_cls = forms.form_class_factory(model_or_form_cls) \
# if isinstance(db.Model) else model_or_form_Cls

# class ExtendedForm(form_cls):

def process_nested_resource(param_name, resource_name):
values = request.params.get(param_name, None)
if values is None:
return

def forward_request(data):
print('data: ', data)
# TODO:

if not isinstance(values, (list, tuple)):
forward_request(values)
return

for val in values:
forward_request(val)


@resource.create
@login_required
@accept_nested('vernacular_names', model=VernacularName)
def create():
taxon = Taxon()
form = resource.save_request_params(taxon)
form = resource.save_request_params(taxon, form=TaxonForm())

process_nested_resource('vernacular_names', 'vernacular_name')

# TODO: accept vernacular names for create only

Expand Down Expand Up @@ -81,10 +143,12 @@ def update(id):
@resource.edit
@login_required
def edit(id):
taxon = Taxon.query.get_or_404(id)
taxon = Taxon.query \
.options(orm.joinedload('vernacular_names')) \
.get_or_404(id)
geographies = Geography.query.all()
return resource.render_html(taxon=taxon, geographies=geographies,
form=form_factory(taxon))
form=TaxonForm(obj=taxon))


@resource.destroy
Expand Down
18 changes: 12 additions & 6 deletions bauble/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from flask_wtf import Form
from sqlalchemy import inspect
from sqlalchemy.orm import Mapper
from wtforms_alchemy import (model_form_factory, model_form_meta_factory, null_or_unicode,
FormGenerator as _FormGenerator, ModelFormMeta)
from wtforms_alchemy import (model_form_factory, null_or_unicode,
FormGenerator as _FormGenerator)


BaseModelForm = model_form_factory(Form)
Expand Down Expand Up @@ -36,18 +36,24 @@ def select_field_kwargs(self, column):


@lru_cache()
def form_class_factory(model_cls):
def form_class_factory(model_cls, base_form_cls=BaseModelForm, **kwargs):
# equivalent to following but gives a nicer class name:
# class UserForm(BaseModelForm):
# class Meta:
# model = User
Meta = type('Meta', (object, ), {
options = {
'form_generator': FormGenerator,
'model': model_cls,
'exclude': ['created_at', 'updated_at'],
'include_foreign_keys': True
})
ModelForm = type('{}Form'.format(model_cls.__name__), (BaseModelForm, ), {
}
options.update(kwargs)

Meta = type('Meta', (object, ), options)

# dynamically define our form type that extends base
# print('model_cls: ', model_cls)
ModelForm = type('{}Form'.format(model_cls.__name__), (base_form_cls, ), {
'Meta': Meta
})
return ModelForm
Expand Down
6 changes: 4 additions & 2 deletions bauble/models/taxon.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from itertools import chain

from flask.ext.babel import gettext as _, ngettext as _n
from sqlalchemy import (func, Boolean, Column, Date, Enum, ForeignKey, Integer, String,
Text, UniqueConstraint)
from sqlalchemy import (func, Boolean, Column, Date, Enum, ForeignKey, Integer,
String, Text)
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.associationproxy import association_proxy

import bauble.db as db
from bauble.models.default_vernacular_name import DefaultVernacularName
import bauble.search as search
import bauble.utils as utils

class VNList(list):
"""
Expand Down
8 changes: 5 additions & 3 deletions bauble/resource.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from functools import partial, wraps
import inspect

from flask import abort, current_app, render_template, Blueprint
from flask import abort, current_app, render_template, request, Blueprint

import bauble.utils as utils
import bauble.db as db
Expand Down Expand Up @@ -94,9 +94,11 @@ def view_func(*args, **kwargs):
def render_json_errors(self, errors):
return utils.json_response(errors, status=422)


def save_request_params(self, model, form=None):
if form is None:
form = form_factory(model)
# a flask_wtf.form will get populated from request.form by default
# or request.json if the content type is json
form = form_factory(model) if form is None else form

if form.validate_on_submit():
form.populate_obj(model)
Expand Down
14 changes: 6 additions & 8 deletions bauble/templates/taxon/_form.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,12 @@
</div> <!-- .row -->

<div id="vernacular-names">
{% for vn in taxon.vernacular_names %}
<vernacular-name
taxon-id="{{ vn.taxon_id }}"
id="{{ vn.id }}"
name="{{ vn.name }}" language="{{ vn.language }}"
default="{{ vn.default }}"
v-on:remove="removeVernacularName({{ vn.id }})">
</vernacular-name>
{% for vn in form.vernacular_names %}
{{ vn['id'](type='hidden') }}
{{ vn['name'] }}
{{ vn.language }}
{{ vn.destroy_ }}
{#{ vn.method_(type='hidden') }#}
{% endfor %}
</div>
<button class="vn-add-btn btn" type="button"
Expand Down
58 changes: 50 additions & 8 deletions test/specs/taxon.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from faker import Faker
from flask import json

import bauble.db as db
from bauble.models import Taxon

faker = Faker()
Expand All @@ -25,12 +26,35 @@ def test_deserialize(session, taxon):
assert taxon_json['sp'] == taxon.sp


def test_form(session, taxon):
from bauble.forms import form_factory, BaseModelForm
def test_form(session, genus, taxon):
# from bauble.forms import form_factory, BaseModelForm
from bauble.controllers.taxon import TaxonForm
session.add(taxon)
session.commit()
form = form_factory(taxon)
assert isinstance(form, BaseModelForm)
data = {
'sp': faker.first_name(),
'genus_id': genus.id,
'vernacular_names': [{
'name': faker.first_name(),
'language': 'Macedamian',
'_destroy': True
}]
}
form = TaxonForm(data=data)
taxon = Taxon()
form.populate_obj(taxon)
for key, value in form.data.items():
print('{} - {}'.format(key, value))
db.session.add(taxon)
db.session.commit()
print('taxon.vernacular_names: ', taxon.vernacular_names)
assert len(data['vernacular_names']) == len(taxon.vernacular_names)
for vn in taxon.vernacular_names:
assert vn.id is not None
db.session.refresh(taxon)
assert taxon.id is not None
# form = form_factory(taxon)
# assert isinstance(form, BaseModelForm)


def test_index_taxon_json(client, session, taxon):
Expand All @@ -46,18 +70,36 @@ def test_post_taxon_json(client, session, genus):
session.add(genus)
session.commit()
sp = faker.first_name()
data = {'sp': sp, 'genus_id': genus.id}
resp = client.post('/taxon.json', data=data)
assert resp.status_code == 201
data = {
'sp': sp,
'genus_id': genus.id,
'vernacular_names': [{
'name': faker.first_name(),
'language': 'Macedamian'
}]
}
resp = client.post('/taxon.json', data=json.dumps(data),
content_type='application/json')
assert resp.status_code == 201, resp.data.decode('utf-8')
assert resp.json['id'] is not None
assert resp.json['sp'] == sp

taxon = Taxon.query.get_or_404(resp.json['id'])
assert len(data['vernacular_names']) == len(taxon.vernacular_names)


def test_post_taxon(client, session, genus):
session.add(genus)
session.commit()
sp = faker.first_name()
data = {'sp': sp, 'genus_id': genus.id}
data = {
'sp': sp,
'genus_id': genus.id,
'vernacular_names': [{
'name': faker.first_name(),
'language': 'Macedamian'
}]
}
resp = client.post('/taxon', data=data)
assert resp.status_code == 201
assert resp.mimetype == 'text/html'
Expand Down

0 comments on commit 6308ba2

Please sign in to comment.