wtf-peewee-3.0.6/0000755000175000017500000000000014706166541014044 5ustar cjwatsoncjwatsonwtf-peewee-3.0.6/.github/0000755000175000017500000000000014706166541015404 5ustar cjwatsoncjwatsonwtf-peewee-3.0.6/.github/workflows/0000755000175000017500000000000014706166541017441 5ustar cjwatsoncjwatsonwtf-peewee-3.0.6/.github/workflows/tests.yaml0000644000175000017500000000073214706166541021471 0ustar cjwatsoncjwatsonname: Tests on: [push] jobs: tests: name: ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [3.7, "3.10"] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: pip deps run: pip install peewee markupsafe wtforms - name: runtests run: python runtests.py wtf-peewee-3.0.6/wtfpeewee/0000755000175000017500000000000014706166541016037 5ustar cjwatsoncjwatsonwtf-peewee-3.0.6/wtfpeewee/__init__.py0000644000175000017500000000000014706166541020136 0ustar cjwatsoncjwatsonwtf-peewee-3.0.6/wtfpeewee/_compat.py0000644000175000017500000000037514706166541020040 0ustar cjwatsoncjwatsonimport sys PY2 = sys.version_info[0] == 2 if PY2: text_type = unicode string_types = (str, unicode) unichr = unichr reduce = reduce else: text_type = str string_types = (str,) unichr = chr from functools import reduce wtf-peewee-3.0.6/wtfpeewee/tests.py0000644000175000017500000004605614706166541017566 0ustar cjwatsoncjwatsonimport datetime import sys import unittest from peewee import * from wtforms import __version__ as wtforms_version from wtforms import fields as wtfields from wtforms.form import Form as WTForm from wtforms.validators import Regexp from wtfpeewee.fields import * from wtfpeewee.fields import wtf_choice from wtfpeewee.orm import model_form from wtfpeewee._compat import PY2 if not PY2: implements_to_string = lambda x: x else: def implements_to_string(cls): cls.__unicode__ = cls.__str__ cls.__str__ = lambda x: x.__unicode__().encode('utf-8') return cls test_db = SqliteDatabase(':memory:') class TestModel(Model): class Meta: database = test_db @implements_to_string class Blog(TestModel): title = CharField() def __str__(self): return self.title @implements_to_string class Entry(TestModel): pk = AutoField() blog = ForeignKeyField(Blog) title = CharField(verbose_name='Wacky title') content = TextField() pub_date = DateTimeField(default=datetime.datetime.now) def __str__(self): return '%s: %s' % (self.blog.title, self.title) class NullEntry(TestModel): blog = ForeignKeyField(Blog, null=True) class NullFieldsModel(TestModel): c = CharField(null=True) b = BooleanField(null=True) class ChoicesModel(TestModel): gender = CharField(choices=(('m', 'Male'), ('f', 'Female'))) status = IntegerField(choices=((1, 'One'), (2, 'Two')), null=True) salutation = CharField(null=True) true_or_false = BooleanField(choices=((True, 't'), (False, 'f'))) class BlankChoices(TestModel): status = IntegerField(choices=((1, 'One'), (2, 'Two')), null=True) class NonIntPKModel(TestModel): id = CharField(primary_key=True) value = CharField() BlogForm = model_form(Blog) EntryForm = model_form(Entry) NullFieldsModelForm = model_form(NullFieldsModel) ChoicesForm = model_form(ChoicesModel, field_args={'salutation': {'choices': (('mr', 'Mr.'), ('mrs', 'Mrs.'))}}) BlankChoicesForm = model_form(BlankChoices) NonIntPKForm = model_form(NonIntPKModel, allow_pk=True) class FakePost(dict): def getlist(self, key): val = self[key] if isinstance(val, list): return val return [val] class WTFPeeweeTestCase(unittest.TestCase): def setUp(self): NullEntry.drop_table(True) Entry.drop_table(True) Blog.drop_table(True) NullFieldsModel.drop_table(True) NonIntPKModel.drop_table(True) Blog.create_table() Entry.create_table() NullEntry.create_table() NullFieldsModel.create_table() NonIntPKModel.create_table() self.blog_a = Blog.create(title='a') self.blog_b = Blog.create(title='b') self.entry_a1 = Entry.create(blog=self.blog_a, title='a1', content='a1 content', pub_date=datetime.datetime(2011, 1, 1)) self.entry_a2 = Entry.create(blog=self.blog_a, title='a2', content='a2 content', pub_date=datetime.datetime(2011, 1, 2)) self.entry_b1 = Entry.create(blog=self.blog_b, title='b1', content='b1 content', pub_date=datetime.datetime(2011, 1, 1)) def assertChoices(self, c, expected): self.assertEqual(list(c.iter_choices()), [wtf_choice(*i) for i in expected]) def test_defaults(self): BlogFormDef = model_form(Blog, field_args={'title': {'default': 'hello world'}}) form = BlogFormDef() self.assertEqual(form.data, {'title': 'hello world'}) form = BlogFormDef(obj=self.blog_a) self.assertEqual(form.data, {'title': 'a'}) def test_duplicate_validators(self): ''' Test whether validators are duplicated when forms share field_args ''' shared_field_args = {'id': {'validators': [Regexp('test')]}} ValueIncludedForm = model_form(NonIntPKModel, field_args=shared_field_args, allow_pk=True) ValueExcludedForm = model_form(NonIntPKModel, field_args=shared_field_args, allow_pk=True, exclude=['value']) value_included_form = ValueIncludedForm() self.assertEqual(len(value_included_form.id.validators), 2) value_excluded_form = ValueExcludedForm() self.assertEqual(len(value_excluded_form.id.validators), 2) def test_non_int_pk(self): form = NonIntPKForm() self.assertEqual(form.data, {'value': None, 'id': None}) self.assertFalse(form.validate()) obj = NonIntPKModel(id='a', value='A') form = NonIntPKForm(obj=obj) self.assertEqual(form.data, {'value': 'A', 'id': 'a'}) self.assertTrue(form.validate()) form = NonIntPKForm(FakePost({'id': 'b', 'value': 'B'})) self.assertTrue(form.validate()) obj = NonIntPKModel() form.populate_obj(obj) self.assertEqual(obj.id, 'b') self.assertEqual(obj.value, 'B') self.assertEqual(NonIntPKModel.select().count(), 0) obj.save(True) self.assertEqual(NonIntPKModel.select().count(), 1) # its hard to validate unique-ness because a form may be updating #form = NonIntPKForm(FakePost({'id': 'b', 'value': 'C'})) #self.assertFalse(form.validate()) def test_choices(self): form = ChoicesForm() self.assertTrue(isinstance(form.gender, SelectChoicesField)) self.assertTrue(isinstance(form.status, SelectChoicesField)) self.assertTrue(isinstance(form.salutation, SelectChoicesField)) self.assertTrue(isinstance(form.true_or_false, wtfields.BooleanField)) self.assertChoices(form.gender, [ ('m', 'Male', False), ('f', 'Female', False)]) self.assertChoices(form.status, [ ('__None', '----------------', True), (1, 'One', False), (2, 'Two', False)]) self.assertChoices(form.salutation, [ ('__None', '----------------', True), ('mr', 'Mr.', False), ('mrs', 'Mrs.', False)]) choices_obj = ChoicesModel(gender='m', status=2, salutation=None) form = ChoicesForm(obj=choices_obj) self.assertEqual(form.data, {'gender': 'm', 'status': 2, 'salutation': None, 'true_or_false': False}) self.assertTrue(form.validate()) choices_obj = ChoicesModel(gender='f', status=1, salutation='mrs', true_or_false=True) form = ChoicesForm(obj=choices_obj) self.assertEqual(form.data, {'gender': 'f', 'status': 1, 'salutation': 'mrs', 'true_or_false': True}) self.assertTrue(form.validate()) choices_obj.gender = 'x' form = ChoicesForm(obj=choices_obj) self.assertFalse(form.validate()) if wtforms_version[0] == '2': errmsg = 'Not a valid choice' else: errmsg = 'Not a valid choice.' self.assertEqual(form.errors, {'gender': [errmsg]}) choices_obj.gender = 'm' choices_obj.status = '1' form = ChoicesForm(obj=choices_obj) self.assertEqual(form.status.data, 1) self.assertTrue(form.validate()) # "3" is not a valid status. form = ChoicesForm(FakePost({'status': '3'}), obj=choices_obj) # Invalid choice -- must be 1 or 2. self.assertFalse(form.validate()) self.assertTrue(list(form.errors), ['status']) # Nullable field with choices: choices_obj.status = None form = ChoicesForm(obj=choices_obj) self.assertEqual(form.status.data, None) self.assertTrue(form.validate()) self.assertFalse(list(form.errors), ['status']) # Not-nullable field with choices: choices_obj.gender = None form = ChoicesForm(obj=choices_obj) self.assertEqual(form.status.data, None) self.assertFalse(form.validate()) self.assertTrue(list(form.errors), ['gender']) def test_blank_choices(self): obj = BlankChoices(status=None) form = BlankChoicesForm(obj=obj) self.assertTrue(form.validate()) # Ensure that the "None" status value is set when populating an object # (overwriting a previous non-empty value). new_obj = BlankChoices(status=1) form.populate_obj(new_obj) self.assertTrue(new_obj.status is None) new_obj = BlankChoices(status=1) form = BlankChoicesForm(FakePost({'status': ''})) self.assertTrue(form.validate()) form.populate_obj(new_obj) self.assertTrue(new_obj.status is None) new_obj = BlankChoices() form = BlankChoicesForm(FakePost({'status': 1})) self.assertTrue(form.validate()) form.populate_obj(new_obj) self.assertEqual(new_obj.status, 1) form = BlankChoicesForm(FakePost({'status': 3})) self.assertFalse(form.validate()) def test_blog_form(self): form = BlogForm() self.assertEqual(list(form._fields.keys()), ['title']) self.assertTrue(isinstance(form.title, wtfields.StringField)) self.assertEqual(form.data, {'title': None}) def test_entry_form(self): form = EntryForm() self.assertEqual(sorted(form._fields.keys()), ['blog', 'content', 'pub_date', 'title']) self.assertTrue(isinstance(form.blog, ModelSelectField)) self.assertTrue(isinstance(form.content, wtfields.TextAreaField)) self.assertTrue(isinstance(form.pub_date, WPDateTimeField)) self.assertTrue(isinstance(form.title, wtfields.StringField)) self.assertEqual(form.title.label.text, 'Wacky title') self.assertEqual(form.blog.label.text, 'Blog') self.assertEqual(form.pub_date.label.text, 'Pub Date') # check that the default value appears self.assertTrue(isinstance(form.pub_date.data, datetime.datetime)) # check that the foreign key defaults to none self.assertEqual(form.blog.data, None) # check that the options look right self.assertChoices(form.blog, [ (self.blog_a._pk, u'a', False), (self.blog_b._pk, u'b', False)]) def test_blog_form_with_obj(self): form = BlogForm(obj=self.blog_a) self.assertEqual(form.data, {'title': 'a'}) self.assertTrue(form.validate()) def test_entry_form_with_obj(self): form = EntryForm(obj=self.entry_a1) self.assertEqual(form.data, { 'title': 'a1', 'content': 'a1 content', 'pub_date': datetime.datetime(2011, 1, 1), 'blog': self.blog_a, }) self.assertTrue(form.validate()) # check that the options look right self.assertChoices(form.blog, [ (self.blog_a._pk, u'a', True), (self.blog_b._pk, u'b', False)]) def test_blog_form_saving(self): form = BlogForm(FakePost({'title': 'new blog'})) self.assertTrue(form.validate()) blog = Blog() form.populate_obj(blog) self.assertEqual(blog.title, 'new blog') # no new blogs were created self.assertEqual(Blog.select().count(), 2) # explicitly calling save will create the new blog blog.save() # make sure we created a new blog self.assertEqual(Blog.select().count(), 3) form = BlogForm(FakePost({'title': 'a edited'}), obj=self.blog_a) self.assertTrue(form.validate()) form.populate_obj(self.blog_a) self.assertEqual(self.blog_a.title, 'a edited') self.blog_a.save() # make sure no new blogs were created self.assertEqual(Blog.select().count(), 3) # grab it from the database a = Blog.get(title='a edited') def test_entry_form_saving(self): # check count of entries self.assertEqual(Entry.select().count(), 3) form = EntryForm(FakePost({ 'title': 'new entry', 'content': 'some content', 'pub_date-date': '2011-02-01', 'pub_date-time': '00:00:00', 'blog': self.blog_b._pk, })) self.assertTrue(form.validate()) self.assertEqual(form.pub_date.data, datetime.datetime(2011, 2, 1)) self.assertEqual(form.blog.data, self.blog_b) entry = Entry() form.populate_obj(entry) # ensure entry count hasn't changed self.assertEqual(Entry.select().count(), 3) entry.save() self.assertEqual(Entry.select().count(), 4) self.assertEqual(self.blog_a.entry_set.count(), 2) self.assertEqual(self.blog_b.entry_set.count(), 2) # make sure the blog object came through ok self.assertEqual(entry.blog, self.blog_b) # edit entry a1 form = EntryForm(FakePost({ 'title': 'a1 edited', 'content': 'a1 content', 'pub_date': '2011-01-01 00:00:00', 'blog': self.blog_b._pk, }), obj=self.entry_a1) self.assertTrue(form.validate()) form.populate_obj(self.entry_a1) self.entry_a1.save() self.assertEqual(self.entry_a1.blog, self.blog_b) self.assertEqual(self.blog_a.entry_set.count(), 1) self.assertEqual(self.blog_b.entry_set.count(), 3) # pull from the db just to be 100% sure a1 = Entry.get(title='a1 edited') form = EntryForm(FakePost({ 'title': 'new', 'content': 'blah', 'pub_date': '2011-01-01 00:00:00', 'blog': 10000 })) self.assertFalse(form.validate()) def test_null_form_saving(self): form = NullFieldsModelForm(FakePost({'c': ''})) self.assertTrue(form.validate()) nfm = NullFieldsModel() form.populate_obj(nfm) self.assertEqual(nfm.c, None) # this is a bit odd, but since checkboxes do not send a value if they # are unchecked this will evaluate to false (and passing in an empty # string evalutes to true) since the wtforms booleanfield blindly coerces # to bool self.assertEqual(nfm.b, False) form = NullFieldsModelForm(FakePost({'c': '', 'b': ''})) self.assertTrue(form.validate()) nfm = NullFieldsModel() form.populate_obj(nfm) self.assertEqual(nfm.c, None) # again, this is for the purposes of documenting behavior -- nullable # booleanfields won't work without a custom field class # Passing an empty string will evalute to False # https://bitbucket.org/simplecodes/wtforms/commits/35c5f7182b7f0c62a4d4db7a1ec8719779b4b018 self.assertEqual(nfm.b, False) form = NullFieldsModelForm(FakePost({'c': 'test'})) self.assertTrue(form.validate()) nfm = NullFieldsModel() form.populate_obj(nfm) self.assertEqual(nfm.c, 'test') def test_form_with_only_exclude(self): frm = model_form(Entry, only=('title', 'content',))() self.assertEqual(sorted(frm._fields.keys()), ['content', 'title']) frm = model_form(Entry, exclude=('title', 'content',))() self.assertEqual(sorted(frm._fields.keys()), ['blog', 'pub_date']) def test_form_multiple(self): class TestForm(WTForm): blog = SelectMultipleQueryField(query=Blog.select()) frm = TestForm() self.assertChoices(frm.blog, [ (self.blog_a.id, 'a', False), (self.blog_b.id, 'b', False)]) frm = TestForm(FakePost({'blog': [self.blog_b.id]})) self.assertChoices(frm.blog, [ (self.blog_a.id, 'a', False), (self.blog_b.id, 'b', True)]) self.assertEqual(frm.blog.data, [self.blog_b]) self.assertTrue(frm.validate()) frm = TestForm(FakePost({'blog': [self.blog_b.id, self.blog_a.id]})) self.assertChoices(frm.blog, [ (self.blog_a.id, 'a', True), (self.blog_b.id, 'b', True)]) self.assertEqual(frm.blog.data, [self.blog_a, self.blog_b]) self.assertTrue(frm.validate()) bad_id = [x for x in range(1,4) if x not in [self.blog_a.id, self.blog_b.id]][0] frm = TestForm(FakePost({'blog': [self.blog_b.id, bad_id]})) self.assertTrue(frm.validate()) def test_hidden_field(self): class TestEntryForm(WTForm): blog = HiddenQueryField(query=Blog.select()) title = wtfields.StringField() content = wtfields.TextAreaField() form = TestEntryForm(FakePost({ 'title': 'new entry', 'content': 'some content', 'blog': self.blog_b._pk, })) # check the htmlz for the form's hidden field html = form._fields['blog']() self.assertEqual(html, u'' % self.blog_b._pk) self.assertTrue(form.validate()) self.assertEqual(form.blog.data, self.blog_b) entry = Entry() form.populate_obj(entry) # ensure entry count hasn't changed self.assertEqual(Entry.select().count(), 3) entry.save() self.assertEqual(Entry.select().count(), 4) self.assertEqual(self.blog_a.entry_set.count(), 2) self.assertEqual(self.blog_b.entry_set.count(), 2) # make sure the blog object came through ok self.assertEqual(entry.blog, self.blog_b) # edit entry a1 form = TestEntryForm(FakePost({ 'title': 'a1 edited', 'content': 'a1 content', 'blog': self.blog_b._pk, }), obj=self.entry_a1) # check the htmlz for the form's hidden field html = form._fields['blog']() self.assertEqual(html, u'' % self.blog_b._pk) self.assertTrue(form.validate()) form.populate_obj(self.entry_a1) self.entry_a1.save() self.assertEqual(self.entry_a1.blog, self.blog_b) self.assertEqual(self.blog_a.entry_set.count(), 1) self.assertEqual(self.blog_b.entry_set.count(), 3) # pull from the db just to be 100% sure a1 = Entry.get(title='a1 edited') def test_hidden_field_none(self): class TestNullEntryForm(WTForm): blog = HiddenQueryField(query=Blog.select()) form = TestNullEntryForm(FakePost({ 'blog': '', })) # check the htmlz for the form's hidden field html = form._fields['blog']() self.assertEqual(html, u'') self.assertTrue(form.validate()) self.assertEqual(form.blog.data, None) entry = NullEntry() form.populate_obj(entry) # ensure entry count hasn't changed self.assertEqual(NullEntry.select().count(), 0) entry.save() self.assertEqual(NullEntry.select().count(), 1) # make sure the blog object came through ok self.assertEqual(entry.blog, None) # edit entry a1 form = TestNullEntryForm(FakePost({ 'blog': None, }), obj=self.entry_a1) # check the htmlz for the form's hidden field html = form._fields['blog']() self.assertEqual(html, u'') self.assertTrue(form.validate()) if __name__ == '__main__': unittest.main(argv=sys.argv) wtf-peewee-3.0.6/wtfpeewee/orm.py0000644000175000017500000002226114706166541017211 0ustar cjwatsoncjwatson""" Tools for generating forms based on Peewee models (cribbed from wtforms.ext.django) """ from collections import namedtuple from collections import OrderedDict from wtforms import __version__ as wtforms_version from wtforms import Form from wtforms import fields as f from wtforms import validators from wtfpeewee.fields import ModelSelectField from wtfpeewee.fields import SelectChoicesField from wtfpeewee.fields import SelectQueryField from wtfpeewee.fields import WPDateField from wtfpeewee.fields import WPDateTimeField from wtfpeewee.fields import WPTimeField from wtfpeewee._compat import text_type from peewee import BareField from peewee import BigIntegerField from peewee import BlobField from peewee import BooleanField from peewee import CharField from peewee import DateField from peewee import DateTimeField from peewee import DecimalField from peewee import DoubleField from peewee import FloatField from peewee import ForeignKeyField from peewee import IntegerField from peewee import IPField from peewee import AutoField from peewee import SmallIntegerField from peewee import TextField from peewee import TimeField from peewee import TimestampField from peewee import UUIDField __all__ = ( 'FieldInfo', 'ModelConverter', 'model_fields', 'model_form') def handle_null_filter(data): if data == '': return None return data class ValueRequired(object): """ Custom validation class that differentiates between false-y values and truly blank values. See the implementation of DataRequired and InputRequired -- this class sits somewhere in the middle of them. """ if wtforms_version < '3.1.0': field_flags = ('required',) else: field_flags = {'required': True} def __init__(self, message=None): self.message = message def __call__(self, form, field): if field.data is None or isinstance(field.data, text_type) \ and not field.data.strip(): message = self.message or field.gettext('This field is required.') field.errors[:] = [] raise validators.StopValidation(message) FieldInfo = namedtuple('FieldInfo', ('name', 'field')) class ModelConverter(object): defaults = OrderedDict(( # Subclasses of other fields. (IPField, f.StringField), # Subclass of BigIntegerField. (TimestampField, WPDateTimeField), # Subclass of BigIntegerField. (AutoField, f.HiddenField), (BigIntegerField, f.IntegerField), (DoubleField, f.FloatField), (SmallIntegerField, f.IntegerField), # Base-classes. (BareField, f.StringField), (BlobField, f.TextAreaField), (BooleanField, f.BooleanField), (CharField, f.StringField), (DateField, WPDateField), (DateTimeField, WPDateTimeField), (DecimalField, f.DecimalField), (FloatField, f.FloatField), (IntegerField, f.IntegerField), (TextField, f.TextAreaField), (TimeField, WPTimeField), (UUIDField, f.StringField), )) coerce_defaults = { BigIntegerField: int, CharField: text_type, DoubleField: float, FloatField: float, IntegerField: int, SmallIntegerField: int, TextField: text_type, UUIDField: text_type, } def __init__(self, additional=None, additional_coerce=None, overrides=None): self.converters = {ForeignKeyField: self.handle_foreign_key} if additional: self.converters.update(additional) self.coerce_settings = dict(self.coerce_defaults) if additional_coerce: self.coerce_settings.update(additional_coerce) self.overrides = overrides or {} def handle_foreign_key(self, model, field, **kwargs): if field.null: kwargs['allow_blank'] = True if field.choices is not None: field_obj = SelectQueryField(query=field.choices, **kwargs) else: field_obj = ModelSelectField(model=field.rel_model, **kwargs) return FieldInfo(field.name, field_obj) def convert(self, model, field, field_args): kwargs = { 'label': field.verbose_name, 'validators': [], 'filters': [], 'default': field.default, 'description': field.help_text} if field_args: kwargs.update(field_args) if kwargs['validators']: # Create a copy of the list since we will be modifying it. kwargs['validators'] = list(kwargs['validators']) if field.null: # Treat empty string as None when converting. kwargs['filters'].append(handle_null_filter) if (field.null or (field.default is not None)) or ( field.choices and any(not (v) for v, _ in field.choices)): # We allow the field to be optional if: # 1. the field is null=True and can be blank. # 2. the field has a default value. kwargs['validators'].append(validators.Optional()) else: kwargs['validators'].append(ValueRequired()) if field.name in self.overrides: return FieldInfo(field.name, self.overrides[field.name](**kwargs)) # Allow custom-defined Peewee field classes to define their own conversion, # making it so that code which calls model_form() doesn't have to have special # cases, especially when called for the same peewee.Model from multiple places, or # when called in a generic context which the end-developer has less control over, # such as via flask-admin. if hasattr(field, 'wtf_field'): return FieldInfo(field.name, field.wtf_field(model, **kwargs)) for converter in self.converters: if isinstance(field, converter): return self.converters[converter](model, field, **kwargs) else: for converter in self.defaults: if not isinstance(field, converter): # Early-continue because it simplifies reading the following code. continue if issubclass(self.defaults[converter], f.FormField): # FormField fields (i.e. for nested forms) do not support # filters. kwargs.pop('filters') if field.choices or 'choices' in kwargs: choices = kwargs.pop('choices', field.choices) if converter in self.coerce_settings or 'coerce' in kwargs: coerce_fn = kwargs.pop('coerce', self.coerce_settings[converter]) allow_blank = kwargs.pop('allow_blank', field.null) kwargs.update({ 'choices': choices, 'coerce': coerce_fn, 'allow_blank': allow_blank}) return FieldInfo(field.name, SelectChoicesField(**kwargs)) return FieldInfo(field.name, self.defaults[converter](**kwargs)) raise AttributeError("There is not possible conversion " "for '%s'" % type(field)) def model_fields(model, allow_pk=False, only=None, exclude=None, field_args=None, converter=None): """ Generate a dictionary of fields for a given Peewee model. See `model_form` docstring for description of parameters. """ converter = converter or ModelConverter() field_args = field_args or {} model_fields = list(model._meta.sorted_fields) if not allow_pk: model_fields.pop(0) if only: model_fields = [x for x in model_fields if x.name in only] elif exclude: model_fields = [x for x in model_fields if x.name not in exclude] field_dict = {} for model_field in model_fields: name, field = converter.convert( model, model_field, field_args.get(model_field.name)) field_dict[name] = field return field_dict def model_form(model, base_class=Form, allow_pk=False, only=None, exclude=None, field_args=None, converter=None): """ Create a wtforms Form for a given Peewee model class:: from wtfpeewee.orm import model_form from myproject.myapp.models import User UserForm = model_form(User) :param model: A Peewee model class :param base_class: Base form class to extend from. Must be a ``wtforms.Form`` subclass. :param only: An optional iterable with the property names that should be included in the form. Only these properties will have fields. :param exclude: An optional iterable with the property names that should be excluded from the form. All other properties will have fields. :param field_args: An optional dictionary of field names mapping to keyword arguments used to construct each field object. :param converter: A converter to generate the fields based on the model properties. If not set, ``ModelConverter`` is used. """ field_dict = model_fields(model, allow_pk, only, exclude, field_args, converter) return type(model.__name__ + 'Form', (base_class, ), field_dict) wtf-peewee-3.0.6/wtfpeewee/fields.py0000644000175000017500000003250614706166541017665 0ustar cjwatsoncjwatson""" Useful form fields for use with the Peewee ORM. (cribbed from wtforms.ext.django.fields) """ import datetime import operator import warnings try: from markupsafe import Markup except ImportError: try: from wtforms.widgets import HTMLString as Markup except ImportError: raise ImportError('Could not import markupsafe.Markup. Please install ' 'markupsafe.') from wtforms import __version__ as wtforms_version from wtforms import fields, form, widgets from wtforms.fields import FormField, _unset_value from wtforms.validators import ValidationError from wtfpeewee._compat import text_type, string_types __all__ = ( 'ModelSelectField', 'ModelSelectMultipleField', 'ModelHiddenField', 'SelectQueryField', 'SelectMultipleQueryField', 'HiddenQueryField', 'SelectChoicesField', 'BooleanSelectField', 'WPTimeField', 'WPDateField', 'WPDateTimeField', ) class StaticAttributesMixin(object): attributes = {} def __call__(self, **kwargs): for key, value in self.attributes.items(): if key in kwargs: curr = kwargs[key] kwargs[key] = '%s %s' % (value, curr) return super(StaticAttributesMixin, self).__call__(**kwargs) if wtforms_version < '3.1.0': def wtf_choice(*args): return args else: def wtf_choice(*args): return args + ({},) class BooleanSelectField(fields.SelectFieldBase): widget = widgets.Select() def iter_choices(self): yield wtf_choice('1', 'True', self.data) yield wtf_choice('', 'False', not self.data) def process_data(self, value): try: self.data = bool(value) except (ValueError, TypeError): self.data = None def process_formdata(self, valuelist): if valuelist: try: self.data = bool(valuelist[0]) except ValueError: raise ValueError(self.gettext(u'Invalid Choice: could not coerce')) class WPTimeField(StaticAttributesMixin, fields.StringField): attributes = {'class': 'time-widget'} formats = ['%H:%M:%S', '%H:%M'] def _value(self): if self.raw_data: return u' '.join(self.raw_data) else: return self.data and self.data.strftime(self.formats[0]) or u'' def convert(self, time_str): for format in self.formats: try: return datetime.datetime.strptime(time_str, format).time() except ValueError: pass def process_formdata(self, valuelist): if valuelist: self.data = self.convert(' '.join(valuelist)) if self.data is None: raise ValueError(self.gettext(u'Not a valid time value')) class WPDateField(StaticAttributesMixin, fields.DateField): attributes = {'class': 'date-widget'} def datetime_widget(field, **kwargs): kwargs.setdefault('id', field.id) kwargs.setdefault('class', '') kwargs['class'] += ' datetime-widget' html = [] for subfield in field: html.append(subfield(**kwargs)) return Markup(u''.join(html)) def generate_datetime_form(validators=None): class _DateTimeForm(form.Form): date = WPDateField(validators=validators) time = WPTimeField(validators=validators) return _DateTimeForm class WPDateTimeField(FormField): widget = staticmethod(datetime_widget) def __init__(self, label='', validators=None, **kwargs): DynamicForm = generate_datetime_form(validators) super(WPDateTimeField, self).__init__( DynamicForm, label, validators=None, **kwargs) def process(self, formdata, data=_unset_value, **_): prefix = self.name + self.separator kwargs = {} if data is _unset_value: try: data = self.default() except TypeError: data = self.default if data and data is not _unset_value: kwargs['date'] = data.date() kwargs['time'] = data.time() self.form = self.form_class(formdata, prefix=prefix, **kwargs) def populate_obj(self, obj, name): setattr(obj, name, self.data) @property def data(self): date_data = self.date.data time_data = self.time.data or datetime.time(0, 0) if date_data: return datetime.datetime.combine(date_data, time_data) class ChosenSelectWidget(widgets.Select): """ `Chosen `_ styled select widget. You must include chosen.js for styling to work. """ def __call__(self, field, **kwargs): if field.allow_blank and not self.multiple: kwargs['data-role'] = u'chosenblank' else: kwargs['data-role'] = u'chosen' return super(ChosenSelectWidget, self).__call__(field, **kwargs) class SelectChoicesField(fields.SelectField): widget = ChosenSelectWidget() # all of this exists so i can get proper handling of None def __init__(self, label=None, validators=None, coerce=text_type, choices=None, allow_blank=False, blank_text=u'', **kwargs): super(SelectChoicesField, self).__init__(label, validators, coerce, choices, **kwargs) self.allow_blank = allow_blank self.blank_text = blank_text or '----------------' def iter_choices(self): if self.allow_blank: yield wtf_choice(u'__None', self.blank_text, self.data is None) for value, label in self.choices: yield wtf_choice(value, label, self.coerce(value) == self.data) def process_data(self, value): if value is None: self.data = None else: try: self.data = self.coerce(value) except (ValueError, TypeError): self.data = None def process_formdata(self, valuelist): if valuelist: if valuelist[0] == '__None': self.data = None else: try: self.data = self.coerce(valuelist[0]) except ValueError: raise ValueError(self.gettext(u'Invalid Choice: could not coerce')) def pre_validate(self, form): if self.allow_blank and self.data is None: return super(SelectChoicesField, self).pre_validate(form) class SelectQueryField(fields.SelectFieldBase): """ Given a SelectQuery either at initialization or inside a view, will display a select drop-down field of choices. The `data` property actually will store/keep an ORM model instance, not the ID. Submitting a choice which is not in the queryset will result in a validation error. Specify `get_label` to customize the label associated with each option. If a string, this is the name of an attribute on the model object to use as the label text. If a one-argument callable, this callable will be passed model instance and expected to return the label text. Otherwise, the model object's `__unicode__` will be used. If `allow_blank` is set to `True`, then a blank choice will be added to the top of the list. Selecting this choice will result in the `data` property being `None`. The label for the blank choice can be set by specifying the `blank_text` parameter. """ widget = ChosenSelectWidget() def __init__(self, label=None, validators=None, query=None, get_label=None, allow_blank=False, blank_text=u'', **kwargs): super(SelectQueryField, self).__init__(label, validators, **kwargs) self.allow_blank = allow_blank self.blank_text = blank_text or '----------------' self.query = query self.model = query.model self._set_data(None) if get_label is None: self.get_label = lambda o: text_type(o) elif isinstance(get_label, string_types): self.get_label = operator.attrgetter(get_label) else: self.get_label = get_label def get_model(self, pk): try: return self.query.where(self.model._meta.primary_key==pk).get() except self.model.DoesNotExist: pass def _get_data(self): if self._formdata is not None: self._set_data(self.get_model(self._formdata)) return self._data def _set_data(self, data): self._data = data self._formdata = None data = property(_get_data, _set_data) def __call__(self, **kwargs): if 'value' in kwargs: self._set_data(self.get_model(kwargs['value'])) return self.widget(self, **kwargs) def iter_choices(self): if self.allow_blank: yield wtf_choice(u'__None', self.blank_text, self.data is None) for obj in self.query.clone(): yield wtf_choice(obj._pk, self.get_label(obj), obj == self.data) def process_formdata(self, valuelist): if valuelist: if valuelist[0] == '__None': self.data = None else: self._data = None self._formdata = valuelist[0] def pre_validate(self, form): if self.data is not None: if not self.query.where(self.model._meta.primary_key==self.data._pk).exists(): raise ValidationError(self.gettext('Not a valid choice')) elif not self.allow_blank: raise ValidationError(self.gettext('Selection cannot be blank')) class SelectMultipleQueryField(SelectQueryField): widget = ChosenSelectWidget(multiple=True) def __init__(self, *args, **kwargs): kwargs.pop('allow_blank', None) super(SelectMultipleQueryField, self).__init__(*args, **kwargs) def get_model_list(self, pk_list): if pk_list: return list(self.query.where(self.model._meta.primary_key << pk_list)) return [] def _get_data(self): if self._formdata is not None: self._set_data(self.get_model_list(self._formdata)) return self._data or [] def _set_data(self, data): self._data = data self._formdata = None data = property(_get_data, _set_data) def __call__(self, **kwargs): if 'value' in kwargs: self._set_data(self.get_model_list(kwargs['value'])) return self.widget(self, **kwargs) def iter_choices(self): for obj in self.query.clone(): yield wtf_choice(obj._pk, self.get_label(obj), obj in self.data) def process_formdata(self, valuelist): if valuelist: self._data = [] self._formdata = list(map(int, valuelist)) def pre_validate(self, form): if self.data: id_list = [m._pk for m in self.data] if id_list and not self.query.where(self.model._meta.primary_key << id_list).count() == len(id_list): raise ValidationError(self.gettext('Not a valid choice')) class HiddenQueryField(fields.HiddenField): def __init__(self, label=None, validators=None, query=None, get_label=None, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) super(fields.HiddenField, self).__init__(label, validators, **kwargs) self.query = query self.model = query.model self._set_data(None) if get_label is None: self.get_label = lambda o: text_type(o) elif isinstance(get_label, basestring): self.get_label = operator.attrgetter(get_label) else: self.get_label = get_label def get_model(self, pk): try: return self.query.where(self.model._meta.primary_key==pk).get() except self.model.DoesNotExist: pass def _get_data(self): if self._formdata is not None: if self.allow_blank and self._formdata == '__None': self._set_data(None) else: self._set_data(self.get_model(self._formdata)) return self._data def _set_data(self, data): self._data = data self._formdata = None data = property(_get_data, _set_data) def __call__(self, **kwargs): if 'value' in kwargs: self._set_data(self.get_model(kwargs['value'])) return self.widget(self, **kwargs) def _value(self): return self.data and self.data._pk or '' def process_formdata(self, valuelist): if valuelist: model_id = valuelist[0] self._data = None self._formdata = model_id or None class ModelSelectField(SelectQueryField): """ Like a SelectQueryField, except takes a model class instead of a queryset and lists everything in it. """ def __init__(self, label=None, validators=None, model=None, **kwargs): super(ModelSelectField, self).__init__(label, validators, query=model.select(), **kwargs) class ModelSelectMultipleField(SelectMultipleQueryField): """ Like a SelectMultipleQueryField, except takes a model class instead of a queryset and lists everything in it. """ def __init__(self, label=None, validators=None, model=None, **kwargs): super(ModelSelectMultipleField, self).__init__(label, validators, query=model.select(), **kwargs) class ModelHiddenField(HiddenQueryField): """ Like a HiddenQueryField, except takes a model class instead of a queryset and lists everything in it. """ def __init__(self, label=None, validators=None, model=None, **kwargs): super(ModelHiddenField, self).__init__(label, validators, query=model.select(), **kwargs) wtf-peewee-3.0.6/README.md0000644000175000017500000000345514706166541015332 0ustar cjwatsoncjwatson# wtf-peewee this project, based on the code found in ``wtforms.ext``, provides a bridge between peewee models and wtforms, mapping model fields to form fields. ## example usage: first, create a couple basic models and then use the model_form class factory to create a form for the Entry model: ```python from peewee import * from wtfpeewee.orm import model_form import wtforms class PasswordField(TextField): """ Custom-defined field example. """ def wtf_field(self, model, **kwargs): return wtforms.PasswordField(**kwargs) class Blog(Model): name = CharField() def __unicode__(self): return self.name class Entry(Model): blog = ForeignKeyField(Blog) title = CharField() body = TextField() protected = PasswordField() def __unicode__(self): return self.title # create a form class for use with the Entry model EntryForm = model_form(Entry) ``` Example implementation for an "edit" view using Flask: ```python @app.route('/entries//', methods=['GET', 'POST']) def edit_entry(entry_id): try: entry = Entry.get(id=entry_id) except Entry.DoesNotExist: abort(404) if request.method == 'POST': form = EntryForm(request.form, obj=entry) if form.validate(): form.populate_obj(entry) entry.save() flash('Your entry has been saved') else: form = EntryForm(obj=entry) return render_template('blog/entry_edit.html', form=form, entry=entry) ``` Example template for above view: ```jinja {% extends "layout.html" %} {% block body %}

Edit {{ entry.title }}

{% for field in form %}

{{ field.label }} {{ field }}

{% endfor %}

{% endblock %} ``` wtf-peewee-3.0.6/setup.py0000644000175000017500000000163214706166541015560 0ustar cjwatsoncjwatsonfrom setuptools import setup setup( name='wtf-peewee', version='3.0.6', url='https://github.com/coleifer/wtf-peewee/', license='MIT', author='Charles Leifer', author_email='coleifer@gmail.com', description='WTForms integration for peewee models', packages=['wtfpeewee'], zip_safe=False, platforms='any', install_requires=[ 'peewee>=3.0.0', 'wtforms', ], classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], test_suite='runtests.runtests' ) wtf-peewee-3.0.6/runtests.py0000755000175000017500000000060314706166541016307 0ustar cjwatsoncjwatson#!/usr/bin/env python import sys import unittest from wtfpeewee import tests def runtests(*test_args): suite = unittest.TestLoader().loadTestsFromModule(tests) result = unittest.TextTestRunner(verbosity=2).run(suite) if result.failures: sys.exit(1) elif result.errors: sys.exit(2) sys.exit(0) if __name__ == '__main__': runtests(*sys.argv[1:]) wtf-peewee-3.0.6/LICENSE0000644000175000017500000000204214706166541015047 0ustar cjwatsoncjwatsonCopyright (c) 2010 Charles Leifer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wtf-peewee-3.0.6/MANIFEST.in0000644000175000017500000000012214706166541015575 0ustar cjwatsoncjwatsoninclude LICENSE include README.md include runtests.py recursive-include example *