Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

grouping field and varius fixes

  • Loading branch information...
commit 52906d55b3e66b736baf81f1acc1d858637e8a06 1 parent e1bb015
@fafhrd91 authored
View
1  .travis.yml
@@ -1,5 +1,6 @@
language: python
python:
+ - 2.6
- 2.7
- 3.2
- 3.3
View
8 CHANGES.txt
@@ -4,10 +4,18 @@ CHANGES
0.5.1 (Unreleased)
------------------
+- Added `GroupingField`
+
- BaseMultiChoiceField.missing value is [] by default
- copy field missing value during fieldset data extraction
+- Fixed form csrf support
+
+- Fixed 'checked' value for radio field
+
+- Do not use missing value if value is not validation
+
0.5 (12-21-2012)
----------------
View
10 examples/composite.py
@@ -68,9 +68,15 @@ def form_content(self):
'country': 'KZ'},
'description': 'Context description'}
- @pform.button2('Update', actype=pform.AC_PRIMARY)
- def update_handler(self, data):
+ @pform.button('Update', actype=pform.AC_PRIMARY)
+ def update_handler(self):
+ data, errors = self.extract()
+
pprint(data)
+ pprint(errors)
+ if errors:
+ self.add_error_message(errors)
+ return
self.request.add_message('Content has been updated.')
return HTTPFound(location='/')
View
28 pform/__init__.py
@@ -9,7 +9,7 @@
'All','Function','Regex','Email','Range', 'Length','OneOf',
- 'InputField', 'CompositeField',
+ 'InputField', 'CompositeField', 'GroupingField',
'VocabularyField', 'BaseChoiceField','BaseMultiChoiceField',
'TextField','IntegerField','FloatField',
@@ -25,6 +25,14 @@
'parse_date','includeme', 'reify',
]
+try:
+ from collections import OrderedDict
+except ImportError: # pragma: no cover
+ import collections
+ from ordereddict import OrderedDict
+ collections.OrderedDict = OrderedDict
+
+
from pyramid.decorator import reify
# validation
@@ -48,11 +56,6 @@
from pform.vocabulary import Term
from pform.vocabulary import Vocabulary
-SimpleTerm = Term
-SimpleVocabulary = Vocabulary
-SimpleVocabulary.from_items = SimpleVocabulary
-SimpleVocabulary.from_values = SimpleVocabulary
-
# validators
from pform.validator import All
from pform.validator import Function
@@ -64,7 +67,11 @@
# helper class
from pform.field import InputField
-from pform.composite import CompositeField
+
+# helper field classes
+from pform.fields import VocabularyField
+from pform.fields import BaseChoiceField
+from pform.fields import BaseMultiChoiceField
# fields
from pform.fields import TextField
@@ -84,10 +91,9 @@
from pform.fields import MultiSelectField
from pform.fields import TimezoneField
-# helper field classes
-from pform.fields import VocabularyField
-from pform.fields import BaseChoiceField
-from pform.fields import BaseMultiChoiceField
+# composite fields
+from pform.composite import CompositeField
+from pform.grouping import GroupingField
# forms
from pform.form import Form
View
54 pform/composite.py
@@ -1,16 +1,28 @@
""" Composite Field """
+import copy
+import pprint
+from player import render
+
+import pform
from pform.field import Field
from pform.fieldset import Fieldset
from pform.interfaces import null, Invalid
+class CompositeError(Invalid):
+
+ def __repr__(self):
+ return 'CompositeError<%s%s>:\n%s' % (
+ self.field.name, ': %s'%self.msg if self.msg else '',
+ pprint.pformat(self.errors, 4))
+
+
class CompositeField(Field):
""" Composit field """
fields = None
-
- tmpl_input = 'form:composite'
- tmpl_widget = 'form:widget-composite'
+ tmpl_input = None
+ tmpl_widget = null
def __init__(self, name, **kw):
super(CompositeField, self).__init__(name, **kw)
@@ -23,6 +35,20 @@ def __init__(self, name, **kw):
self.fields.prefix = '%s.'%self.name
+ def render(self):
+ tmpl = self.tmpl_input or 'form:composite'
+ return render(self.request, tmpl, self,
+ view=self, value=self.form_value)
+
+ def render_widget(self):
+ if self.tmpl_input is None:
+ tmpl = self.tmpl_widget or 'form:widget-composite'
+ else:
+ tmpl = self.tmpl_widget or 'form:widget'
+
+ return render(self.request, tmpl, self,
+ view=self, value=self.form_value)
+
def bind(self, request, prefix, value, params, context=None):
""" Bind field to value and request params """
if value in (null, None):
@@ -44,6 +70,8 @@ def set_id_prefix(self, prefix):
def update(self):
""" Update field, prepare field for rendering """
+ super(CompositeField, self).update()
+
for field in self.fields.values():
field.update()
@@ -51,24 +79,28 @@ def to_field(self, value):
""" convert form value to field value """
result = {}
errors = []
- for name, field in self.fields.items():
+ for name, val in value.items():
+ field = self.fields[name]
try:
- result[name] = field.to_field(value[name])
+ result[name] = field.to_field(val)
except Invalid as error:
error.name = name
errors.append(error)
+ if field.error is None:
+ field.error = error
if errors:
- raise Invalid(field=self, errors=errors)
+ raise CompositeError(field=self, errors=errors)
return result
def validate(self, value):
""" validate value """
errors = []
- for name, field in self.fields.items():
+ for name, val in value.items():
+ field = self.fields[name]
try:
- field.validate(value[name])
+ field.validate(val)
except Invalid as error:
error.name = name
errors.append(error)
@@ -76,7 +108,7 @@ def validate(self, value):
field.error = error
if errors:
- raise Invalid(field=self, errors=errors)
+ raise CompositeError(field=self, errors=errors)
if self.validator is not None:
self.validator(self, value)
@@ -85,8 +117,8 @@ def extract(self):
value = {}
for name, field in self.fields.items():
val = field.extract()
- if val is null:
- val = field.missing
+ if val is null and field.missing is not null:
+ val = copy.copy(field.missing)
value[name] = val
View
6 pform/field.py
@@ -74,8 +74,8 @@ def __init__(self, name, **kw):
self.__dict__.update(kw)
self.name = name
- self.title = kw.get('title', name.capitalize())
- self.description = kw.get('description', '')
+ self.title = kw.get('title', self.title or name.capitalize())
+ self.description = kw.get('description', self.description)
self.readonly = kw.get('readonly', None)
self.default = kw.get('default', self.default)
self.preparer = kw.get('preparer', None)
@@ -162,7 +162,7 @@ def render_widget(self):
view=self, value=self.form_value)
def __repr__(self):
- return '<%s %r>' % (self.__class__.__name__, self.name)
+ return '%s<%s>' % (self.__class__.__name__, self.name)
# Field metaclass
View
7 pform/fields.py
@@ -78,7 +78,7 @@ def to_field(self, value):
raise Invalid(self.error_msg, self, {'val': value})
def is_checked(self, term):
- return term.token == self.form_value
+ return 'checked' if term.token == self.form_value else None
def update(self):
super(BaseChoiceField, self).update()
@@ -137,7 +137,7 @@ def extract(self):
return value
def is_checked(self, term):
- return term.token in self.form_value
+ return 'checked' if term.token in self.form_value else None
def update(self):
super(BaseMultiChoiceField, self).update()
@@ -397,7 +397,6 @@ class RadioField(BaseChoiceField):
tmpl_input = 'form:radio'
-
@field('bool')
class BoolField(BaseChoiceField):
"""Boolean input widget. Field name is ``bool``."""
@@ -429,7 +428,7 @@ def update_items(self):
'name': self.name,
'value': self.no_value_token,
'label': self.prompt_message,
- 'checked': self.form_value is null,
+ 'checked': 'checked' if self.form_value is null else None,
'description': '',
})
View
9 pform/fieldset.py
@@ -134,15 +134,12 @@ def extract(self):
try:
field.validate(value)
- if field.preparer is not None:
- value = field.preparer(value)
except Invalid as e:
- value = null
- if field.missing is not null:
- value = copy.copy(field.missing)
-
errors.append(e)
+ if field.preparer is not None:
+ value = field.preparer(value)
+
if field.flat:
data.update(value)
else:
View
57 pform/grouping.py
@@ -0,0 +1,57 @@
+""" GroupingField """
+from player import render
+from pform.interfaces import null, Invalid
+from pform.fields import RadioField
+from pform.fieldset import Fieldset
+from pform.composite import CompositeField
+from pform.composite import CompositeError
+from pform.vocabulary import Term, Vocabulary
+
+
+class GroupingField(CompositeField):
+ """ Grouping field
+
+ ``key_name``: Name of group key name
+
+ """
+
+ key_name = ''
+
+ extract_all = False
+
+ def __init__(self, name, **kw):
+ super(GroupingField, self).__init__(name, **kw)
+
+ voc = Vocabulary(
+ *[Term(fname, fname, field.title)
+ for fname, field in self.fields.items()])
+
+ if not self.key_name:
+ self.key_name = name
+
+ self.fields = Fieldset(
+ RadioField(
+ self.key_name,
+ missing = voc[0].value,
+ default = voc[0].value,
+ required = False,
+ vocabulary = voc)) + self.fields
+
+ def render_widget(self):
+ if self.tmpl_input is None:
+ tmpl = self.tmpl_widget or 'form:widget-grouping'
+ else:
+ tmpl = self.tmpl_widget or 'form:widget'
+
+ return render(self.request, tmpl, self,
+ view=self, value=self.form_value)
+
+ def extract(self):
+ value = super(GroupingField, self).extract()
+
+ if not self.extract_all:
+ group = value[self.key_name]
+
+ return {self.key_name: group, group: value[group]}
+
+ return value
View
3  pform/interfaces.py
@@ -45,8 +45,7 @@ def __str__(self):
return get_localizer(request).translate(self.msg, mapping=self.mapping)
def __repr__(self):
- return 'Invalid%s(%s: <%s>)' % (
- ':%s'%self.name if self.name else '', self.field, self.msg)
+ return 'Invalid(%s: %s)' % (self.field or self.name or '', self.msg)
def __contains__(self, name):
""" Check for subexception """
View
2  pform/templates/form.jinja2
@@ -22,7 +22,7 @@
{{request.render_tmpl(context.tmpl_actions, context)|safe}}
{%if context.csrf%}
- <input type="hidden" name="{{context.csrf_name}}" value="{{context.token}}" />
+ <input type="hidden" name="{{context.csrf_name}}" value="{{context.csrf_token}}" />
{%endif%}
<input type="hidden" name="__form_identity__" value="{{context.id}}" />
</form>
View
15 pform/templates/widget-composite.jinja2
@@ -4,18 +4,25 @@
{% for field in context.fields.values() %}
{% set error = field.error %}
- <div class="{{'control-group error' if error else ''}}">
- <label class="control-label" style="font-size:75%"
+ <div{{{'class': 'control-group error' if error else None}|xmlattr}}>
+ <label class="control-label"
for="{{field.id}}" title="{{field.description}}">
- {{field.title|safe}} {% if field.required %}*{% endif %}
+ <small class="muted">{{field.title|safe}}</small>
</label>
<div class="controls">
{{field.render()|safe}}
{% if error %}
<span class="help-inline">{{error|safe}}</span>
+ {% elif field.required %}
+ *
{% endif %}
- <p class="help-block">{{context.description|safe}}</p>
+ <p class="help-block"></p>
</div>
</div>
{% endfor %}
+ {% if context.description %}
+ <div class="controls">
+ <p class="help-block">{{context.description|safe}}</p>
+ </div>
+ {% endif %}
</div>
View
32 pform/templates/widget-grouping.jinja2
@@ -0,0 +1,32 @@
+<div class="control-group">
+ <label class="control-label">{{context.title|safe}}</label>
+ <div class="clearfix"></div>
+
+ {% set grouping = context.fields[context.key_name] %}
+
+ {% for item in grouping.items %}
+ {% set field = context.fields[item['value']] %}
+ <div{{{'class': 'control-group error' if field.error else None}|xmlattr}}>
+ <label class="control-label"
+ for="{{field.id}}" title="{{field.description}}">
+ <small class="muted">{{field.title|safe}}</small>
+ <input type="radio" {{grouping.get_html_attrs(**item)|xmlattr}}>
+ </label>
+ <div class="controls">
+ {{field.render()|safe}}
+ {% if field.error %}
+ <span class="help-inline">{{field.error|safe}}</span>
+ {% elif field.required %}
+ *
+ {% endif %}
+ <p class="help-block"></p>
+ </div>
+ </div>
+ {% endfor %}
+
+ {% if context.description %}
+ <div class="controls">
+ <p class="help-block">{{context.description|safe}}</p>
+ </div>
+ {% endif %}
+</div>
View
9 pform/tests/base.py
@@ -1,8 +1,15 @@
-from unittest import TestCase
+import sys
from pyramid import testing
from pyramid.interfaces import IRequest
from pyramid.interfaces import IRequestExtensions
+if sys.version_info[:2] == (2, 6):
+ from unittest2 import TestCase
+ TestCase
+else:
+ from unittest import TestCase
+
+
def strip(text):
return ' '.join([s.strip() for s in text.split('\n')])
View
27 pform/tests/test_composite.py
@@ -73,6 +73,33 @@ def test_extract(self):
result = widget.extract()
self.assertEqual({'lastname': '', 'firstname': 'Nikolay'}, result)
+ def test_extract_missing(self):
+ """ Extract returns missing """
+ fields=(pform.TextField('firstname', missing='name'),
+ pform.TextField('lastname'))
+
+ field = pform.CompositeField('test', fields=fields)
+
+ widget = field.bind(self.request, '', None, {})
+
+ result = widget.extract()
+ self.assertEqual({'lastname': fields[1].missing,
+ 'firstname': fields[0].missing}, result)
+
+ def test_form_extract_missing(self):
+ """ Extract form data, composite field no data """
+ fields=(pform.TextField('firstname', missing='name'),
+ pform.TextField('lastname'))
+
+ field = pform.CompositeField('test', fields=fields)
+
+ form = pform.Form(object(), self.request, fields=(field,))
+ form.update_form()
+
+ data, errors = form.extract()
+ self.assertEqual({'lastname': fields[1].missing,
+ 'firstname': fields[0].missing}, data['test'])
+
def test_validate(self):
""" Validate data """
field = pform.CompositeField(
View
4 pform/tests/test_field.py
@@ -31,7 +31,7 @@ def test_field_ctor(self):
self.assertEqual(field.validator, 'validator')
self.assertEqual(field.custom_attr, 'Custom attr')
- self.assertEqual(repr(field), "<Field 'test'>")
+ self.assertEqual(repr(field), "Field<test>")
def test_field_bind(self):
orig_field = pform.Field(
@@ -61,7 +61,7 @@ def test_field_bind(self):
self.assertIsNone(field.context)
self.assertIsNone(orig_field.context)
- self.assertEqual(repr(field), "<Field 'field.test'>")
+ self.assertEqual(repr(field), "Field<field.test>")
context = object()
field = orig_field.bind(object(), 'field.', pform.null, {}, context)
View
10 pform/tests/test_fields.py
@@ -381,13 +381,13 @@ def test_vocabulary_field(self):
field.update()
self.assertEqual(field.items,
- [{'checked': True,
+ [{'checked': 'checked',
'description': None,
'id': 'test-0',
'label': 'One',
'name': 'test',
'value': 'one'},
- {'checked': False,
+ {'checked': None,
'description': None,
'id': 'test-1',
'label': 'Two',
@@ -400,19 +400,19 @@ def test_vocabulary_field(self):
field.update()
self.assertEqual(field.items,
- [{'checked': False,
+ [{'checked': None,
'description': '',
'id': 'test-novalue',
'label': 'select a value ...',
'name': 'test',
'value': '--NOVALUE--'},
- {'checked': True,
+ {'checked': 'checked',
'description': None,
'id': 'test-0',
'label': 'One',
'name': 'test',
'value': 'one'},
- {'checked': False,
+ {'checked': None,
'description': None,
'id': 'test-1',
'label': 'Two',
View
2  pform/tests/test_fieldset.py
@@ -283,7 +283,7 @@ def test_fieldset_errors(self):
self.assertEqual(errors.msg, {'test': 'error1', 'test1': 'error2'})
self.assertEqual(str(err1), "error1")
- self.assertEqual(repr(err1), "Invalid(<TextField 'test'>: <error1>)")
+ self.assertEqual(repr(err1), "Invalid(TextField<test>: error1)")
self.assertEqual(str(pform.null), '<widget.null>')
def test_fieldset_contains_by_fieldname(self):
View
5 pform/tests/test_vocabulary.py
@@ -93,6 +93,7 @@ def test_term_ctor(self):
t = vocabulary.Term(1, "One")
self.assertEqual(t.value, 1)
self.assertEqual(t.token, "One")
+ self.assertEqual(repr(t), 'Term<"1:One:1">')
def test_term_ctor_title(self):
t = vocabulary.Term(1)
@@ -128,6 +129,10 @@ def test_iter_and_get_term(self):
self.assert_(v.get_term(term.value) is term)
self.assert_(v.get_term_bytoken(term.token) is term)
+ def test_getitem(self):
+ t = self.list_vocab[0]
+ self.assertIs(t, self.list_vocab.get_term(1))
+
def test_getvalue(self):
self.assertEqual(self.items_vocab.get_value('one'), 1)
self.assertRaises(LookupError, self.items_vocab.get_value, 'unknown')
View
8 pform/vocabulary.py
@@ -23,6 +23,11 @@ def __init__(self, value, token=None,
self.title = title
self.description = description
+ def __str__(self):
+ return 'Term<"%s:%s:%s">'%(self.value, self.token, self.title)
+
+ __repr__ = __str__
+
@implementer(IVocabulary)
class Vocabulary(object):
@@ -96,3 +101,6 @@ def __iter__(self):
def __len__(self):
return len(self.by_value)
+
+ def __getitem__(self, index):
+ return self._terms[index]
View
10 setup.py
@@ -1,16 +1,23 @@
import os
+import sys
from setuptools import setup, find_packages
version = '0.5.1'
install_requires = ['setuptools',
'pyramid >= 1.4',
- 'pyramid_amdjs',
+ 'pyramid_amdjs >= 0.3',
'pyramid_jinja2',
'player >= 0.6',
'pytz',
]
+if sys.version_info[:2] == (2, 6):
+ install_requires.extend((
+ 'argparse',
+ 'ordereddict',
+ 'unittest2'))
+
tests_require = install_requires + ['nose', 'mock']
@@ -25,6 +32,7 @@ def read(f):
classifiers=[
"Intended Audience :: Developers",
"Programming Language :: Python",
+ "Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
Please sign in to comment.
Something went wrong with that request. Please try again.