pax_global_header00006660000000000000000000000064147166104230014516gustar00rootroot0000000000000052 comment=3845ac24196d7a34c3ba9a05289bed73008bce8c wtforms-alchemy-0.19.0/000077500000000000000000000000001471661042300147265ustar00rootroot00000000000000wtforms-alchemy-0.19.0/.github/000077500000000000000000000000001471661042300162665ustar00rootroot00000000000000wtforms-alchemy-0.19.0/.github/workflows/000077500000000000000000000000001471661042300203235ustar00rootroot00000000000000wtforms-alchemy-0.19.0/.github/workflows/lint.yml000066400000000000000000000007021471661042300220130ustar00rootroot00000000000000name: Lint on: - push - pull_request jobs: test: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 - name: Install dependencies run: | python -m pip install --upgrade pip pip install .[test] - name: Run linting run: | ruff check . ruff format --check wtforms-alchemy-0.19.0/.github/workflows/test.yaml000066400000000000000000000013771471661042300221760ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: test: name: Python ${{ matrix.python-version }} + SQLAlchemy ${{ matrix.sqlalchemy-version }} + WTForms ${{ matrix.wtforms-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] sqlalchemy-version: ["1.4", "2.0"] wtforms-version: ["3.1", "3.2"] steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install tox run: pip install tox - name: Run tests run: tox -e sqlalchemy${{ matrix.sqlalchemy-version }}-wtforms${{ matrix.wtforms-version }} wtforms-alchemy-0.19.0/.gitignore000066400000000000000000000004031471661042300167130ustar00rootroot00000000000000*.py[co] # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg # Built docs docs/_build/ wtforms-alchemy-0.19.0/CHANGES.rst000066400000000000000000000260031471661042300165310ustar00rootroot00000000000000Changelog ========= Here you can see the full list of changes between each WTForms-Alchemy release. 0.19.0 (2024-11-18) ^^^^^^^^^^^^^^^^^^^ - Dropped support for Python 3.8 and earlier. The minimum supported Python version is now 3.9. - Dropped support for SQLAlchemy 1.3 and earlier. The minimum supported SQLAlchemy version is now 1.4. - Added support for Python 3.10–3.13. - Added support for SQLAlchemy 2.0. - Added support for WTForms 3.2. The minimum supported WTForms version is now 3.1. 0.18.0 (2021-12-21) ^^^^^^^^^^^^^^^^^^^ - Dropped WTForms 1.0 support - Added WTForms 3.0 and SA 1.4 support - Dropped py35 support 0.17.0 (2020-06-02) ^^^^^^^^^^^^^^^^^^^ - Dropped py27, py33 and py34 support 0.16.9 (2019-03-06) ^^^^^^^^^^^^^^^^^^^ - Added support for JSON type in TypeMap (#142, pull request courtesy of fedExpress) 0.16.8 (2018-12-04) ^^^^^^^^^^^^^^^^^^^ - Fixed QuerySelectField.query allowing no results (#136, pull request courtesy of TrilceAC) 0.16.7 (2018-05-07) ^^^^^^^^^^^^^^^^^^^ - Fixed UnknownTypeException being thrown correctly for unsupported types (#131, pull request courtesy of tvuotila) 0.16.6 (2018-01-21) ^^^^^^^^^^^^^^^^^^^ - Added SQLAlchemy 1.2 support 0.16.5 (2017-07-29) ^^^^^^^^^^^^^^^^^^^ - Fixed GroupedQuerySelectMultipleField validator to support empty data (#123, pull request courtesy of superosku) 0.16.4 (2017-07-29) ^^^^^^^^^^^^^^^^^^^ - Fixed GroupedQuerySelectMultipleField validator (#121, pull request courtesy of superosku) 0.16.3 (2017-06-25) ^^^^^^^^^^^^^^^^^^^ - Fixed ChoiceType conversion for Enums (#112, pull request courtesy of fayazkhan) 0.16.2 (2017-02-28) ^^^^^^^^^^^^^^^^^^^ - Added GroupedQueryMultipleSelectField (#113, pull request courtesy of adarshk7) 0.16.1 (2016-05-11) ^^^^^^^^^^^^^^^^^^^ - Updated SQLAlchemy-Utils requirement to 0.32.6 - Fixed PhoneNumberType conversion (#102) 0.16.0 (2016-04-20) ^^^^^^^^^^^^^^^^^^^ - Dropped python 2.6 support - Made PhoneNumberField work correctly together with DataRequired (#101, pull request courtesy of jmagnusson) 0.15.0 (2016-01-27) ^^^^^^^^^^^^^^^^^^^ - Moved GroupedQuerySelectField from WTForms-Components package to WTForms-Alchemy - Moved WeekdaysField from WTForms-Components package to WTForms-Alchemy - Moved PhoneNumberField from WTForms-Components package to WTForms-Alchemy - Moved Unique validator from WTForms-Components package to WTForms-Alchemy 0.14.0 (2016-01-23) ^^^^^^^^^^^^^^^^^^^ - Added QuerySelectField and QuerySelectMultipleField which were deprecated from WTForms as of version 2.1 0.13.3 (2015-06-17) ^^^^^^^^^^^^^^^^^^^ - Removed ClassMap's inheritance sorting. This never really worked properly and resulted in weird undeterministic bugs on Python 3. 0.13.2 (2015-05-21) ^^^^^^^^^^^^^^^^^^^ - Added support for callables in type map argument 0.13.1 (2015-04-19) ^^^^^^^^^^^^^^^^^^^ - Added flake8 checks - Added isort checks - Fixed country import caused by SQLAlchemy-Utils 0.30.0 - Update SQLAlchemy-Utils dependency to 0.30.0 0.13.0 (2014-10-14) ^^^^^^^^^^^^^^^^^^^ - Made all default validators configurable in model_form_factory - Added support for disabling default validators 0.12.9 (2014-08-30) ^^^^^^^^^^^^^^^^^^^ - Added support for composite primary keys in ModelFieldList 0.12.8 (2014-07-28) ^^^^^^^^^^^^^^^^^^^ - Added support for URLType of SQLAlchemy-Utils 0.12.7 (2014-07-21) ^^^^^^^^^^^^^^^^^^^ - Fix ModelFieldList handling of simultaneous deletes and updates 0.12.6 (2014-06-12) ^^^^^^^^^^^^^^^^^^^ - Fix various issues with new-style classes 0.12.5 (2014-05-29) ^^^^^^^^^^^^^^^^^^^ - Added CountryField - Added CountryType to CountryField conversion - Fixed various issues with column aliases 0.12.4 (2014-03-26) ^^^^^^^^^^^^^^^^^^^ - Added WeekDaysType to WeekDaysField conversion 0.12.3 (2014-03-24) ^^^^^^^^^^^^^^^^^^^ - Fixed ChoiceType coercion for SelectFields 0.12.2 (2014-02-20) ^^^^^^^^^^^^^^^^^^^ - New configuration option: attr_errors - Min and max info attributes generate NumberRange validator for Numeric, Float, IntRangeType and NumericRangeType columns 0.12.1 (2014-02-13) ^^^^^^^^^^^^^^^^^^^ - Updated SQLAlchemy-i18n optional dependency to 0.8.2 0.12.0 (2013-12-19) ^^^^^^^^^^^^^^^^^^^ - Added support for SQLAlchemy-Utils range types IntRange, NumericRange, DateRange and DateTimeRange - Deprecated support for NumberRangeField - Updated SQLAlchemy-Utils dependency to 0.23.1 - Updated WTForms-Components dependency to 0.9.0 0.11.0 (2013-12-19) ^^^^^^^^^^^^^^^^^^^ - Added configurable default validators - Fixed ModelFieldList processing 0.10.0 (2013-12-16) ^^^^^^^^^^^^^^^^^^^ - Replaced assign_required configuration option with not_null_validator for more fine grained control of not null validation - Replaced not_null_str_validator with not_null_validator_type_map 0.9.3 (2013-12-12) ^^^^^^^^^^^^^^^^^^ - Support for hybrid properties that return column properties - Better exception messages for properties that are not of type ColumnProperty - Support for class level type map customization 0.9.2 (2013-12-11) ^^^^^^^^^^^^^^^^^^ - Smarter object value inspection for ModelFieldList - Changed ModelFieldList default population strategy to 'update' instead of 'replace' 0.9.1 (2013-12-03) ^^^^^^^^^^^^^^^^^^ - Fixed property alias handling (issue #46) 0.9.0 (2013-11-30) ^^^^^^^^^^^^^^^^^^ - Initial WTForms 2.0 support - New configuration options: not_null_validator, not_null_str_validator 0.8.6 (2013-11-18) ^^^^^^^^^^^^^^^^^^ - Form fields now generated in class initialization time rather than on form object initialization 0.8.5 (2013-11-13) ^^^^^^^^^^^^^^^^^^ - Added Numeric type scale to DecimalField places conversion 0.8.4 (2013-11-11) ^^^^^^^^^^^^^^^^^^ - Declaration order of model fields now preserved in generated forms 0.8.3 (2013-10-28) ^^^^^^^^^^^^^^^^^^ - Added Python 2.6 support (supported versions now 2.6, 2.7 and 3.3) - Enhanced coerce func generator 0.8.2 (2013-10-25) ^^^^^^^^^^^^^^^^^^ - TypeDecorator derived type support SelectField coerce callable generator 0.8.1 (2013-10-24) ^^^^^^^^^^^^^^^^^^ - Added support for SQLAlchemy-Utils ChoiceType - Updated SQLAlchemy-Utils dependency to 0.18.0 0.8.0 (2013-10-11) ^^^^^^^^^^^^^^^^^^ - Fixed None value handling in string stripping when strip_string_fields option is enabled - Python 3 support - ModelFormMeta now configurable 0.7.15 (2013-09-06) ^^^^^^^^^^^^^^^^^^^ - Form generation now understands column aliases 0.7.14 (2013-08-27) ^^^^^^^^^^^^^^^^^^^ - Length validators only assigned to string typed columns 0.7.13 (2013-08-22) ^^^^^^^^^^^^^^^^^^^ - Model column_property methods now skipped in model generation process 0.7.12 (2013-08-18) ^^^^^^^^^^^^^^^^^^^ - Updated SQLAlchemy-Utils dependency to 0.16.7 - Updated SQLAlchemy-i18n dependency to 0.6.3 0.7.11 (2013-08-05) ^^^^^^^^^^^^^^^^^^^ - Added configuration skip_unknown_types to silently skip columns with types WTForms-Alchemy does not understand 0.7.10 (2013-08-01) ^^^^^^^^^^^^^^^^^^^ - DecimalField with scales and choices now generate SelectField as expected 0.7.9 (2013-08-01) ^^^^^^^^^^^^^^^^^^ - TSVectorType columns excluded by default 0.7.8 (2013-07-31) ^^^^^^^^^^^^^^^^^^ - String typed columns now convert to WTForms-Components StringFields instead of WTForms TextFields 0.7.7 (2013-07-31) ^^^^^^^^^^^^^^^^^^ - HTML5 step widget param support added - Updated WTForms-Components dependency to 0.6.6 0.7.6 (2013-07-24) ^^^^^^^^^^^^^^^^^^ - TypeDecorator support added 0.7.5 (2013-05-30) ^^^^^^^^^^^^^^^^^^ - Fixed _obj setting to better cope with wtforms_components unique validator 0.7.4 (2013-05-30) ^^^^^^^^^^^^^^^^^^ - Fixed min and max arg handling when using zero values 0.7.3 (2013-05-24) ^^^^^^^^^^^^^^^^^^ - Fixed ModelFieldList object population when using 'update' population strategy 0.7.2 (2013-05-24) ^^^^^^^^^^^^^^^^^^ - Updated WTForms-Components dependency to 0.6.3 - Made type conversion use WTForms-Components HTML5 fields 0.7.1 (2013-05-23) ^^^^^^^^^^^^^^^^^^ - DataRequired validator now added to not nullable booleans by default 0.7.0 (2013-05-14) ^^^^^^^^^^^^^^^^^^ - SQLAlchemy-i18n support added 0.6.0 (2013-05-07) ^^^^^^^^^^^^^^^^^^ - Updated WTForms dependency to 1.0.4 - Updated WTForms-Components dependency to 0.5.5 - EmailType now converts to HTML5 EmailField - Integer now converts to HTML5 IntegerField - Numeric now converts to HTML5 DecimalField - Date now converts to HTML5 DateField - DateTime now converts to HTML5 DateTimeField 0.5.7 (2013-05-03) ^^^^^^^^^^^^^^^^^^ - Fixed trim function for None values 0.5.6 (2013-05-02) ^^^^^^^^^^^^^^^^^^ - Column trim option added for fine-grained control of string field trimming 0.5.5 (2013-05-02) ^^^^^^^^^^^^^^^^^^ - Bug fix: strip_string_fields applied only for string fields 0.5.4 (2013-05-02) ^^^^^^^^^^^^^^^^^^ - Possibility to give default configuration for model_form_factory function - strip_string_fields configuration option 0.5.3 (2013-04-30) ^^^^^^^^^^^^^^^^^^ - Updated SQLAlchemy-Utils dependency to 0.10.0 - Updated WTForms-Components dependency to 0.5.4 - Added support for ColorType 0.5.2 (2013-04-25) ^^^^^^^^^^^^^^^^^^ - Added custom widget support - Added custom filters support 0.5.1 (2013-04-16) ^^^^^^^^^^^^^^^^^^ - Updated SQLAlchemy-Utils dependency to 0.9.1 - Updated WTForms-Components dependency to 0.5.2 - Fixed Email validator auto-assigning for EmailType - Smarter type conversion for subclassed types - Fixed ModelFormField update handling 0.5.0 (2013-04-12) ^^^^^^^^^^^^^^^^^^ - Updated SQLAlchemy dependency to 0.8 - Completely rewritten ModelFieldList implementation 0.4.5 (2013-03-27) ^^^^^^^^^^^^^^^^^^ - Updated WTForms-Components dependencies - Updated docs 0.4.4 (2013-03-27) ^^^^^^^^^^^^^^^^^^ - Updated WTForms-Components and SQLAlchemy-Utils dependencies 0.4.3 (2013-03-26) ^^^^^^^^^^^^^^^^^^ - Disalbed length validation for PhoneNumberType 0.4.2 (2013-03-26) ^^^^^^^^^^^^^^^^^^ - Added conversion from NumberRangeType to NumberRangeField 0.4.1 (2013-03-21) ^^^^^^^^^^^^^^^^^^ - Added conversion from PhoneNumberType to PhoneNumberField 0.4 (2013-03-15) ^^^^^^^^^^^^^^^^ - Moved custome fields, validators and widgets to WTForms-Components package 0.3.3 (2013-03-14) ^^^^^^^^^^^^^^^^^^ - Added handling of form_field_class = None 0.3.2 (2013-03-14) ^^^^^^^^^^^^^^^^^^ - Added custom field class attribute 0.3.1 (2013-03-01) ^^^^^^^^^^^^^^^^^^ - Better exception messages 0.3.0 (2013-03-01) ^^^^^^^^^^^^^^^^^^ - New unique validator syntax 0.2.5 (2013-02-16) ^^^^^^^^^^^^^^^^^^ - API documentation 0.2.4 (2013-02-08) ^^^^^^^^^^^^^^^^^^ - Enhanced unique validator - Documented new unique validator 0.2.3 (2012-11-26) ^^^^^^^^^^^^^^^^^^ - Another fix for empty choices handling 0.2.2 (2012-11-26) ^^^^^^^^^^^^^^^^^^ - Fixed empty choices handling for string fields 0.2.1 (2012-11-22) ^^^^^^^^^^^^^^^^^^ - If validator - Chain validator 0.2 (2012-11-05) ^^^^^^^^^^^^^^^^^^ - DateRange validator - SelectField with optgroup support 0.1.1 ^^^^^ - Added smart one-to-one and one-to-many relationship population 0.1.0 ^^^^^ - Initial public release wtforms-alchemy-0.19.0/LICENSE000066400000000000000000000026351471661042300157410ustar00rootroot00000000000000Copyright (c) 2012, Konsta Vesterinen All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. wtforms-alchemy-0.19.0/MANIFEST.in000066400000000000000000000003011471661042300164560ustar00rootroot00000000000000include CHANGES.rst LICENSE README.rst recursive-include tests * recursive-exclude tests *.pyc recursive-include docs * recursive-exclude docs *.pyc prune docs/_build exclude docs/_themes/.git wtforms-alchemy-0.19.0/README.rst000066400000000000000000000011271471661042300164160ustar00rootroot00000000000000WTForms-Alchemy =============== |Version Status| |Downloads| Tools for creating WTForms forms from SQLAlchemy models Resources --------- - `Documentation `_ - `Issue Tracker `_ - `Code `_ .. |Version Status| image:: https://img.shields.io/pypi/v/WTForms-Alchemy.svg :target: https://pypi.python.org/pypi/WTForms-Alchemy/ .. |Downloads| image:: https://img.shields.io/pypi/dm/WTForms-Alchemy.svg :target: https://pypi.python.org/pypi/WTForms-Alchemy/ wtforms-alchemy-0.19.0/docs/000077500000000000000000000000001471661042300156565ustar00rootroot00000000000000wtforms-alchemy-0.19.0/docs/Makefile000066400000000000000000000110221471661042300173120ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WTForms-Alchemy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WTForms-Alchemy.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/WTForms-Alchemy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WTForms-Alchemy" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." wtforms-alchemy-0.19.0/docs/advanced.rst000066400000000000000000000033261471661042300201610ustar00rootroot00000000000000Advanced concepts ================= Using WTForms-Alchemy with SQLAlchemy-Defaults ---------------------------------------------- WTForms-Alchemy works wonderfully with `SQLAlchemy-Defaults`_. When using `SQLAlchemy-Defaults`_ with WTForms-Alchemy you can define your models and model forms with much more robust syntax. For more information see `SQLAlchemy-Defaults`_ documentation. .. _SQLAlchemy-Defaults: https://github.com/kvesteri/sqlalchemy-defaults Example :: from sqlalchemy_defaults import LazyConfigured class User(Base, LazyConfigured): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column( sa.Unicode(255), nullable=False, label=u'Name' ) age = sa.Column( sa.Integer, nullable=False, min=18, max=100, label=u'Age' ) class UserForm(ModelForm): class Meta: model = User Using WTForms-Alchemy with Flask-WTF ------------------------------------ In order to make WTForms-Alchemy work with `Flask-WTF`_ you need the following snippet: .. _Flask-WTF: https://github.com/lepture/flask-wtf/ :: from flask_wtf import FlaskForm from wtforms_alchemy import model_form_factory # The variable db here is a SQLAlchemy object instance from # Flask-SQLAlchemy package from myproject.extensions import db BaseModelForm = model_form_factory(FlaskForm) class ModelForm(BaseModelForm): @classmethod def get_session(self): return db.session Then you can use the ModelForm just like before: :: class UserForm(ModelForm): class Meta: model = User wtforms-alchemy-0.19.0/docs/api.rst000066400000000000000000000016261471661042300171660ustar00rootroot00000000000000API Documentation ================= This part of the documentation covers all the public classes and functions in WTForms-Alchemy. :mod:`wtforms_alchemy` ---------------------- .. module:: wtforms_alchemy .. autoclass:: ModelForm :members: .. autofunction:: model_form_factory .. autoclass:: ModelFormMeta :members: .. autofunction:: model_form_meta_factory :mod:`wtforms_alchemy.generator` -------------------------------- .. module:: wtforms_alchemy.generator .. autoclass:: FormGenerator :members: :mod:`wtforms_alchemy.fields` -------------------------------- .. module:: wtforms_alchemy.fields .. autoclass:: QuerySelectField :members: .. autoclass:: QuerySelectMultipleField :members: :mod:`wtforms_alchemy.utils` ---------------------------- .. module:: wtforms_alchemy.utils .. autofunction:: translated_attributes .. autoclass:: ClassMap :members: :special-members: wtforms-alchemy-0.19.0/docs/column_conversion.rst000066400000000000000000000154741471661042300221650ustar00rootroot00000000000000Column to form field conversion =============================== Basic type conversion --------------------- By default WTForms-Alchemy converts SQLAlchemy model columns using the following type table. So for example if an Unicode column would be converted to TextField. The reason why so many types here convert to wtforms_components based fields is that wtforms_components provides better HTML5 compatible type handling than WTForms at the moment. ==================================== ================= **SQAlchemy column type** **Form field** ------------------------------------ ----------------- BigInteger wtforms_components.fields.IntegerField Boolean BooleanField Date wtforms_components.fields.DateField DateTime wtforms_components.fields.DateTimeField Enum wtforms_components.fields.SelectField Float FloatField Integer wtforms_components.fields.IntegerField Numeric wtforms_components.fields.DecimalField SmallInteger wtforms_components.fields.IntegerField String TextField Text TextAreaField Time wtforms_components.fields.TimeField Unicode TextField UnicodeText TextAreaField ==================================== ================= WTForms-Alchemy also supports many types provided by SQLAlchemy-Utils. ==================================== ================= **SQAlchemy-Utils type** **Form field** ------------------------------------ ----------------- ArrowType wtforms_components.fields.DateTimeField ChoiceType wtforms_components.fields.SelectField ColorType wtforms_components.fields.ColorField CountryType wtforms_alchemy.fields.CountryType EmailType wtforms_components.fields.EmailField IPAddressType wtforms_components.fields.IPAddressField PasswordType wtforms.fields.PasswordField PhoneNumberType wtforms_components.fields.PhoneNumberField URLType wtforms_components.fields.StringField + URL validator UUIDType wtforms.fields.TextField + UUID validator WeekDaysType wtforms_components.fields.WeekDaysField ==================================== ================= ==================================== ================= **SQAlchemy-Utils range type** **Form field** ------------------------------------ ----------------- DateRangeType wtforms_components.fields.DateIntervalField DateTimeRangeType wtforms_components.fields.DateTimeIntervalField IntRangeType wtforms_components.fields.IntIntervalField NumericRangeType wtforms_components.fields.DecimalIntervalField ==================================== ================= Excluded fields --------------- By default WTForms-Alchemy excludes a column from the ModelForm if one of the following conditions is True: * Column is primary key * Column is foreign key * Column is DateTime field which has default value (usually this is a generated value) * Column is of TSVectorType type * Column is set as model inheritance discriminator field Using include, exclude and only ------------------------------- If you wish the include some of the excluded fields described in the earlier chapter you can use the 'include' configuration parameter. In the following example we include the field 'author_id' in the ArticleForm (by default it is excluded since it is a foreign key column). :: class Article(Base): __tablename__ = 'article' id = sa.Column(sa.Integer, primary_key=True, nullable=False) name = sa.Column( sa.Unicode(255), nullable=False ) author_id = sa.Column(sa.Integer, sa.ForeignKey(User.id)) author = sa.orm.relationship(User) class ArticleForm(Form): class Meta: include = ['author_id'] If you wish the exclude fields you can either use 'exclude' or 'only' configuration parameters. The recommended way is using only, since in most cases it is desirable to explicitly tell which fields the form should contain. Consider the following model: :: class Article(Base): __tablename__ = 'article' id = sa.Column(sa.Integer, primary_key=True, nullable=False) name = sa.Column( sa.Unicode(255), nullable=False ) content = sa.Column( sa.UnicodeText ) description = sa.Column( sa.UnicodeText ) Now let's say we want to exclude 'description' from the form. This can be achieved as follows: :: class ArticleForm(Form): class Meta: exclude = ['description'] Or as follows (the recommended way): :: class ArticleForm(Form): class Meta: only = ['name', 'content'] Adding/overriding fields ------------------------ Example:: from wtforms.fields import TextField, IntegerField from wtforms.validators import Email class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column( sa.Unicode(255), nullable=False ) class UserForm(ModelForm): class Meta: model = User email = TextField(validators=[Optional()]) age = IntegerField() Now the UserForm would have three fields: * name, a required TextField * email, an optional TextField * age, IntegerField Type decorators --------------- WTForms-Alchemy supports SQLAlchemy TypeDecorator based types. When WTForms-Alchemy encounters a TypeDecorator typed column it tries to convert it to underlying type field. Example:: import sqlalchemy as sa from wtforms.fields import TextField, IntegerField from wtforms.validators import Email class CustomUnicodeType(sa.types.TypeDecorator): impl = sa.types.Unicode class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) name = sa.Column(CustomUnicodeType(100), primary_key=True) class UserForm(ModelForm): class Meta: model = User Now the name field of UserForm would be a simple TextField since the underlying type implementation is Unicode. wtforms-alchemy-0.19.0/docs/conf.py000066400000000000000000000167151471661042300171670ustar00rootroot00000000000000# # WTForms-Alchemy documentation build configuration file, created by # sphinx-quickstart on Wed Aug 29 16:20:21 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) from wtforms_alchemy import __version__ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "WTForms-Alchemy" copyright = "2012, Konsta Vesterinen" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "WTForms-Alchemydoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "WTForms-Alchemy.tex", "WTForms-Alchemy Documentation", "Konsta Vesterinen", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "wtforms-alchemy", "WTForms-Alchemy Documentation", ["Konsta Vesterinen"], 1, ) ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "http://docs.python.org/": None, "https://wtforms.readthedocs.io/en/latest/": None, "https://sqlalchemy-utils.readthedocs.io/en/latest/": None, "https://wtforms-components.readthedocs.io/en/latest/": None, } wtforms-alchemy-0.19.0/docs/configuration.rst000066400000000000000000000174721471661042300212720ustar00rootroot00000000000000Configuration ============= ModelForm meta parameters ------------------------- The following configuration options are available for ModelForm's Meta subclass. **include_primary_keys** (default: False) If you wish to include primary keys in the generated form please set this to True. This is useful when dealing with natural primary keys. In the following example each user has a natural primary key on its column name. The UserForm would contain two fields name and email. :: class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column(sa.Unicode(255), nullable=False) class UserForm(ModelForm): class Meta: model = User include_primary_keys = True **exclude** .. warning:: Using ``exclude`` might lead to problems in situations where you add columns to your model and forget to exclude those from the form by using ``exclude``, hence it is recommended to use ``only`` rather than ``exclude``. You can exclude certain fields by adding them to the exclude list. :: class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column(sa.Unicode(255), nullable=False) class UserForm(ModelForm): class Meta: model = User include_primary_keys = True exclude = ['email'] # this form contains only 'name' field **only** Generates a form using only the field names provided in ``only``. :: class UserForm(ModelForm): class Meta: model = User only = ['email'] **field_args** (default: {}) This parameter can be used for overriding field arguments. In the following example we force the email field optional. :: class UserForm(ModelForm): class Meta: model = User field_args = {'email': {'validators': [Optional()]}} **include_foreign_keys** (default: False) Foreign keys can be included in the form by setting include_foreign_keys to True. **only_indexed_fields** (default: False) When setting this option to True, only fields that have an index will be included in the form. This is very useful when creating forms for searching a specific model. **include_datetimes_with_default** (default: False) When setting this option to True, datetime with default values will be included in the form. By default this is False since usually datetime fields that have default values are generated columns such as "created_at" or "updated_at", which should not be included in the form. **validators** A dict containing additional validators for the generated form field objects. Example:: from wtforms.validators import Email class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column(sa.Unicode(255), nullable=False) class UserForm(ModelForm): class Meta: model = User include_primary_keys = True validators = {'email': [Email()]} **datetime_format** (default: '%Y-%m-%d %H:%M:%S') Defines the default datetime format, which will be assigned to generated datetime fields. **date_format** (default: '%Y-%m-%d') Defines the default date format, which will be assigned to generated datetime fields. **all_fields_optional** (default: False) Defines all generated fields as optional (useful for update forms). **assign_required** (default: True) Whether or not to assign non-nullable fields as required. **strip_string_fields** (default: False) Whether or not to add stripping filter to all string fields. Example :: from werkzeug.datastructures import MultiDict class UserForm(ModelForm): class Meta: model = User strip_string_fields = True form = UserForm(MultiDict([('name', 'someone ')])) assert form.name.data == 'someone' You can also fine-grain field stripping by using trim argument for columns. In the example below the field 'name' would have its values stripped whereas field 'password' would not. :: from wtforms.validators import Email class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(100)) password = sa.Column(sa.Unicode(100), info={'trim': False}) class UserForm(ModelForm): class Meta: model = User strip_string_fields = True **form_generator** (default: FormGenerator class) Change this if you want to use custom form generator class. Form inheritance ---------------- ModelForm's configuration support inheritance. This means that child classes inherit parents Meta properties. Example:: from wtforms.validators import Email class UserForm(ModelForm): class Meta: model = User validators = {'email': [Email()]} class UserUpdateForm(UserForm): class Meta: all_fields_optional = True Here UserUpdateForm inherits the configuration properties of UserForm, hence it would use model User and have additional Email validator on column 'email'. Also it assigns all fields as optional. Not nullable column validation ------------------------------ WTForms-Alchemy offers two options for configuring how not nullable columns are validated: * not_null_validator The default validator to be used for not nullable columns. Set this to `None` if you wish to disable it. By default this is `[InputRequired()]`. * not_null_validator_type_map Type map which overrides the **not_null_validator** on specific column type. By default this is `ClassMap({sa.String: [InputRequired(), DataRequired()]})`. In the following example we set `DataRequired` validator for all not nullable Enum typed columns: :: import sqlalchemy as sa from wtforms.validators import DataRequired from wtforms_alchemy import ClassMap class MyForm(ModelForm): class Meta: not_null_validator_type_map = ClassMap({sa.Enum: [DataRequired()]}) Customizing type conversion --------------------------- You can customize the SQLAlchemy type conversion on class level with type_map Meta property. Type map accepts dictionary of SQLAlchemy types as keys and WTForms field classes as values. The key value pairs of this dictionary override the key value pairs of FormGenerator.TYPE_MAP. Let's say we want to convert all unicode typed properties to TextAreaFields instead of StringFields. We can do this by assigning Unicode, TextAreaField key value pair into type map. :: from wtforms.fields import TextAreaField from wtforms_alchemy import ClassMap class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(100)) class UserForm(ModelForm): class Meta: type_map = ClassMap({sa.Unicode: TextAreaField}) In case the type_map dictionary values are not inherited from WTForm field class, they are considered callable functions. These functions will be called with the corresponding column as their only parameter. .. _custom_base: Custom form base class ---------------------- You can use custom base class for your model forms by using model_form_factory function. In the following example we have a UserForm which uses Flask-WTF form as a parent form for ModelForm. :: from flask.ext.wtf import Form from wtforms_alchemy import model_form_factory ModelForm = model_form_factory(Form) class UserForm(ModelForm): class Meta: model = User You can also pass any form generator option to model_form_factory. :: ModelForm = model_form_factory(Form, strip_string_fields=True) class UserForm(ModelForm): class Meta: model = User wtforms-alchemy-0.19.0/docs/customization.rst000066400000000000000000000070311471661042300213210ustar00rootroot00000000000000Form customization ================== Custom fields ------------- If you want to use a custom field class, you can pass it by using form_field_class parameter for the column info dictionary. Example :: class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) color = sa.Column( sa.String(7), info={'form_field_class': ColorField}, nullable=False ) class UserForm(ModelForm): class Meta: model = User Now the 'color' field of UserForm would be a custom ColorField. Forcing the use of SelectField ------------------------------ Sometimes you may want to have integer and unicode fields convert to SelectFields. Probably the easiest way to achieve this is by using choices parameter for the column info dictionary. Example :: class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) age = sa.Column( sa.Integer, info={'choices': [(i, i) for i in xrange(13, 99)]}, nullable=False ) class UserForm(ModelForm): class Meta: model = User Here the UserForm would have two fields. One TextField for the name column and one SelectField for the age column containing range of choices from 13 to 99. Notice that WTForms-Alchemy is smart enough to use the right coerce function based on the underlying column type, hence in the previous example the age column would convert to the following SelectField. :: SelectField('Age', coerce=int, choices=[(i, i) for i in xrange(13, 99)]) For nullable unicode and string columns WTForms-Alchemy uses special null_or_unicode coerce function, which converts empty strings to None values. Field descriptions ------------------ Example:: class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column( sa.Unicode(255), nullable=False, info={'description': 'This is the description of email.'} ) class UserForm(ModelForm): class Meta: model = User Now the 'email' field of UserForm would have description 'This is the description of email.' Field labels ------------ Example:: class User(Base): __tablename__ = 'user' name = sa.Column( sa.Unicode(100), primary_key=True, nullable=False, info={'label': 'Name'} ) class UserForm(ModelForm): class Meta: model = User Now the 'name' field of UserForm would have label 'Name'. Custom widgets -------------- Example:: from wtforms import widgets class User(Base): __tablename__ = 'user' name = sa.Column( sa.Unicode(100), primary_key=True, nullable=False, info={'widget': widgets.HiddenInput()} ) class UserForm(ModelForm): class Meta: model = User Now the 'name' field of UserForm would use HiddenInput widget instead of TextInput. Default values -------------- By default WTForms-Alchemy ModelForm assigns the default values from column definitions. Example :: class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) level = sa.Column(sa.Integer, default=1) class UserForm(ModelForm): class Meta: model = User Now the UseForm 'level' field default value would be 1. wtforms-alchemy-0.19.0/docs/index.rst000066400000000000000000000011071471661042300175160ustar00rootroot00000000000000WTForms-Alchemy =============== WTForms-Alchemy is a WTForms extension toolkit for easier creation of model based forms. Strongly influenced by Django ModelForm. .. toctree:: :maxdepth: 2 introduction column_conversion types customization validators configuration relationships advanced api license .. _`SQLAlchemy-Utils ChoiceType`: https://sqlalchemy-utils.readthedocs.io/en/latest/#choicetype .. _`SQLAlchemy-Defaults`: https://sqlalchemy-defaults.readthedocs.io/en/latest/ .. _`Flask-WTF`: https://flask-wtf.readthedocs.io/en/latest/ wtforms-alchemy-0.19.0/docs/introduction.rst000066400000000000000000000061701471661042300211350ustar00rootroot00000000000000Introduction ============ What for? --------- Many times when building modern web apps with SQLAlchemy you’ll have forms that map closely to models. For example, you might have a Article model, and you want to create a form that lets people post new article. In this case, it would be time-consuming to define the field types and basic validators in your form, because you’ve already defined the fields in your model. WTForms-Alchemy provides a helper class that let you create a Form class from a SQLAlchemy model. Differences with wtforms.ext.sqlalchemy model_form -------------------------------------------------- WTForms-Alchemy does not try to replace all the functionality of wtforms.ext.sqlalchemy. It only tries to replace the model_form function of wtforms.ext.sqlalchemy by a much better solution. Other functionality of .ext.sqlalchemy such as QuerySelectField and QuerySelectMultipleField can be used along with WTForms-Alchemy. Now how is WTForms-Alchemy ModelForm better than wtforms.ext.sqlachemy's model_form? * Provides explicit declaration of ModelForms (much easier to override certain columns) * Form generation supports Unique and NumberRange validators * Form inheritance support (along with form configuration inheritance) * Automatic SelectField type coercing based on underlying column type * By default uses wtforms_components SelectField for fields with choices. This field understands None values and renders nested datastructures as optgroups. * Provides better Unique validator * Supports custom user defined types as well as type decorators * Supports SQLAlchemy-Utils datatypes * Supports ModelForm model relations population * Smarter field exclusion * Smarter field conversion * Understands join table inheritance * Better configuration Installation ------------ :: pip install WTForms-Alchemy The supported Python versions are 3.9–3.13. QuickStart ---------- Lets say we have a model called User with couple of fields:: import sqlalchemy as sa from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker from wtforms_alchemy import ModelForm engine = create_engine('sqlite:///:memory:') Base = declarative_base(engine) Session = sessionmaker(bind=engine) session = Session() class User(Base): __tablename__ = 'user' id = sa.Column(sa.BigInteger, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(100), nullable=False) email = sa.Column(sa.Unicode(255), nullable=False) Now we can create our first ModelForm for the User model. ModelForm behaves almost like your ordinary WTForms Form except it accepts special Meta arguments. Every ModelForm must define model parameter in the Meta arguments.:: class UserForm(ModelForm): class Meta: model = User Now this ModelForm is essentially the same as :: class UserForm(Form): name = TextField(validators=[DataRequired(), Length(max=100)]) email = TextField(validators=[DataRequired(), Length(max=255)]) In the following chapters you'll learn how WTForms-Alchemy converts SQLAlchemy model columns to form fields. wtforms-alchemy-0.19.0/docs/license.rst000066400000000000000000000000511471661042300200260ustar00rootroot00000000000000License ======= .. include:: ../LICENSE wtforms-alchemy-0.19.0/docs/make.bat000066400000000000000000000106611471661042300172670ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WTForms-Alchemy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WTForms-Alchemy.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end wtforms-alchemy-0.19.0/docs/relationships.rst000066400000000000000000000053541471661042300213030ustar00rootroot00000000000000Forms with relations ==================== WTForms-Alchemy provides special Field subtypes ModelFormField and ModelFieldList. When using these types WTForms-Alchemy understands model relations and is smart enough to populate related objects accordingly. One-to-one relations -------------------- Consider the following example. We have Event and Location classes with each event having one location. :: from sqlalchemy.orm import declarative_base from wtforms_alchemy import ModelForm, ModelFormField Base = declarative_base() class Location(Base): __tablename__ = 'location' id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) class Event(Base): __tablename__ = 'event' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id)) location = sa.orm.relationship(Location) class LocationForm(ModelForm): class Meta: model = Location class EventForm(ModelForm): class Meta: model = Event location = ModelFormField(LocationForm) Now if we populate the EventForm, WTForms-Alchemy is smart enough to populate related location too. :: event = Event() form = EventForm(request.POST) form.populate_obj(event) One-to-many relations --------------------- Consider the following example. We have Event and Location classes with each event having many location. Notice we are using FormField along with ModelFieldList. :: from sqlalchemy.orm import declarative_base from wtforms_alchemy import ModelForm, ModelFieldList from wtforms.fields import FormField Base = declarative_base() class Event(Base): __tablename__ = 'event' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) class Location(Base): __tablename__ = 'location' id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id)) event = sa.orm.relationship( Location, backref='locations' # the event needs to have this ) class LocationForm(ModelForm): class Meta: model = Location class EventForm(ModelForm): class Meta: model = Event locations = ModelFieldList(FormField(LocationForm)) Now if we populate the EventForm, WTForms-Alchemy is smart enough to populate related locations too. :: event = Event() form = EventForm(request.POST) form.populate_obj(event) wtforms-alchemy-0.19.0/docs/types.rst000066400000000000000000000141341471661042300175570ustar00rootroot00000000000000Type specific conversion ======================== Numeric type ------------ WTForms-Alchemy automatically converts Numeric columns to DecimalFields. The converter is also smart enough to convert different decimal scales to appropriate HTML5 input step args. :: class Account(Base): __tablename__ = 'event' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) balance = sa.Column( sa.Numeric(scale=2), nullable=False ) class AccountForm(ModelForm): class Meta: model = Account Now rendering AccountForm.balance would return the following HTML: Arrow type ---------- WTForms-Alchemy supports the ArrowType of SQLAlchemy-Utils and converts it to HTML5 compatible DateTimeField of WTForms-Components. :: from sqlalchemy_utils import ArrowType class Event(Base): __tablename__ = 'event' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) start_time = sa.Column( ArrowType(), nullable=False ) class EventForm(ModelForm): class Meta: model = Event Now the EventForm is essentially the same as: :: class EventForm(Form): start_time = DateTimeField(validators=[DataRequired()]) Choice type ----------- WTForms-Alchemy automatically converts :class:`sqlalchemy_utils.types.choice.ChoiceType` to WTForms-Components SelectField. :: from sqlalchemy_utils import ChoiceType class Event(Base): __tablename__ = 'event' TYPES = [ (u'party', u'Party'), (u'training, u'Training') ] id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) type = sa.Column(ChoiceType(TYPES)) class EventForm(ModelForm): class Meta: model = Event Now the EventForm is essentially the same as: :: from wtforms_alchemy.utils import choice_type_coerce_factory class EventForm(Form): type = SelectField( choices=Event.TYPES, coerce=choice_type_coerce_factory(Event.type.type), validators=[DataRequired()] ) Color type ---------- :: from sqlalchemy_utils import ColorType class CustomView(Base): __tablename__ = 'view' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) background_color = sa.Column( ColorType(), nullable=False ) class CustomViewForm(ModelForm): class Meta: model = CustomView Now the CustomViewForm is essentially the same as: :: from wtforms_components import ColorField class CustomViewForm(Form): color = ColorField(validators=[DataRequired()]) Country type ------------ :: from sqlalchemy_utils import CountryType class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) country = sa.Column(CountryType, nullable=False) class UserForm(ModelForm): class Meta: model = User The UserForm is essentially the same as: :: from wtforms_components import CountryField class UserForm(Form): country = CountryField(validators=[DataRequired()]) Email type ---------- :: from sqlalchemy_utils import EmailType class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) email = sa.Column(EmailType, nullable=False) class UserForm(ModelForm): class Meta: model = User The good old wtforms equivalent of this form would be: :: from wtforms_components import EmailField class UserForm(Form): email = EmailField(validators=[DataRequired()]) Password type ------------- Consider the following model definition: :: from sqlalchemy_utils import PasswordType class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) name = sa.Column(sa.Unicode(100), nullable=False) password = sa.Column( PasswordType( schemes=['pbkdf2_sha512'] ), nullable=False ) class UserForm(ModelForm): class Meta: model = User Now the UserForm is essentially the same as: :: class UserForm(Form): name = TextField(validators=[DataRequired(), Length(max=100)]) password = PasswordField(validators=[DataRequired()]) Phonenumber type ---------------- WTForms-Alchemy supports the PhoneNumberType of SQLAlchemy-Utils and converts it automatically to WTForms-Components PhoneNumberField. This field renders itself as HTML5 compatible phonenumber input. Consider the following model definition: :: from sqlalchemy_utils import PhoneNumberType class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) name = sa.Column(sa.Unicode(100), nullable=False) phone_number = sa.Column(PhoneNumberType()) class UserForm(ModelForm): class Meta: model = User Now the UserForm is essentially the same as: :: from wtforms_components import PhoneNumberField class UserForm(Form): name = TextField(validators=[DataRequired(), Length(max=100)]) phone_number = PhoneNumberField(validators=[DataRequired()]) URL type -------- WTForms-Alchemy automatically converts SQLAlchemy-Utils URLType to StringField and adds URL validator for it. Consider the following model definition: :: from sqlalchemy_utils import URLType class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) website = sa.Column(URLType()) class UserForm(ModelForm): class Meta: model = User Now the UserForm is essentially the same as: :: from wtforms_components import StringField from wtforms.validators import URL class UserForm(Form): website = StringField(validators=[URL()]) wtforms-alchemy-0.19.0/docs/validators.rst000066400000000000000000000142201471661042300205570ustar00rootroot00000000000000Validators ========== Auto-assigned validators ------------------------ By default WTForms-Alchemy ModelForm assigns the following validators: * InputRequired validator if column is not nullable and has no default value * DataRequired validator if column is not nullable, has no default value and is of type `sqlalchemy.types.String` * NumberRange validator if column if of type Integer, Float or Decimal and column info parameter has min or max arguments defined * DateRange validator if column is of type Date or DateTime and column info parameter has min or max arguments defined * TimeRange validator if column is of type Time and info parameter has min or max arguments defined * Unique validator if column has a unique index * Length validator for String/Unicode columns with max length * Optional validator for all nullable columns Unique validator ---------------- WTForms-Alchemy automatically assigns unique validators for columns which have unique indexes defined. Unique validator raises ValidationError exception whenever a non-unique value for given column is assigned. Consider the following model/form definition. Notice how you need to define get_session() classmethod for your form. Unique validator uses this method for getting the appropriate SQLAlchemy session. :: engine = create_engine('sqlite:///:memory:') Base = declarative_base() Session = sessionmaker(bind=engine) session = Session() class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(100), nullable=False) email = sa.Column( sa.Unicode(255), nullable=False, unique=True ) class UserForm(ModelForm): class Meta: model = User @classmethod def get_session(): # this method should return sqlalchemy session return session Here UserForm would behave the same as the following form: :: class UserForm(Form): name = TextField('Name', validators=[DataRequired(), Length(max=100)]) email = TextField( 'Email', validators=[ DataRequired(), Length(max=255), Unique(User.email, get_session=lambda: session) ] ) If you are using Flask-SQLAlchemy or similar tool, which assigns session-bound query property to your declarative models, you don't need to define the get_session() method. Simply use: :: Unique(User.email) Using unique validator with existing objects -------------------------------------------- When editing an existing object, WTForms-Alchemy must know the object currently edited to avoid raising a ValidationError. Here how to proceed to inform WTForms-Alchemy of this case. Example:: obj = MyModel.query.get(1) form = MyForm(obj=obj) form.populate_obj(obj) form.validate() WTForms-Alchemy will then understand to avoid the unique validation of the object with this same object. Range validators ---------------- WTForms-Alchemy automatically assigns range validators based on column type and assigned column info min and max attributes. In the following example we create a form for Event model where start_time can't be set in the past. :: class Event(Base): __tablename__ = 'event' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) start_time = sa.Column(sa.DateTime, info={'min': datetime.now()}) class EventForm(ModelForm): class Meta: model = Event Additional field validators --------------------------- Example:: from wtforms.validators import Email class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column( sa.Unicode(255), nullable=False, info={'validators': Email()} ) class UserForm(ModelForm): class Meta: model = User Now the 'email' field of UserForm would have Email validator. Overriding default validators ----------------------------- Sometimes you may want to override what class WTForms-Alchemy uses for email, number_range, length etc. validations. For all automatically assigned validators WTForms-Alchemy provides configuration options to override the default validator. In the following example we set a custom Email validator for User class. :: from sqlalchemy_utils import EmailType from wtforms_components import Email class User(Base): __tablename__ = 'user' name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False) email = sa.Column( EmailType, nullable=False, ) class MyEmailValidator(Email): def __init__(self, message='My custom email error message'): Email.__init__(self, message=message) class UserForm(ModelForm): class Meta: model = User email_validator = MyEmailValidator If you don't wish to subclass you can simply use functions / lambdas: :: def email(): return Email(message='My custom email error message') class UserForm(ModelForm): class Meta: model = User email_validator = email You can also override validators that take multiple arguments this way: :: def length(min=None, max=None): return Length(min=min, max=max, message='Wrong length') class UserForm(ModelForm): class Meta: model = User length_validator = length Here is the full list of configuration options you can use to override default validators: * email_validator * length_validator * unique_validator * number_range_validator * date_range_validator * time_range_validator * optional_validator Disabling validators -------------------- You can disable certain validators by assigning them as `None`. Let's say you want to disable nullable columns having `Optional` validator. This can be achieved as follows:: class UserForm(ModelForm): class Meta: model = User optional_validator = None wtforms-alchemy-0.19.0/pyproject.toml000066400000000000000000000007311471661042300176430ustar00rootroot00000000000000[tool.pytest.ini_options] filterwarnings = [ 'error:.*:sqlalchemy.exc.SADeprecationWarning', 'error:.*:sqlalchemy.exc.SAWarning', 'ignore:.*:sqlalchemy.exc.SADeprecationWarning:sqlalchemy_i18n', ] [tool.ruff] target-version = "py39" [tool.ruff.lint] select = [ "C90", # mccabe "E", # pycodestyle errors "F", # Pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.lint.isort] order-by-type = false wtforms-alchemy-0.19.0/setup.py000066400000000000000000000047401471661042300164450ustar00rootroot00000000000000""" WTForms-Alchemy --------------- Generates WTForms forms from SQLAlchemy models. """ import os import re import sys from setuptools import setup HERE = os.path.dirname(os.path.abspath(__file__)) PY3 = sys.version_info[0] == 3 def get_version(): filename = os.path.join(HERE, "wtforms_alchemy", "__init__.py") with open(filename) as f: contents = f.read() pattern = r'^__version__ = "(.*?)"$' return re.search(pattern, contents, re.MULTILINE).group(1) extras_require = { "test": [ "enum34", "pytest>=2.3", "Pygments>=1.2", "Jinja2>=2.3", "docutils>=0.10", "flexmock>=0.9.7", "ruff==0.7.4", "WTForms-Test>=0.1.1", ], "babel": ["Babel>=1.3"], "arrow": ["arrow>=0.3.4"], "phone": ["phonenumbers>=5.9.2"], "intervals": ["intervals>=0.2.0"], "password": ["passlib >= 1.6, < 2.0"], "color": ["colour>=0.0.4"], "i18n": ["SQLAlchemy-i18n >= 0.8.2"], "ipaddress": ["ipaddr"] if not PY3 else [], "timezone": ["python-dateutil"], } # Add all optional dependencies to testing requirements. for name, requirements in extras_require.items(): if name != "test": extras_require["test"] += requirements setup( name="WTForms-Alchemy", version=get_version(), url="https://github.com/kvesteri/wtforms-alchemy", license="BSD", author="Konsta Vesterinen", author_email="konsta@fastmonkeys.com", description="Generates WTForms forms from SQLAlchemy models.", long_description=__doc__, packages=["wtforms_alchemy"], zip_safe=False, include_package_data=True, platforms="any", install_requires=[ "SQLAlchemy>=1.4", "WTForms>=3.1.0", "WTForms-Components>=0.11.0", "SQLAlchemy-Utils>=0.40.0", ], extras_require=extras_require, classifiers=[ "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", ], ) wtforms-alchemy-0.19.0/tests/000077500000000000000000000000001471661042300160705ustar00rootroot00000000000000wtforms-alchemy-0.19.0/tests/__init__.py000066400000000000000000000036261471661042300202100ustar00rootroot00000000000000import sqlalchemy as sa from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy.orm.session import close_all_sessions from sqlalchemy_utils import force_auto_coercion from wtforms_test import FormTestCase from wtforms_alchemy import ModelForm force_auto_coercion() class MultiDict(dict): def getlist(self, key): return [self[key]] class ModelFormTestCase(FormTestCase): dns = "sqlite:///:memory:" def setup_method(self, method): self.engine = create_engine(self.dns) self.base = declarative_base() def teardown_method(self, method): self.engine.dispose() self.ModelTest = None self.form_class = None def init_model(self, type_=sa.Unicode(255), **kwargs): kwargs.setdefault("nullable", False) class ModelTest(self.base): __tablename__ = "model_test" query = None id = sa.Column(sa.Integer, primary_key=True) test_column = sa.Column(type_, **kwargs) some_property = "something" self.ModelTest = ModelTest def init(self, type_=sa.Unicode(255), **kwargs): self.init_model(type_=type_, **kwargs) self.init_form() def init_form(self): class ModelTestForm(ModelForm): class Meta: model = self.ModelTest self.form_class = ModelTestForm class FormRelationsTestCase: dns = "sqlite:///:memory:" def setup_method(self, method): self.engine = create_engine(self.dns) self.base = declarative_base() self.create_models() self.create_forms() self.base.metadata.create_all(self.engine) Session = sessionmaker(bind=self.engine) self.session = Session() def teardown_method(self, method): close_all_sessions() self.base.metadata.drop_all(self.engine) self.engine.dispose() wtforms-alchemy-0.19.0/tests/conftest.py000066400000000000000000000017431471661042300202740ustar00rootroot00000000000000import pytest from wtforms.form import FormMeta from wtforms_alchemy import ( model_form_factory, model_form_meta_factory, ModelFormMeta, ) class _MetaWithInit(FormMeta): def __init__(cls, *args, **kwargs): cls.test_attr = "SomeVal" FormMeta.__init__(cls, *args, **kwargs) MetaWithInit = model_form_meta_factory(_MetaWithInit) class _MetaWithoutInit(FormMeta): test_attr = "SomeVal" MetaWithoutInit = model_form_meta_factory(_MetaWithoutInit) @pytest.fixture(params=[MetaWithInit, MetaWithoutInit, ModelFormMeta]) def model_form_all(request): """Returns one of each possible model form classes with custom and the original metaclass.""" ModelForm = model_form_factory(meta=request.param) return ModelForm @pytest.fixture(params=[MetaWithInit, MetaWithoutInit]) def model_form_custom(request): """Returns one of each possible model form classes with custom metaclasses.""" return model_form_factory(meta=request.param) wtforms-alchemy-0.19.0/tests/test_class_map.py000066400000000000000000000021341471661042300214430ustar00rootroot00000000000000from pytest import mark, raises from wtforms_alchemy.utils import ClassMap class A: pass class B: pass class A2(A): pass class A3(A2): pass class A4(A3): pass class A5(A4): pass class B2(B): pass class C: pass @mark.parametrize("key", [B2, B, A, A2]) def test_contains_with_subclass_check(key): class_map = ClassMap({A: 3, B: 6}) assert key in class_map @mark.parametrize("key", [B2(), B(), A(), A2()]) def test_contains_with_isinstance_check(key): class_map = ClassMap({A: 3, B: 6}) assert key in class_map @mark.parametrize(("key", "value"), [(B2, 6), (B, 6), (A, 3), (A2, 3)]) def test_getitem_with_classes(key, value): class_map = ClassMap({A: 3, B: 6}) assert class_map[key] == value @mark.parametrize(("key", "value"), [(B2(), 6), (B(), 6), (A(), 3), (A2(), 3)]) def test_getitem_with_objects(key, value): class_map = ClassMap({A: 3, B: 6}) assert class_map[key] == value def test_getitem_throws_keyerror_for_unknown_key(): class_map = ClassMap({A: 3, B: 6}) with raises(KeyError): class_map["unknown"] wtforms-alchemy-0.19.0/tests/test_column_aliases.py000066400000000000000000000053451471661042300225060ustar00rootroot00000000000000import sqlalchemy as sa from wtforms.validators import NumberRange from tests import ModelFormTestCase from wtforms_alchemy import ModelForm class TestColumnAliases(ModelFormTestCase): def test_supports_column_aliases(self): class TestModel(self.base): __tablename__ = "TestTable" id = sa.Column(sa.Integer, primary_key=True) some_alias = sa.Column("some_name", sa.Integer) class TestForm(ModelForm): class Meta: model = TestModel form = TestForm() assert hasattr(form, "some_alias") assert not hasattr(form, "some_name") def test_labels(self): class TestModel(self.base): __tablename__ = "TestTable" id = sa.Column(sa.Integer, primary_key=True) some_alias = sa.Column( "some_name", sa.Integer, ) class TestForm(ModelForm): class Meta: model = TestModel form = TestForm() assert form.some_alias.label.text == "some_alias" def test_unique_indexes(self): class TestModel(self.base): __tablename__ = "TestTable" id = sa.Column(sa.Integer, primary_key=True) some_alias = sa.Column("some_name", sa.Integer, unique=True) class TestForm(ModelForm): class Meta: model = TestModel @staticmethod def get_session(): return None form = TestForm() assert hasattr(form, "some_alias") assert not hasattr(form, "some_name") def test_meta_field_args(self): class TestModel(self.base): __tablename__ = "TestTable" id = sa.Column(sa.Integer, primary_key=True) some_alias = sa.Column("some_name", sa.Integer) validators = [NumberRange(max=4)] class TestForm(ModelForm): class Meta: model = TestModel field_args = {"some_alias": {"validators": validators}} form = TestForm() assert hasattr(form, "some_alias") assert not hasattr(form, "some_name") assert form.some_alias.validators == validators def test_additional_validators(self): class TestModel(self.base): __tablename__ = "TestTable" id = sa.Column(sa.Integer, primary_key=True) some_alias = sa.Column("some_name", sa.Integer) number_range = NumberRange(max=4) validator_list = [number_range] class TestForm(ModelForm): class Meta: model = TestModel validators = {"some_alias": validator_list} form = TestForm() assert number_range in form.some_alias.validators wtforms-alchemy-0.19.0/tests/test_configuration.py000066400000000000000000000153341471661042300223560ustar00rootroot00000000000000import sqlalchemy as sa from pytest import raises from wtforms.fields import IntegerField from wtforms.validators import Email from tests import ModelFormTestCase, MultiDict from wtforms_alchemy import ( AttributeTypeException, InvalidAttributeException, ModelForm, ) class UnknownType(sa.types.UserDefinedType): def get_col_spec(self): return "UNKNOWN()" class TestModelFormConfiguration(ModelFormTestCase): def test_skip_unknown_types(self): class ModelTest(self.base): __tablename__ = "model_test" query = None id = sa.Column(sa.Integer, primary_key=True) test_column = sa.Column(UnknownType) some_property = "something" self.ModelTest = ModelTest class ModelTestForm(ModelForm): class Meta: model = self.ModelTest skip_unknown_types = True self.form_class = ModelTestForm assert not self.has_field("test_column") def test_supports_field_exclusion(self): self.init_model() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest exclude = ["test_column"] self.form_class = ModelTestForm assert not self.has_field("test_column") def test_throws_exception_for_unknown_excluded_column(self): self.init_model() with raises(InvalidAttributeException): class ModelTestForm(ModelForm): class Meta: model = self.ModelTest exclude = ["some_unknown_column"] def test_invalid_exclude_with_attr_errors_as_false(self): self.init_model() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest attr_errors = False exclude = ["some_unknown_column"] def test_throws_exception_for_unknown_included_column(self): self.init_model() with raises(InvalidAttributeException): class ModelTestForm(ModelForm): class Meta: model = self.ModelTest include = ["some_unknown_column"] def test_invalid_include_with_attr_errors_as_false(self): self.init_model() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest attr_errors = False include = ["some_unknown_column"] def test_throws_exception_for_non_column_fields(self): self.init_model() with raises(AttributeTypeException): class ModelTestForm(ModelForm): class Meta: model = self.ModelTest include = ["some_property"] def test_supports_field_inclusion(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest include = ["id"] self.form_class = ModelTestForm assert self.has_field("id") def test_supports_only_attribute(self): class ModelTest(self.base): __tablename__ = "model_test" query = None id = sa.Column(sa.Integer, primary_key=True) test_column = sa.Column(sa.UnicodeText) test_column2 = sa.Column(sa.UnicodeText) class ModelTestForm(ModelForm): class Meta: model = ModelTest only = ["test_column"] form = ModelTestForm() assert len(form._fields) == 1 def test_supports_field_overriding(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest test_column = IntegerField() self.form_class = ModelTestForm self.assert_type("test_column", IntegerField) def test_supports_assigning_all_fields_as_optional(self): self.init(nullable=False) class ModelTestForm(ModelForm): class Meta: model = self.ModelTest all_fields_optional = True self.form_class = ModelTestForm self.assert_not_required("test_column") self.assert_optional("test_column") def test_supports_custom_datetime_format(self): self.init(sa.DateTime, nullable=False) class ModelTestForm(ModelForm): class Meta: model = self.ModelTest datetime_format = "%Y-%m-%dT%H:%M:%S" form = ModelTestForm() assert form.test_column.format == ["%Y-%m-%dT%H:%M:%S"] def test_supports_additional_validators(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest validators = {"test_column": Email()} self.form_class = ModelTestForm self.assert_has_validator("test_column", Email) def test_inherits_config_params_from_parent_meta(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest only = ["test_column"] class AnotherModelTestForm(ModelTestForm): class Meta: pass assert AnotherModelTestForm.Meta.only == ["test_column"] def test_child_classes_override_parents_config_params(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest only = ["test_column"] class AnotherModelTestForm(ModelTestForm): class Meta: only = [] assert AnotherModelTestForm.Meta.only == [] def test_strip_strings_fields(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest only = ["test_column"] strip_string_fields = True form = ModelTestForm(MultiDict(test_column=" something ")) assert form.test_column.data == "something" def test_strip_strings_fields_with_empty_values(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest only = ["test_column"] strip_string_fields = True ModelTestForm() def test_class_meta_regression(self): self.init() class SomeForm(ModelForm): class Meta: model = self.ModelTest foo = 9 class OtherForm(SomeForm): class Meta: pass assert issubclass(OtherForm.Meta, SomeForm.Meta) form = OtherForm() # Create a side effect on the base meta. assert form.Meta.foo == 9 SomeForm.Meta.foo = 12 assert form.Meta.foo == 12 wtforms-alchemy-0.19.0/tests/test_country_field.py000066400000000000000000000021341471661042300223470ustar00rootroot00000000000000import sqlalchemy_utils from babel import Locale from wtforms import Form from tests import MultiDict from wtforms_alchemy import CountryField sqlalchemy_utils.i18n.get_locale = lambda: Locale("en") class TestCountryField: field_class = CountryField def init_form(self, **kwargs): class TestForm(Form): test_field = self.field_class(**kwargs) self.form_class = TestForm return self.form_class def setup_method(self, method): self.valid_countries = ["US", "SA", "FI"] self.invalid_countries = [ "unknown", ] def test_valid_countries(self): form_class = self.init_form() for country in self.valid_countries: form = form_class(MultiDict(test_field=country)) form.validate() assert len(form.errors) == 0 def test_invalid_countries(self): form_class = self.init_form() for country in self.invalid_countries: form = form_class(MultiDict(test_field=country)) form.validate() assert len(form.errors["test_field"]) == 2 wtforms-alchemy-0.19.0/tests/test_custom_fields.py000066400000000000000000000010101471661042300223310ustar00rootroot00000000000000from wtforms import Form from wtforms_components import SelectField from tests import MultiDict from wtforms_alchemy import null_or_unicode class TestSelectField: def test_understands_none_values(self): class MyForm(Form): choice_field = SelectField( choices=[("", "-- Choose --"), ("choice 1", "Something")], coerce=null_or_unicode, ) form = MyForm(MultiDict({"choice_field": ""})) form.validate() assert form.errors == {} wtforms-alchemy-0.19.0/tests/test_deep_form_relations.py000066400000000000000000000121651471661042300235260ustar00rootroot00000000000000import sqlalchemy as sa from wtforms.fields import FormField from tests import FormRelationsTestCase, MultiDict from wtforms_alchemy import ModelFieldList, ModelForm, ModelFormField class TestDeepFormRelationsOneToManyToOne(FormRelationsTestCase): def create_models(self): class Event(self.base): __tablename__ = "event" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) class Address(self.base): __tablename__ = "address" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) street = sa.Column(sa.Unicode(255), nullable=True) class Location(self.base): __tablename__ = "location" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) address_id = sa.Column(sa.Integer, sa.ForeignKey(Address.id)) address = sa.orm.relationship(Address) event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id)) event = sa.orm.relationship(Event, backref="locations") self.Event = Event self.Location = Location self.Address = Address def create_forms(self): class AddressForm(ModelForm): class Meta: model = self.Address class LocationForm(ModelForm): class Meta: model = self.Location address = ModelFormField(AddressForm) class EventForm(ModelForm): class Meta: model = self.Event locations = ModelFieldList(FormField(LocationForm)) self.LocationForm = LocationForm self.EventForm = EventForm self.AddressForm = AddressForm def save(self): data = { "name": "Some event", "locations-0-name": "Some location", "locations-0-address-street": "Some address", } event = self.Event() self.session.add(event) form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() def test_assigment_and_deletion(self): self.save() event = self.session.query(self.Event).first() assert event.locations[0].name == "Some location" assert event.locations[0].address.street == "Some address" data = {"name": "Some event"} form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() event = self.session.query(self.Event).first() assert event.locations == [] class TestDeepFormRelationsOneToOneToMany(FormRelationsTestCase): def create_models(self): class Location(self.base): __tablename__ = "location" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) class Address(self.base): __tablename__ = "address" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) street = sa.Column(sa.Unicode(255), nullable=True) location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id)) location = sa.orm.relationship(Location, backref="addresses") class Event(self.base): __tablename__ = "event" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id)) location = sa.orm.relationship(Location) self.Event = Event self.Location = Location self.Address = Address def create_forms(self): class AddressForm(ModelForm): class Meta: model = self.Address class LocationForm(ModelForm): class Meta: model = self.Location addresses = ModelFieldList(FormField(AddressForm)) class EventForm(ModelForm): class Meta: model = self.Event location = ModelFormField(LocationForm) self.LocationForm = LocationForm self.EventForm = EventForm self.AddressForm = AddressForm def save(self): data = { "name": "Some event", "location-name": "Some location", "location-addresses-0-street": "Some address", } event = self.Event() self.session.add(event) form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() def test_assigment_and_deletion(self): self.save() event = self.session.query(self.Event).first() assert event.location.name == "Some location" assert event.location.addresses[0].street == "Some address" data = {"name": "Some event"} form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() event = self.session.query(self.Event).first() assert event.location.addresses == [] wtforms-alchemy-0.19.0/tests/test_descriptions.py000066400000000000000000000012231471661042300222050ustar00rootroot00000000000000from tests import ModelFormTestCase from wtforms_alchemy import ModelForm class TestFieldParameters(ModelFormTestCase): def test_assigns_description_from_column_info(self): self.init(info={"description": "Description"}) self.assert_description("test_column", "Description") def test_assigns_descriptions_from_form_configuration(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest field_args = {"test_column": {"description": "TESTING"}} self.form_class = ModelTestForm self.assert_description("test_column", "TESTING") wtforms-alchemy-0.19.0/tests/test_field_exclusion.py000066400000000000000000000016341471661042300226610ustar00rootroot00000000000000from datetime import datetime import sqlalchemy as sa from sqlalchemy_utils import TSVectorType from tests import ModelFormTestCase class TestFieldExclusion(ModelFormTestCase): def test_does_not_include_datetime_columns_with_default(self): self.init(sa.DateTime, default=datetime.now()) assert not self.has_field("test_column") def test_excludes_surrogate_primary_keys_by_default(self): self.init() assert not self.has_field("id") def test_excludes_column_properties(self): self.init() self.ModelTest.calculated_value = sa.orm.column_property( sa.func.lower(self.ModelTest.test_column) ) self.init_form() self.form_class() class TestTSVectorType(ModelFormTestCase): def test_does_not_include_tsvector_typed_columns_with_default(self): self.init(TSVectorType) assert not self.has_field("test_column") wtforms-alchemy-0.19.0/tests/test_field_order.py000066400000000000000000000017471471661042300217700ustar00rootroot00000000000000import sqlalchemy as sa from tests import ModelFormTestCase class TestFieldOrder(ModelFormTestCase): def setup_method(self, method): ModelFormTestCase.setup_method(self, method) class ModelTest(self.base): __tablename__ = "model_test" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) full_description = sa.Column(sa.UnicodeText) description = sa.Column(sa.UnicodeText) start_time = sa.Column(sa.DateTime) end_time = sa.Column(sa.DateTime) category = sa.Column(sa.Unicode(255)) entry_fee = sa.Column(sa.Numeric) type = sa.Column(sa.Unicode(255)) self.ModelTest = ModelTest self.init_form() def test_field_definition_order(self): field_names = [field.name for field in self.form_class()] assert field_names == sa.inspect(self.ModelTest).attrs.keys()[1:] wtforms-alchemy-0.19.0/tests/test_field_parameters.py000066400000000000000000000101071471661042300230060ustar00rootroot00000000000000from datetime import date, time import sqlalchemy as sa from sqlalchemy_utils import IntRangeType from wtforms import widgets from wtforms.fields import StringField from wtforms.validators import NumberRange from wtforms_components import DateRange, TimeRange from tests import ModelFormTestCase from wtforms_alchemy import ModelForm class TestFieldParameters(ModelFormTestCase): def test_accepts_custom_widgets(self): self.init(info={"widget": widgets.HiddenInput()}) form = self.form_class() assert isinstance(form.test_column.widget, widgets.HiddenInput) def test_accepts_custom_filters(self): def test_filter(a): return a self.init(info={"filters": [test_filter]}) form = self.form_class() assert test_filter in form.test_column.filters def test_assigns_description_from_column_info(self): self.init(info={"description": "Description"}) self.assert_description("test_column", "Description") def test_does_not_add_default_value_if_default_is_callable(self): self.init(default=lambda: "test") self.assert_default("test_column", None) def test_assigns_scalar_defaults(self): self.init(default="test") self.assert_default("test_column", "test") def test_min_and_max_info_attributes_with_integer_field(self): self.init(type_=sa.Integer, info={"min": 1, "max": 100}) validator = self.get_validator("test_column", NumberRange) assert validator.min == 1 assert validator.max == 100 def test_min_and_max_info_attributes_with_numeric_field(self): self.init(type_=sa.Numeric, info={"min": 1, "max": 100}) validator = self.get_validator("test_column", NumberRange) assert validator.min == 1 assert validator.max == 100 def test_min_and_max_info_attributes_with_float_field(self): self.init(type_=sa.Float, info={"min": 1, "max": 100}) validator = self.get_validator("test_column", NumberRange) assert validator.min == 1 assert validator.max == 100 def test_min_and_max_info_attributes_with_int_range_field(self): self.init(type_=IntRangeType, info={"min": 1, "max": 100}) validator = self.get_validator("test_column", NumberRange) assert validator.min == 1 assert validator.max == 100 def test_min_and_max_info_attributes_generate_time_range_validator(self): self.init(type_=sa.types.Time, info={"min": time(12, 30), "max": time(14, 30)}) validator = self.get_validator("test_column", TimeRange) assert validator.min == time(12, 30) assert validator.max == time(14, 30) def test_min_and_max_info_attributes_generate_date_range_validator(self): self.init( type_=sa.Date, info={"min": date(1990, 1, 1), "max": date(2000, 1, 1)} ) validator = self.get_validator("test_column", DateRange) assert validator.min == date(1990, 1, 1) assert validator.max == date(2000, 1, 1) def test_uses_custom_field_class(self): class InputTest(widgets.Input): input_type = "color" validation_attrs = [] class FieldTest(StringField): widget = InputTest() class ModelTest(self.base): __tablename__ = "model_test" query = None id = sa.Column(sa.Integer, primary_key=True) test_column = sa.Column( sa.UnicodeText, info={"form_field_class": FieldTest} ) class ModelTestForm(ModelForm): class Meta: model = ModelTest form = ModelTestForm() assert 'type="color"' in str(form.test_column) def test_accepts_none_as_custom_field_class(self): class ModelTest(self.base): __tablename__ = "model_test" query = None id = sa.Column(sa.Integer, primary_key=True) test_column = sa.Column(sa.UnicodeText, info={"form_field_class": None}) class ModelTestForm(ModelForm): class Meta: model = ModelTest assert ModelTestForm() wtforms-alchemy-0.19.0/tests/test_field_trimming.py000066400000000000000000000015461471661042300225000ustar00rootroot00000000000000from tests import ModelFormTestCase, MultiDict from wtforms_alchemy import ModelForm class TestStringFieldTrimming(ModelFormTestCase): def test_strip_string_fields_set_for_string_field(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest strip_string_fields = True f = ModelTestForm(MultiDict([("test_column", "strip this ")])) assert f.test_column.data == "strip this" def test_does_not_trim_fields_when_trim_param_is_false(self): self.init(info={"trim": False}) class ModelTestForm(ModelForm): class Meta: model = self.ModelTest strip_string_fields = True f = ModelTestForm(MultiDict([("test_column", "strip this ")])) assert f.test_column.data == "strip this " wtforms-alchemy-0.19.0/tests/test_form_meta.py000066400000000000000000000021731471661042300214550ustar00rootroot00000000000000import sqlalchemy as sa from tests import ModelFormTestCase class TestModelFormMetaWithInheritance(ModelFormTestCase): def test_skip_unknown_types(self, model_form_all): self.init(type_=sa.Integer) class ModelTestForm(model_form_all): class Meta: skip_unknown_types = True class ModelTestForm2(ModelTestForm): class Meta: model = self.ModelTest self.form_class = ModelTestForm2 assert self.form_class.Meta.skip_unknown_types is True def test_inheritance_attributes(self, model_form_custom): self.init(type_=sa.Integer) class ModelTestForm(model_form_custom): class Meta: model = self.ModelTest assert ModelTestForm.test_attr == "SomeVal" class TestUnboundFieldsInitialization(ModelFormTestCase): def test_skip_unknown_types(self, model_form_all): self.init(type_=sa.Integer) class ModelTestForm(model_form_all): class Meta: model = self.ModelTest skip_unknown_types = True assert ModelTestForm.test_column wtforms-alchemy-0.19.0/tests/test_hybrid_properties.py000066400000000000000000000037721471661042300232470ustar00rootroot00000000000000import sqlalchemy as sa from pytest import raises from sqlalchemy.ext.hybrid import hybrid_property from tests import ModelFormTestCase from wtforms_alchemy import ModelForm from wtforms_alchemy.exc import AttributeTypeException class TestHybridProperties(ModelFormTestCase): def test_hybrid_property_returning_column_property(self): class ModelTest(self.base): __tablename__ = "model_test" id = sa.Column(sa.Integer, primary_key=True) _test_column = sa.Column("test_column", sa.Boolean, nullable=False) @hybrid_property def test_column_hybrid(self): return self._test_column @test_column_hybrid.setter def test_column_hybrid(self, value): self._test_column = value class ModelTestForm(ModelForm): class Meta: model = ModelTest not_null_str_validator = None not_null_validator = None include = ("test_column_hybrid",) exclude = ("_test_column",) form = ModelTestForm() assert form.test_column_hybrid def test_hybrid_property_returning_expression(self): class ModelTest(self.base): __tablename__ = "model_test" id = sa.Column(sa.Integer, primary_key=True) _test_column = sa.Column("test_column", sa.Boolean, nullable=False) @hybrid_property def test_column_hybrid(self): return self._test_column + self._test_column @test_column_hybrid.setter def test_column_hybrid(self, value): self._test_column = value with raises(AttributeTypeException): class ModelTestForm(ModelForm): class Meta: model = ModelTest not_null_str_validator = None not_null_validator = None include = ("test_column_hybrid",) exclude = ("_test_column",) wtforms-alchemy-0.19.0/tests/test_i18n_extension.py000066400000000000000000000040001471661042300223460ustar00rootroot00000000000000import sqlalchemy as sa from packaging.version import Version from pytest import raises, skip from sqlalchemy_i18n import make_translatable, Translatable, translation_base from tests import ModelFormTestCase, MultiDict from wtforms_alchemy import ModelForm sqlalchemy_version = sa.__version__ if Version(sqlalchemy_version) >= Version("2.0"): skip("sqlalchemy_i18n does not support SQLAlchemy 2.0", allow_module_level=True) make_translatable() class TestInternationalizationExtension(ModelFormTestCase): def init(self): class ModelTest(self.base, Translatable): __tablename__ = "model_test" __translatable__ = {"locales": ["fi", "en"]} id = sa.Column(sa.Integer, primary_key=True) some_property = "something" locale = "en" class ModelTranslation(translation_base(ModelTest)): __tablename__ = "model_translation" name = sa.Column(sa.Unicode(255)) content = sa.Column(sa.Unicode(255)) self.ModelTest = ModelTest sa.orm.configure_mappers() Session = sa.orm.sessionmaker(bind=self.engine) self.session = Session() self.init_form() def test_supports_translated_columns(self): self.init() form = self.form_class() assert form.name assert form.content def test_supports_field_exclusion(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest exclude = ["name"] with raises(AttributeError): ModelTestForm().name def test_model_population(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest form = ModelTestForm( MultiDict([("name", "something"), ("content", "something")]) ) obj = self.ModelTest() form.populate_obj(obj) assert obj.name == "something" assert obj.content == "something" wtforms-alchemy-0.19.0/tests/test_inheritance.py000066400000000000000000000020551471661042300217740ustar00rootroot00000000000000from wtforms import Form from wtforms_test import FormTestCase from wtforms_alchemy import model_form_factory, ModelForm class TestInheritance(FormTestCase): class Base(Form): @classmethod def get_session(self): return "TestSession" def test_default_base(self): assert ModelForm.get_session is None def test_custom_base_without_session(self): cls = model_form_factory(Form) assert cls.get_session is None def test_custom_base_with_session(self): cls = model_form_factory(self.Base) assert cls.get_session() == "TestSession" def test_inherit_with_new_session(self): cls = model_form_factory(self.Base) class Sub(cls): @classmethod def get_session(self): return "SubTestSession" assert Sub.get_session() == "SubTestSession" def test_inherit_without_new_session(self): cls = model_form_factory(self.Base) class Sub(cls): pass assert Sub.get_session() == "TestSession" wtforms-alchemy-0.19.0/tests/test_labels.py000066400000000000000000000011541471661042300207440ustar00rootroot00000000000000from tests import ModelFormTestCase from wtforms_alchemy import ModelForm class TestFieldLabels(ModelFormTestCase): def test_assigns_labels_from_column_info(self): self.init(info={"label": "Test Column"}) self.assert_label("test_column", "Test Column") def test_assigns_labels_from_form_configuration(self): self.init() class ModelTestForm(ModelForm): class Meta: model = self.ModelTest field_args = {"test_column": {"label": "TESTING"}} self.form_class = ModelTestForm self.assert_label("test_column", "TESTING") wtforms-alchemy-0.19.0/tests/test_model_field_list.py000066400000000000000000000177701471661042300230130ustar00rootroot00000000000000import sqlalchemy as sa from wtforms.fields import FormField from wtforms_components import PassiveHiddenField from tests import FormRelationsTestCase, MultiDict from wtforms_alchemy import ModelFieldList, ModelForm class ModelFieldListTestCase(FormRelationsTestCase): def create_models(self): class Event(self.base): __tablename__ = "event" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) class Location(self.base): __tablename__ = "location" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id)) event = sa.orm.relationship(Event, backref="locations") self.Event = Event self.Location = Location def save(self, event=None, data=None): if data is None: data = { "name": "Some event", "locations-0-name": "Some location", "locations-0-description": "Some description", } if not event: event = self.Event() self.session.add(event) form = self.EventForm(MultiDict(data)) else: form = self.EventForm(MultiDict(data), obj=event) form.validate() form.populate_obj(event) self.session.commit() return event class TestReplaceStrategy(ModelFieldListTestCase): def create_forms(self): class LocationForm(ModelForm): class Meta: model = self.Location class EventForm(ModelForm): class Meta: model = self.Event locations = ModelFieldList(FormField(LocationForm)) self.LocationForm = LocationForm self.EventForm = EventForm def test_assigment_and_deletion(self): self.save() event = self.session.query(self.Event).first() assert event.locations[0].name == "Some location" data = {"name": "Some event"} form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() event = self.session.query(self.Event).first() assert event.locations == [] class TestUpdateStrategy(ModelFieldListTestCase): def create_models(self): class Event(self.base): __tablename__ = "event" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) class Location(self.base): __tablename__ = "location" TYPES = ("", "football field", "restaurant") id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) description = sa.Column(sa.Unicode(255), default="") type = sa.Column( sa.Unicode(255), info={"choices": zip(TYPES, TYPES)}, default="" ) event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id)) event = sa.orm.relationship(Event, backref="locations") def __repr__(self): return f"Location(id={self.id!r}, name={self.name!r})" self.Event = Event self.Location = Location def create_forms(self): class LocationForm(ModelForm): class Meta: model = self.Location only = ["name", "description", "type"] id = PassiveHiddenField() class EventForm(ModelForm): class Meta: model = self.Event locations = ModelFieldList( FormField(LocationForm), population_strategy="update" ) self.LocationForm = LocationForm self.EventForm = EventForm def test_single_entry_update(self): event = self.save() location_id = event.locations[0].id data = { "name": "Some event", "locations-0-id": location_id, "locations-0-name": "Some other location", } self.save(event, data) assert len(event.locations) == 1 assert event.locations[0].id == location_id assert event.locations[0].name == "Some other location" def test_creates_new_objects_for_entries_with_unknown_identifiers(self): event = self.save() location_id = event.locations[0].id data = { "name": "Some event", "locations-0-id": 12, "locations-0-name": "Some other location", } self.save(event, data) assert event.locations assert event.locations[0].id != location_id def test_replace_entry(self): data = { "name": "Some event", "locations-0-name": "Some location", "locations-0-description": "Some description", "locations-0-type": "restaurant", } event = self.save(data=data) location_id = event.locations[0].id self.session.commit() data = { "name": "Some event", "locations-0-name": "Some other location", } self.save(event, data) location = event.locations[0] assert location.id != location_id assert location.name == "Some other location" assert location.description == "" assert location.type == "" assert len(event.locations) == 1 def test_replace_and_update(self): data = { "name": "Some event", "locations-0-name": "Location 1", "locations-0-description": "Location 1 description", "locations-1-name": "Location 2", "locations-1-description": "Location 2 description", } event = self.save(data=data) self.session.commit() data = { "name": "Some event", "locations-0-id": event.locations[1].id, "locations-0-name": "Location 2 updated", "locations-0-description": "Location 2 description updated", "locations-1-name": "Location 3", } self.save(event, data) self.session.commit() location = event.locations[0] location2 = event.locations[1] assert location.name == "Location 2 updated" assert location.description == "Location 2 description updated" assert len(event.locations) == 2 assert location2.name == "Location 3" assert location2.description == "" def test_multiple_entries(self): event = self.save() location_id = event.locations[0].id data = { "name": "Some event", "locations-0-name": "Some location", "locations-1-id": str(location_id), # test coercing works "locations-1-name": "Some other location", "locations-2-name": "Third location", "locations-3-id": 123, "locations-3-name": "Fourth location", } self.save(event, data) assert len(event.locations) == 4 assert event.locations[0].id == location_id assert event.locations[0].name == "Some other location" assert event.locations[1].name == "Some location" assert event.locations[2].name == "Third location" assert event.locations[3].name == "Fourth location" def test_delete_all_field_list_entries(self): event = self.save() data = {"name": "Some event"} self.save(event, data) assert not event.locations def test_update_and_remove(self): location = self.Location(name="Location #2") event = self.Event( name="Some event", locations=[self.Location(name="Location #1"), location] ) self.session.add(event) self.session.commit() data = { "locations-0-id": location.id, "locations-0-name": "Location", } self.save(event, data) self.session.refresh(event) assert len(event.locations) == 1 assert event.locations[0] == location wtforms-alchemy-0.19.0/tests/test_model_form_factory.py000066400000000000000000000070141471661042300233550ustar00rootroot00000000000000import wtforms from pytest import raises from wtforms import Form from tests import ModelFormTestCase from wtforms_alchemy import ( FormGenerator, model_form_factory, UnknownConfigurationOption, ) class TestModelFormFactory(ModelFormTestCase): def test_supports_parameter_overriding(self): self.init() class MyFormGenerator(FormGenerator): pass defaults = { "all_fields_optional": True, "only_indexed_fields": True, "include_primary_keys": True, "include_foreign_keys": True, "strip_string_fields": True, "include_datetimes_with_default": True, "form_generator": True, "date_format": "%d-%m-%Y", "datetime_format": "%Y-%m-%dT%H:%M:%S", } ModelForm = model_form_factory(Form, **defaults) for key, value in defaults.items(): assert getattr(ModelForm.Meta, key) == value def test_throws_exception_for_unknown_configuration_option(self): self.init() class SomeForm(Form): pass defaults = {"unknown": "something"} with raises(UnknownConfigurationOption): model_form_factory(SomeForm, **defaults) def test_supports_custom_base_class_with_model_form_factory(self): self.init() class SomeForm(Form): pass class TestCustomBase(model_form_factory(SomeForm)): class Meta: model = self.ModelTest assert isinstance(TestCustomBase(), SomeForm) def test_url_validator(self): form = model_form_factory(url_validator=None) assert form.Meta.url_validator is None def test_email_validator(self): form = model_form_factory(email_validator=None) assert form.Meta.email_validator is None def test_length_validator(self): form = model_form_factory(length_validator=None) assert form.Meta.length_validator is None def test_number_range_validator(self): form = model_form_factory(number_range_validator=None) assert form.Meta.number_range_validator is None def test_date_range_validator(self): form = model_form_factory(date_range_validator=None) assert form.Meta.date_range_validator is None def test_time_range_validator(self): form = model_form_factory(time_range_validator=None) assert form.Meta.time_range_validator is None def test_optional_validator(self): form = model_form_factory(optional_validator=None) assert form.Meta.optional_validator is None def test_unique_validator(self): form = model_form_factory(unique_validator=None) assert form.Meta.unique_validator is None def test_class_meta_wtforms2(self): self.init() class SomeForm(Form): class Meta: locales = ["fr"] foo = 9 class OtherForm(SomeForm): class Meta: pass class TestCustomBase(model_form_factory(SomeForm)): class Meta: model = self.ModelTest form = TestCustomBase() other_form = OtherForm() assert isinstance(form.meta, wtforms.meta.DefaultMeta) assert form.meta.locales == ["fr"] assert hasattr(form.meta, "model") assert hasattr(form.meta, "csrf") assert form.meta.foo == 9 # Create a side effect on the base meta. SomeForm.Meta.foo = 12 assert other_form.meta.foo == 12 assert form.meta.foo == 12 wtforms-alchemy-0.19.0/tests/test_model_form_field.py000066400000000000000000000054141471661042300227730ustar00rootroot00000000000000import sqlalchemy as sa from pytest import raises from tests import FormRelationsTestCase, MultiDict from wtforms_alchemy import ModelForm, ModelFormField class TestOneToOneModelFormRelations(FormRelationsTestCase): def create_models(self): class Location(self.base): __tablename__ = "location" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=True) class Event(self.base): __tablename__ = "event" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255), nullable=False) location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id)) location = sa.orm.relationship(Location) self.Event = Event self.Location = Location def create_forms(self): class LocationForm(ModelForm): class Meta: model = self.Location class EventForm(ModelForm): class Meta: model = self.Event location = ModelFormField(LocationForm) self.LocationForm = LocationForm self.EventForm = EventForm def save(self, event=None, data={}): if not data: data = { "name": "Some event", "location-name": "Some location", } if not event: event = self.Event() self.session.add(event) form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() return event def test_assigment_and_deletion(self): self.save() event = self.session.query(self.Event).first() assert event.location.name == "Some location" data = {"name": "Some event"} form = self.EventForm(MultiDict(data)) form.validate() form.populate_obj(event) self.session.commit() event = self.session.query(self.Event).first() assert not event.location.name def test_only_populates_related_if_they_are_obj_attributes(self): class EventForm(ModelForm): class Meta: model = self.Event unknown_field = ModelFormField(self.LocationForm) self.EventForm = EventForm with raises(TypeError): self.save( data={ "name": "Some event", "unknown_field-name": "Some location", } ) def test_updating_related_object(self): event = self.save() location_id = event.location.id self.save(event, {"name": "some name", "location-name": "Some other location"}) assert event.name == "some name" assert event.location.id == location_id wtforms-alchemy-0.19.0/tests/test_phone_number.py000066400000000000000000000054511471661042300221670ustar00rootroot00000000000000import sqlalchemy as sa from pytest import mark from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy.orm.session import close_all_sessions from sqlalchemy_utils.types import phone_number from tests import MultiDict from wtforms_alchemy import ModelForm class TestCase: def setup_method(self, method): self.engine = create_engine("sqlite:///:memory:") self.Base = declarative_base() self.create_models() self.Base.metadata.create_all(self.engine) Session = sessionmaker(bind=self.engine) self.session = Session() def teardown_method(self, method): close_all_sessions() self.Base.metadata.drop_all(self.engine) self.engine.dispose() def create_models(self): class User(self.Base): __tablename__ = "user" id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) name = sa.Column(sa.Unicode(255)) phone_number = sa.Column(phone_number.PhoneNumberType(country_code="FI")) self.User = User @mark.xfail("phone_number.phonenumbers is None") class TestPhoneNumbers(TestCase): """ Simple tests to ensure that sqlalchemy_utils.PhoneNumber, wtforms_alchemy.PhoneNumberType and sqlalchemy_utils.PhoneNumberField work nicely together. """ def setup_method(self, method): super().setup_method(method) class UserForm(ModelForm): class Meta: model = self.User self.UserForm = UserForm super().setup_method(method) self.phone_number = phone_number.PhoneNumber("040 1234567", "FI") self.user = self.User() self.user.name = "Someone" self.user.phone_number = self.phone_number self.session.add(self.user) self.session.commit() def test_query_returns_phone_number_object(self): queried_user = self.session.query(self.User).first() assert queried_user.phone_number == self.phone_number def test_phone_number_is_stored_as_string(self): result = self.session.execute( sa.text("SELECT phone_number FROM user WHERE id=:param"), {"param": self.user.id}, ) assert result.first()[0] == "+358401234567" def test_phone_number_in_form(self): form = self.UserForm( MultiDict(name="Matti Meikalainen", phone_number="+358401231233") ) form.validate() assert len(form.errors) == 0 assert form.data["phone_number"] == (phone_number.PhoneNumber("+358401231233")) def test_empty_phone_number_in_form(self): form = self.UserForm(MultiDict(name="Matti Meikalainen", phone_number="")) form.validate() assert len(form.errors) == 0 assert form.data["phone_number"] is None wtforms-alchemy-0.19.0/tests/test_phone_number_field.py000066400000000000000000000070241471661042300233300ustar00rootroot00000000000000import pytest from wtforms import Form from tests import MultiDict from wtforms_alchemy import DataRequired, PhoneNumberField class TestPhoneNumberField: def setup_method(self, method): self.valid_phone_numbers = [ "040 1234567", "+358 401234567", "09 2501234", "+358 92501234", "0800 939393", "09 4243 0456", "0600 900 500", ] self.invalid_phone_numbers = ["abc", "+040 1234567", "0111234567", "358"] def init_form(self, **kwargs): class TestForm(Form): phone_number = PhoneNumberField(**kwargs) return TestForm def test_valid_phone_numbers(self): form_class = self.init_form(region="FI") for phone_number in self.valid_phone_numbers: form = form_class(MultiDict(phone_number=phone_number)) form.validate() assert len(form.errors) == 0 def test_invalid_phone_numbers(self): form_class = self.init_form(region="FI") for phone_number in self.invalid_phone_numbers: form = form_class(MultiDict(phone_number=phone_number)) form.validate() assert len(form.errors["phone_number"]) == 1 def test_render_empty_phone_number_value(self): form_class = self.init_form(region="FI") form = form_class(MultiDict(phone_number="")) assert 'value=""' in form.phone_number() def test_empty_phone_number_value_passed_as_none(self): form_class = self.init_form(region="FI") form = form_class(MultiDict(phone_number="")) form.validate() assert len(form.errors) == 0 assert form.data["phone_number"] is None def test_default_display_format(self): form_class = self.init_form(region="FI") form = form_class(MultiDict(phone_number="+358401234567")) assert 'value="040 1234567"' in form.phone_number() def test_international_display_format(self): form_class = self.init_form(region="FI", display_format="international") form = form_class(MultiDict(phone_number="0401234567")) assert 'value="+358 40 1234567"' in form.phone_number() def test_e164_display_format(self): form_class = self.init_form(region="FI", display_format="e164") form = form_class(MultiDict(phone_number="0401234567")) assert 'value="+358401234567"' in form.phone_number() def test_field_rendering_when_invalid_phone_number(self): form_class = self.init_form() form = form_class(MultiDict(phone_number="invalid")) form.validate() assert 'value="invalid"' in form.phone_number() @pytest.mark.parametrize( "number,error_msg,check_value", ( ("", "This field is required.", lambda v, orig: v is None), ("1", "Not a valid phone number value", lambda v, orig: v is not None), ("123", "Not a valid phone number value", lambda v, orig: v is not None), ("+46123456789", None, lambda v, orig: v.e164 == orig), ), ) def test_required_phone_number_form(self, number, error_msg, check_value): class PhoneNumberForm(Form): phone = PhoneNumberField("Phone number", validators=[DataRequired()]) form = PhoneNumberForm(MultiDict(phone=number)) form.validate() if error_msg: assert len(form.errors) == 1 assert form.errors["phone"][0] == error_msg else: assert len(form.errors) == 0 assert check_value(form.phone.data, number) is True wtforms-alchemy-0.19.0/tests/test_query_select_field.py000066400000000000000000000327021471661042300233540ustar00rootroot00000000000000import sqlalchemy as sa from sqlalchemy.orm import declarative_base from sqlalchemy.orm.session import close_all_sessions from wtforms import Form from wtforms_alchemy import ( GroupedQuerySelectField, GroupedQuerySelectMultipleField, ModelForm, QuerySelectField, QuerySelectMultipleField, ) class DummyPostData(dict): def getlist(self, key): v = self[key] if not isinstance(v, (list, tuple)): v = [v] return v class LazySelect: def __call__(self, field, **kwargs): return list( (val, str(label), selected) for val, label, selected, _ in field.iter_choices() ) class Base: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) class TestBase: def create_models(self): class Test(self.base): __tablename__ = "test" id = sa.Column(sa.Integer, primary_key=True, nullable=False) name = sa.Column(sa.String, nullable=False) self.Test = Test class PKTest(self.base): __tablename__ = "pk_test" foobar = sa.Column(sa.String, primary_key=True, nullable=False) baz = sa.Column(sa.String, nullable=False) def __str__(self): return self.baz self.PKTest = PKTest def _fill(self, sess): for i, n in [(1, "apple"), (2, "banana")]: s = self.Test(id=i, name=n) p = self.PKTest(foobar=f"hello{i}", baz=n) sess.add(s) sess.add(p) sess.flush() sess.commit() class TestQuerySelectField(TestBase): def setup_method(self): self.engine = sa.create_engine("sqlite:///:memory:", echo=False) self.base = declarative_base() self.create_models() self.base.metadata.create_all(self.engine) Session = sa.orm.session.sessionmaker(bind=self.engine) self.session = Session() def teardown_method(self): close_all_sessions() self.base.metadata.drop_all(self.engine) self.engine.dispose() def test_without_factory(self): self._fill(self.session) class F(Form): a = QuerySelectField( get_label="name", widget=LazySelect(), get_pk=lambda x: x.id ) form = F(DummyPostData(a=["1"])) form.a.query = self.session.query(self.Test) assert form.a.data is not None assert form.a.data.id, 1 assert form.a(), [("1", "apple", True), ("2", "banana", False)] assert form.validate() form = F(a=self.session.query(self.Test).filter_by(name="banana").first()) form.a.query = self.session.query(self.Test).filter(self.Test.name != "banana") assert not form.validate() assert form.a.errors, ["Not a valid choice"] # Test query with no results form = F() form.a.query = ( self.session.query(self.Test) .filter(self.Test.id == 1, self.Test.id != 1) .all() ) assert form.a() == [] def test_with_query_factory(self): self._fill(self.session) class F(Form): a = QuerySelectField( get_label=(lambda model: model.name), query_factory=lambda: self.session.query(self.Test), widget=LazySelect(), ) b = QuerySelectField( allow_blank=True, query_factory=lambda: self.session.query(self.PKTest), widget=LazySelect(), ) form = F() assert form.a.data is None assert form.a() == [("1", "apple", False), ("2", "banana", False)] assert form.b.data is None assert form.b() == [ ("__None", "", True), ("hello1", "apple", False), ("hello2", "banana", False), ] assert not form.validate() # Test query with no results form = F() form.a.query = ( self.session.query(self.Test) .filter(self.Test.id == 1, self.Test.id != 1) .all() ) assert form.a() == [] form = F(DummyPostData(a=["1"], b=["hello2"])) assert form.a.data.id == 1 assert form.a() == [("1", "apple", True), ("2", "banana", False)] assert form.b.data.baz == "banana" assert form.b() == [ ("__None", "", False), ("hello1", "apple", False), ("hello2", "banana", True), ] assert form.validate() # Make sure the query is cached self.session.add(self.Test(id=3, name="meh")) self.session.flush() self.session.commit() assert form.a() == [("1", "apple", True), ("2", "banana", False)] form.a._object_list = None assert form.a() == [ ("1", "apple", True), ("2", "banana", False), ("3", "meh", False), ] # Test bad data form = F(DummyPostData(b=["__None"], a=["fail"])) assert not form.validate() assert form.a.errors == ["Not a valid choice"] assert form.b.errors == [] assert form.b.data is None class TestQuerySelectMultipleField(TestBase): def setup_method(self): self.engine = sa.create_engine("sqlite:///:memory:", echo=False) self.base = declarative_base() self.create_models() self.base.metadata.create_all(self.engine) Session = sa.orm.session.sessionmaker(bind=self.engine) self.session = Session() self._fill(self.session) def teardown_method(self): close_all_sessions() self.base.metadata.drop_all(self.engine) self.engine.dispose() class F(Form): a = QuerySelectMultipleField(get_label="name", widget=LazySelect()) def test_unpopulated_default(self): form = self.F() assert [] == form.a.data def test_single_value_without_factory(self): form = self.F(DummyPostData(a=["1"])) form.a.query = self.session.query(self.Test) assert [1] == [v.id for v in form.a.data] assert form.a() == [("1", "apple", True), ("2", "banana", False)] assert form.validate() def test_multiple_values_without_query_factory(self): form = self.F(DummyPostData(a=["1", "2"])) form.a.query = self.session.query(self.Test) assert [1, 2] == [v.id for v in form.a.data] assert form.a() == [("1", "apple", True), ("2", "banana", True)] assert form.validate() form = self.F(DummyPostData(a=["1", "3"])) form.a.query = self.session.query(self.Test) assert [x.id for x in form.a.data], [1] assert not form.validate() def test_single_default_value(self): first_test = self.session.get(self.Test, 2) class F(Form): a = QuerySelectMultipleField( get_label="name", default=[first_test], widget=LazySelect(), query_factory=lambda: self.session.query(self.Test), ) form = F() assert [v.id for v in form.a.data], [2] assert form.a(), [("1", "apple", False), ("2", "banana", True)] assert form.validate() def test_empty_query(self): # Test query with no results form = self.F() form.a.query = ( self.session.query(self.Test) .filter(self.Test.id == 1, self.Test.id != 1) .all() ) assert form.a() == [] class DatabaseTestCase: def setup_method(self, method): self.engine = sa.create_engine("sqlite:///:memory:") self.base = declarative_base() self.create_models() self.base.metadata.create_all(self.engine) Session = sa.orm.session.sessionmaker(bind=self.engine) self.session = Session() def teardown_method(self, method): close_all_sessions() self.base.metadata.drop_all(self.engine) self.engine.dispose() def create_models(self): class City(self.base): __tablename__ = "city" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String) country = sa.Column(sa.String) state_id = sa.Column(sa.Integer, sa.ForeignKey("state.id")) self.City = City class State(self.base): __tablename__ = "state" id = sa.Column(sa.Integer, primary_key=True) cities = sa.orm.relationship("City") self.State = State def create_cities(self): self.session.add_all( [ self.City(name="Helsinki", country="Finland"), self.City(name="Vantaa", country="Finland"), self.City(name="New York", country="USA"), self.City(name="Washington", country="USA"), self.City(name="Stockholm", country="Sweden"), ] ) class TestGroupedQuerySelectField(DatabaseTestCase): def create_form(self, **kwargs): query = self.session.query(self.City).order_by("name", "country") class MyForm(Form): city = GroupedQuerySelectField( label=kwargs.get("label", "City"), query_factory=kwargs.get("query_factory", lambda: query), get_label=kwargs.get("get_label", lambda c: c.name), get_group=kwargs.get("get_group", lambda c: c.country), allow_blank=kwargs.get("allow_blank", False), blank_text=kwargs.get("blank_text", ""), blank_value=kwargs.get("blank_value", "__None"), ) return MyForm def test_custom_none_value(self): self.create_cities() MyForm = self.create_form( allow_blank=True, blank_text="Choose city...", blank_value="" ) form = MyForm(DummyPostData({"city": ""})) assert form.validate(), form.errors assert '' in (str(form.city)) def test_rendering(self): MyForm = self.create_form() self.create_cities() assert str(MyForm().city).replace("\n", "") == ( '" ) class TestGroupedQuerySelectMultipleField(DatabaseTestCase): def create_form(self, **kwargs): query = self.session.query(self.City).order_by("name", "country") class MyForm(ModelForm): class Meta: model = self.State cities = GroupedQuerySelectMultipleField( label=kwargs.get("label", "City"), query_factory=kwargs.get("query_factory", lambda: query), get_label=kwargs.get("get_label", lambda c: c.name), get_group=kwargs.get("get_group", lambda c: c.country), blank_text=kwargs.get("blank_text", ""), ) return MyForm def test_unpopulated_default(self): MyForm = self.create_form() self.create_cities() assert MyForm().cities.data == [] def test_single_value_without_factory(self): obj = self.State() MyForm = self.create_form() self.create_cities() form = MyForm(DummyPostData(cities=["1"]), obj=obj) assert [1] == [v.id for v in form.cities.data] assert form.validate() form.populate_obj(obj) assert [city.id for city in obj.cities] == [1] def test_multiple_values_without_query_factory(self): obj = self.State() MyForm = self.create_form() self.create_cities() form = MyForm(DummyPostData(cities=["1", "2"]), obj=obj) form.cities.query = self.session.query(self.City) assert [1, 2] == [v.id for v in form.cities.data] assert form.validate() form.populate_obj(obj) assert [city.id for city in obj.cities] == [1, 2] form = MyForm(DummyPostData(cities=["1", "666"])) form.cities.query = self.session.query(self.City) assert not form.validate() assert [x.id for x in form.cities.data] == [1] assert not form.validate() form.populate_obj(obj) assert [city.id for city in obj.cities] == [1] form = MyForm(DummyPostData(cities=["666"])) form.cities.query = self.session.query(self.City) assert not form.validate() assert [x.id for x in form.cities.data] == [] assert not form.validate() form.populate_obj(obj) assert [city.id for city in obj.cities] == [] def test_rendering(self): MyForm = self.create_form() self.create_cities() assert str(MyForm().cities).replace("\n", "") == ( '" ) wtforms-alchemy-0.19.0/tests/test_select_field.py000066400000000000000000000064611471661042300221320ustar00rootroot00000000000000from decimal import Decimal import six import sqlalchemy as sa from wtforms_components import SelectField from tests import ModelFormTestCase class MultiDict(dict): def getlist(self, key): return [self[key]] class TestSelectFieldDefaultValue(ModelFormTestCase): def test_option_selected_by_field_default_value(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.Integer, default="1", info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2"})) assert '' in str(form.test_column) class TestSelectFieldCoerce(ModelFormTestCase): def test_integer_coerces_values_to_integers(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.Integer, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2"})) assert form.test_column.data == 2 def test_nullable_integer_coerces_values_to_integers(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.Integer, nullable=True, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2"})) assert form.test_column.data == 2 def test_integer_coerces_empty_strings_to_nulls(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.Integer, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": ""})) assert form.test_column.data is None def test_big_integer_coerces_values_to_integers(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.BigInteger, info={"choices": choices}) self.assert_type("test_column", SelectField) form = self.form_class(MultiDict({"test_column": "2"})) assert form.test_column.data == 2 def test_small_integer_coerces_values_to_integers(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.SmallInteger, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2"})) assert form.test_column.data == 2 def test_numeric_coerces_values_to_decimals(self): choices = [("1.0", "1.0"), ("2.0", "2.0")] self.init(type_=sa.Numeric, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2.0"})) assert form.test_column.data == Decimal("2.0") def test_float_coerces_values_to_floats(self): choices = [("1.0", "1.0"), ("2.0", "2.0")] self.init(type_=sa.Float, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2.0"})) assert form.test_column.data == 2.0 def test_unicode_coerces_values_to_unicode_strings(self): choices = [("1.0", "1.0"), ("2.0", "2.0")] self.init(type_=sa.Unicode(255), info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2.0"})) assert form.test_column.data == "2.0" assert isinstance(form.test_column.data, six.text_type) def test_unicode_text_coerces_values_to_unicode_strings(self): choices = [("1.0", "1.0"), ("2.0", "2.0")] self.init(type_=sa.UnicodeText, info={"choices": choices}) form = self.form_class(MultiDict({"test_column": "2.0"})) assert form.test_column.data == "2.0" assert isinstance(form.test_column.data, six.text_type) wtforms-alchemy-0.19.0/tests/test_synonym.py000066400000000000000000000037041471661042300212210ustar00rootroot00000000000000import sqlalchemy as sa from sqlalchemy.ext.hybrid import hybrid_property from tests import ModelFormTestCase from wtforms_alchemy import ModelForm class TestSynonym(ModelFormTestCase): def test_synonym_returning_column_property_with_include(self): class ModelTest(self.base): __tablename__ = "model_test" id = sa.Column(sa.Integer, primary_key=True) _test_column = sa.Column("test_column", sa.Integer, nullable=False) @hybrid_property def test_column_hybrid(self): return self.test_column * 2 @test_column_hybrid.setter def test_column_hybrid(self, value): self._test_column = value test_column = sa.orm.synonym("_test_column") class ModelTestForm(ModelForm): class Meta: model = ModelTest not_null_str_validator = None not_null_validator = None include = ("test_column",) exclude = ("_test_column",) form = ModelTestForm() assert form.test_column def test_synonym_returning_column_property_with_only(self): class ModelTest(self.base): __tablename__ = "model_test" id = sa.Column(sa.Integer, primary_key=True) _test_column = sa.Column("test_column", sa.Integer, nullable=False) @hybrid_property def test_column_hybrid(self): return self.test_column * 2 @test_column_hybrid.setter def test_column_hybrid(self, value): self._test_column = value test_column = sa.orm.synonym("_test_column") class ModelTestForm(ModelForm): class Meta: model = ModelTest not_null_str_validator = None not_null_validator = None only = ("test_column",) form = ModelTestForm() assert form.test_column wtforms-alchemy-0.19.0/tests/test_types.py000066400000000000000000000263251471661042300206550ustar00rootroot00000000000000from enum import Enum import sqlalchemy as sa from pytest import mark, raises from sqlalchemy_utils import ( ChoiceType, ColorType, CountryType, EmailType, IntRangeType, PasswordType, PhoneNumberType, URLType, UUIDType, ) from sqlalchemy_utils.types import arrow, phone_number, WeekDaysType # noqa from wtforms.fields import ( BooleanField, FloatField, PasswordField, TextAreaField, ) from wtforms.validators import Length, URL from wtforms_components import Email from wtforms_components.fields import ( ColorField, DateField, DateTimeField, DecimalField, EmailField, IntegerField, IntIntervalField, SelectField, StringField, TimeField, ) from tests import ModelFormTestCase from wtforms_alchemy import ( CountryField, ModelForm, null_or_unicode, PhoneNumberField, UnknownTypeException, WeekDaysField, ) from wtforms_alchemy.utils import ClassMap try: import passlib # noqa except ImportError: passlib = None class UnknownType(sa.types.UserDefinedType): def get_col_spec(self): return "UNKNOWN()" class CustomUnicodeTextType(sa.types.TypeDecorator): impl = sa.types.UnicodeText class CustomUnicodeType(sa.types.TypeDecorator): impl = sa.types.Unicode class CustomNumericType(sa.types.TypeDecorator): impl = sa.types.Numeric class TestModelColumnToFormFieldTypeConversion(ModelFormTestCase): def test_raises_exception_for_unknown_type(self): with raises(UnknownTypeException): self.init(type_=UnknownType) self.form_class() def test_raises_exception_for_array_type(self): with raises(UnknownTypeException): self.init(type_=sa.ARRAY(sa.Integer)) self.form_class() def test_unicode_converts_to_text_field(self): self.init() self.assert_type("test_column", StringField) def test_custom_unicode_converts_to_text_field(self): self.init(type_=CustomUnicodeType) self.assert_type("test_column", StringField) def test_string_converts_to_text_field(self): self.init(type_=sa.String) self.assert_type("test_column", StringField) def test_integer_converts_to_integer_field(self): self.init(type_=sa.Integer) self.assert_type("test_column", IntegerField) def test_unicode_text_converts_to_text_area_field(self): self.init(type_=sa.UnicodeText) self.assert_type("test_column", TextAreaField) def test_custom_unicode_text_converts_to_text_area_field(self): self.init(type_=CustomUnicodeTextType) self.assert_type("test_column", TextAreaField) def test_boolean_converts_to_boolean_field(self): self.init(type_=sa.Boolean) self.assert_type("test_column", BooleanField) def test_datetime_converts_to_datetime_field(self): self.init(type_=sa.DateTime) self.assert_type("test_column", DateTimeField) def test_date_converts_to_date_field(self): self.init(type_=sa.Date) self.assert_type("test_column", DateField) def test_float_converts_to_float_field(self): self.init(type_=sa.Float) self.assert_type("test_column", FloatField) def test_numeric_converts_to_decimal_field(self): self.init(type_=sa.Numeric) self.assert_type("test_column", DecimalField) def test_numeric_scale_converts_to_decimal_field_scale(self): self.init(type_=sa.Numeric(scale=4)) form = self.form_class() assert form.test_column.places == 4 def test_custom_numeric_converts_to_decimal_field(self): self.init(type_=CustomNumericType) self.assert_type("test_column", DecimalField) def test_enum_field_converts_to_select_field(self): choices = ["1", "2"] self.init(type_=sa.Enum(*choices)) self.assert_type("test_column", SelectField) form = self.form_class() assert form.test_column.choices == [(s, s) for s in choices] def test_nullable_enum_uses_null_or_unicode_coerce_func_by_default(self): choices = ["1", "2"] self.init(type_=sa.Enum(*choices), nullable=True) field = self._get_field("test_column") assert field.coerce == null_or_unicode def test_custom_choices_override_enum_choices(self): choices = ["1", "2"] custom_choices = [("2", "2"), ("3", "3")] self.init(type_=sa.Enum(*choices), info={"choices": custom_choices}) form = self.form_class() assert form.test_column.choices == custom_choices def test_column_with_choices_converts_to_select_field(self): choices = [("1", "1"), ("2", "2")] self.init(type_=sa.Integer, info={"choices": choices}) self.assert_type("test_column", SelectField) form = self.form_class() assert form.test_column.choices == choices def test_assigns_email_validator_for_email_type(self): self.init(type_=EmailType) self.assert_has_validator("test_column", Email) def test_assigns_url_validator_for_url_type(self): self.init(type_=URLType) self.assert_has_validator("test_column", URL) def test_time_converts_to_time_field(self): self.init(type_=sa.types.Time) self.assert_type("test_column", TimeField) def test_varchar_converts_to_text_field(self): self.init(type_=sa.types.VARCHAR) self.assert_type("test_column", StringField) def test_text_converts_to_textarea_field(self): self.init(type_=sa.types.TEXT) self.assert_type("test_column", TextAreaField) def test_char_converts_to_text_field(self): self.init(type_=sa.types.CHAR) self.assert_type("test_column", StringField) def test_real_converts_to_float_field(self): self.init(type_=sa.types.REAL) self.assert_type("test_column", FloatField) def test_json_converts_to_textarea_field(self): self.init(type_=sa.types.JSON) self.assert_type("test_column", TextAreaField) @mark.xfail("phone_number.phonenumbers is None") def test_phone_number_converts_to_phone_number_field(self): self.init(type_=PhoneNumberType) self.assert_type("test_column", PhoneNumberField) @mark.xfail("phone_number.phonenumbers is None") def test_phone_number_country_code_passed_to_field(self): self.init(type_=PhoneNumberType(region="SE")) form = self.form_class() assert form.test_column.region == "SE" @mark.xfail("phone_number.phonenumbers is None") def test_phone_number_type_has_no_length_validation(self): self.init(type_=PhoneNumberType(country_code="FI")) field = self._get_field("test_column") for validator in field.validators: assert validator.__class__ != Length @mark.parametrize(("type", "field"), ((IntRangeType, IntIntervalField),)) def test_range_type_conversion(self, type, field): self.init(type_=type) self.assert_type("test_column", field) @mark.xfail("passlib is None") def test_password_type_converts_to_password_field(self): self.init(type_=PasswordType) self.assert_type("test_column", PasswordField) @mark.xfail("arrow.arrow is None") def test_arrow_type_converts_to_datetime_field(self): self.init(type_=arrow.ArrowType) self.assert_type("test_column", DateTimeField) def test_url_type_converts_to_string_field(self): self.init(type_=URLType) self.assert_type("test_column", StringField) def test_uuid_type_converst_to_uuid_type(self): self.init(type_=UUIDType) self.assert_type("test_column", StringField) def test_color_type_converts_to_color_field(self): self.init(type_=ColorType) self.assert_type("test_column", ColorField) def test_email_type_converts_to_email_field(self): self.init(type_=EmailType) self.assert_type("test_column", EmailField) def test_country_type_converts_to_country_field(self): self.init(type_=CountryType) self.assert_type("test_column", CountryField) def test_choice_type_converts_to_select_field(self): choices = [("1", "choice 1"), ("2", "choice 2")] self.init(type_=ChoiceType(choices)) self.assert_type("test_column", SelectField) assert list(self.form_class().test_column.choices) == choices def test_choice_type_uses_custom_coerce_func(self): choices = [("1", "choice 1"), ("2", "choice 2")] self.init(type_=ChoiceType(choices)) self.assert_type("test_column", SelectField) model = self.ModelTest(test_column="2") form = self.form_class(obj=model) assert '