pax_global_header00006660000000000000000000000064140601016250014505gustar00rootroot0000000000000052 comment=e405586eef4be0474b719a994de91c154e847589 django-extra-views-0.14.0/000077500000000000000000000000001406010162500153055ustar00rootroot00000000000000django-extra-views-0.14.0/.editorconfig000066400000000000000000000004131406010162500177600ustar00rootroot00000000000000# EditorConfig: http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space quote_type = double insert_final_newline = true trim_trailing_whitespace = true max_line_length = 88 [*.json] insert_final_newline = false django-extra-views-0.14.0/.github/000077500000000000000000000000001406010162500166455ustar00rootroot00000000000000django-extra-views-0.14.0/.github/SUPPORT.md000066400000000000000000000012221406010162500203400ustar00rootroot00000000000000# A note about django-extra-views maintenance Dear fellow Pythonista: This project is currently in maintenance mode -- it was abandoned by the original author and taken into custody by me, @jonashaag. I can't spend a lot of time on the project, but will do my best to triage any issues and give guidance for those who want to contribute patches to the project. If you find any bugs in this library, **please consider submitting a pull request** that fixes your bug, even if it's trivial to fix. On a related note: If you've ever wanted to become a core member of an open-source project, this is your chance :-) Contact me at jonas %AT% lophus %DOT% org. django-extra-views-0.14.0/.gitignore000066400000000000000000000002271406010162500172760ustar00rootroot00000000000000*.pyc .project .pydevproject .coverage .DS_Store django_extra_views.egg-info/ htmlcov/ build/ docs/_build/ dist/ .idea nosetests.xml *.sqlite3 /.tox/ django-extra-views-0.14.0/.travis.yml000066400000000000000000000012271406010162500174200ustar00rootroot00000000000000sudo: false language: python python: - "3.5" - "3.6" - "3.7" - "3.8" env: - DJANGO=django21 - DJANGO=django22 - DJANGO=django30 - DJANGO=django31 - DJANGO=djangomaster matrix: exclude: - python: "3.5" env: DJANGO=django30 - python: "3.5" env: DJANGO=django31 - python: "3.5" env: DJANGO=djangomaster - python: "3.8" env: DJANGO=django21 allow_failures: - env: DJANGO=djangomaster before_install: - pip install codecov install: - pip install tox script: - TOX_TEST_PASSENV="TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH" tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO after_success: - codecov django-extra-views-0.14.0/AUTHORS.rst000066400000000000000000000007201406010162500171630ustar00rootroot00000000000000Primary Author(s): * Andrew Ingram (https://github.com/AndrewIngram) Other Contributors: * Sergey Fursov (https://github.com/GeyseR) * Pavel Zhukov (https://github.com/zeus) * Pi Delport (https://github.com/pjdelport) * jgsogo (https://github.com/jgsogo) * Krzysiek Szularz (https://github.com/szuliq) * Miguel Restrepo (https://github.com/miguelrestrepo) * Henry Ward (https://bitbucket.org/henward0) * Mark Gensler (https://github.com/sdolemelipone) django-extra-views-0.14.0/CHANGELOG.rst000066400000000000000000000115431406010162500173320ustar00rootroot00000000000000Change History ============== 0.14.0 (2021-06-08) ------------------------- Changes: ~~~~~~~~ Supported Versions: ======== ========== Python Django ======== ========== 3.5 2.1–2.2 3.6-3.7 2.1–3.1 3.8 2.2–3.1 ======== ========== - Removed support for Python 2.7. - Added support for Python 3.8 and Django 3.1. - Removed the following classes (use the class in parentheses instead): - ``BaseFormSetMixin`` (use ``BaseFormSetFactory``). - ``BaseInlineFormSetMixin`` (use ``BaseInlineFormSetFactory``). - ``InlineFormSet`` (use ``InlineFormSetFactory``). - ``BaseGenericInlineFormSetMixin`` (use ``BaseGenericInlineFormSetFactory``). - ``GenericInlineFormSet`` (use ``GenericInlineFormSetFactory``). 0.13.0 (2019-12-20) ------------------------- Changes: ~~~~~~~~ Supported Versions: ======== ========== Python Django ======== ========== 2.7 1.11 3.5 1.11–2.2 3.6-3.7 1.11–3.0 ======== ========== - Added ``SuccessMessageMixin`` and ``FormSetSuccessMessageMixin``. - ``CreateWithInlinesView`` and ``UpdateWithInlinesView`` now call ``self.form_valid`` method within ``self.forms_valid``. - Revert ``view.object`` back to it's original value from the GET request if validation fails for the inline formsets in ``CreateWithInlinesView`` and ``UpdateWithInlinesview``. - Added support for Django 3.0. 0.12.0 (2018-10-21) ------------------- Supported Versions: ======== ========== Python Django ======== ========== 2.7 1.11 3.4 1.11–2.0 3.5-3.7 1.11–2.1 ======== ========== Changes: ~~~~~~~~ - Removed setting of ``BaseInlineFormSetMixin.formset_class`` and ``GenericInlineFormSetMixin.formset_class`` so that ``formset`` can be set in ``factory_kwargs`` instead. - Removed ``ModelFormSetMixin.get_context_data`` and ``BaseInlineFormSetMixin.get_context_data`` as this code was duplicated from Django's ``MultipleObjectMixin`` and ``SingleObjectMixin`` respectively. - Renamed ``BaseFormSetMixin`` to ``BaseFormSetFactory``. - Renamed ``BaseInlineFormSetMixin`` to ``BaseInlineFormSetFactory``. - Renamed ``InlineFormSet`` to ``InlineFormSetFactory``. - Renamed ``BaseGenericInlineFormSetMixin`` to ``BaseGenericInlineFormSetFactory``. - Renamed ``GenericInlineFormSet`` to ``GenericInlineFormSetFactory``. All renamed classes will be removed in a future release. 0.11.0 (2018-04-24) ------------------- Supported Versions: ======== ========== Python Django ======== ========== 2.7 1.11 3.4–3.6 1.11–2.0 ======== ========== Backwards-incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Dropped support for Django 1.7–1.10. - Removed support for factory kwargs ``extra``, ``max_num``, ``can_order``, ``can_delete``, ``ct_field``, ``formfield_callback``, ``fk_name``, ``widgets``, ``ct_fk_field`` being set on ``BaseFormSetMixin`` and its subclasses. Use ``BaseFormSetMixin.factory_kwargs`` instead. - Removed support for formset_kwarg ``save_as_new`` being set on ``BaseInlineFormSetMixin`` and its subclasses. Use ``BaseInlineFormSetMixin.formset_kwargs`` instead. - Removed support for ``get_extra_form_kwargs``. This can be set in the dictionary key ``form_kwargs`` in ``BaseFormSetMixin.formset_kwargs`` instead. 0.10.0 (2018-02-28) ------------------ New features: - Added SuccessMessageWithInlinesMixin (#151) - Allow the formset prefix to be overridden (#154) Bug fixes: - SearchableMixin: Fix reduce() of empty sequence error (#149) - Add fields attributes (Issue #144, PR #150) - Fix Django 1.11 AttributeError: This QueryDict instance is immutable (#156) 0.9.0 (2017-03-08) ------------------ This version supports Django 1.7, 1.8, 1.9, 1.10 (latest minor versions), and Python 2.7, 3.4, 3.5 (latest minor versions). - Added Django 1.10 support - Dropped Django 1.6 support 0.8 (2016-06-14) ---------------- This version supports Django 1.6, 1.7, 1.8, 1.9 (latest minor versions), and Python 2.7, 3.4, 3.5 (latest minor versions). - Added ``widgets`` attribute setting; allow to change form widgets in the ``ModelFormSetView``. - Added Django 1.9 support. - Fixed ``get_context_data()`` usage of ``*args, **kwargs``. - Fixed silent overwriting of ``ModelForm`` fields to ``__all__``. Backwards-incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Dropped support for Django <= 1.5 and Python 3.3. - Removed the ``extra_views.multi`` module as it had neither documentation nor test coverage and was broken for some of the supported Django/Python versions. - This package no longer implicitly set ``fields = '__all__'``. If you face ``ImproperlyConfigured`` exceptions, you should have a look at the `Django 1.6 release notes`_ and set the ``fields`` or ``exclude`` attributes on your ``ModelForm`` or extra-views views. .. _Django 1.6 release notes: https://docs.djangoproject.com/en/stable/releases/1.6/#modelform-without-fields-or-exclude 0.7.1 (2015-06-15) ------------------ Beginning of this changelog. django-extra-views-0.14.0/LICENSE000066400000000000000000000020611406010162500163110ustar00rootroot00000000000000The MIT License Copyright (c) 2012 Andrew Ingram Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.django-extra-views-0.14.0/MANIFEST.in000066400000000000000000000004171406010162500170450ustar00rootroot00000000000000include LICENSE include README.rst recursive-include extra_views *.py recursive-include extra_views/contrib *.py recursive-include extra_views/tests *.py recursive-include extra_views/tests/templates *.html recursive-include extra_views/tests/templates/extra_views *.htmldjango-extra-views-0.14.0/README.rst000066400000000000000000000123551406010162500170020ustar00rootroot00000000000000|travis| |codecov| |docs-status| Django Extra Views - The missing class-based generic views for Django ======================================================================== Django-extra-views is a Django package which introduces additional class-based views in order to simplify common design patterns such as those found in the Django admin interface. Full documentation is available at `read the docs`_. .. _read the docs: https://django-extra-views.readthedocs.io/ .. |travis| image:: https://secure.travis-ci.org/AndrewIngram/django-extra-views.svg?branch=master :target: https://travis-ci.org/AndrewIngram/django-extra-views :alt: Build Status .. |codecov| image:: https://codecov.io/github/AndrewIngram/django-extra-views/coverage.svg?branch=master :target: https://codecov.io/github/AndrewIngram/django-extra-views?branch=master :alt: Coverage Status .. |docs-status| image:: https://readthedocs.org/projects/django-extra-views/badge/?version=latest :target: https://django-extra-views.readthedocs.io/ :alt: Documentation Status .. installation-start Installation ------------ Install the stable release from pypi (using pip): .. code-block:: sh pip install django-extra-views Or install the current master branch from github: .. code-block:: sh pip install -e git://github.com/AndrewIngram/django-extra-views.git#egg=django-extra-views Then add ``'extra_views'`` to your ``INSTALLED_APPS``: .. code-block:: python INSTALLED_APPS = [ ... 'extra_views', ... ] .. installation-end .. features-start Features -------- - ``FormSet`` and ``ModelFormSet`` views - The formset equivalents of ``FormView`` and ``ModelFormView``. - ``InlineFormSetView`` - Lets you edit a formset related to a model (using Django's ``inlineformset_factory``). - ``CreateWithInlinesView`` and ``UpdateWithInlinesView`` - Lets you edit a model and multiple inline formsets all in one view. - ``GenericInlineFormSetView``, the equivalent of ``InlineFormSetView`` but for ``GenericForeignKeys``. - Support for generic inlines in ``CreateWithInlinesView`` and ``UpdateWithInlinesView``. - Support for naming each inline or formset in the template context with ``NamedFormsetsMixin``. - ``SortableListMixin`` - Generic mixin for sorting functionality in your views. - ``SearchableListMixin`` - Generic mixin for search functionality in your views. - ``SuccessMessageMixin`` and ``FormSetSuccessMessageMixin`` - Generic mixins to display success messages after form submission. .. features-end Still to do ----------- Add support for pagination in ModelFormSetView and its derivatives, the goal being to be able to mimic the change_list view in Django's admin. Currently this is proving difficult because of how Django's MultipleObjectMixin handles pagination. .. quick-examples-start Quick Examples -------------- FormSetView ^^^^^^^^^^^^^^^^^^^^^^^ Define a :code:`FormSetView`, a view which creates a single formset from :code:`django.forms.formset_factory` and adds it to the context. .. code-block:: python from extra_views import FormSetView from my_forms import AddressForm class AddressFormSet(FormSetView): form_class = AddressForm template_name = 'address_formset.html' Then within ``address_formset.html``, render the formset like this: .. code-block:: html
... {{ formset }} ...
ModelFormSetView ^^^^^^^^^^^^^^^^^^^^ Define a :code:`ModelFormSetView`, a view which works as :code:`FormSetView` but instead renders a model formset using :code:`django.forms.modelformset_factory`. .. code-block:: python from extra_views import ModelFormSetView class ItemFormSetView(ModelFormSetView): model = Item fields = ['name', 'sku'] template_name = 'item_formset.html' CreateWithInlinesView or UpdateWithInlinesView ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Define :code:`CreateWithInlinesView` and :code:`UpdateWithInlinesView`, views which render a form to create/update a model instance and its related inline formsets. Each of the :code:`InlineFormSetFactory` classes use similar class definitions as the :code:`ModelFormSetView`. .. code-block:: python from extra_views import CreateWithInlinesView, UpdateWithInlinesView, InlineFormSetFactory class ItemInline(InlineFormSetFactory): model = Item fields = ['sku', 'price', 'name'] class ContactInline(InlineFormSetFactory): model = Contact fields = ['name', 'email'] class CreateOrderView(CreateWithInlinesView): model = Order inlines = [ItemInline, ContactInline] fields = ['customer', 'name'] template_name = 'order_and_items.html' class UpdateOrderView(UpdateWithInlinesView): model = Order inlines = [ItemInline, ContactInline] fields = ['customer', 'name'] template_name = 'order_and_items.html' Then within ``order_and_items.html``, render the formset like this: .. code-block:: html
... {{ form }} {% for formset in inlines %} {{ formset }} {% endfor %} ...
.. quick-examples-enddjango-extra-views-0.14.0/docs/000077500000000000000000000000001406010162500162355ustar00rootroot00000000000000django-extra-views-0.14.0/docs/Makefile000066400000000000000000000127441406010162500177050ustar00rootroot00000000000000# 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) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 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 " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @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/DjangoExtraViews.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoExtraViews.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/DjangoExtraViews" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoExtraViews" @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." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 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." django-extra-views-0.14.0/docs/conf.py000066400000000000000000000204431406010162500175370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Django Extra Views documentation build configuration file, created by # sphinx-quickstart on Sun Jan 6 03:11:50 2013. # # 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 re 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('.')) # -- 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"] # 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 = u"Django Extra Views" copyright = u"2013, Andrew Ingram" # 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. # with open("../extra_views/__init__.py", "rb") as f: # The full version, including alpha/beta/rc tags. release = str(re.search('__version__ = "(.+?)"', f.read().decode("utf-8")).group(1)) # The short X.Y version. version = release.rpartition(".")[0] # 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. on_rtd = os.environ.get("READTHEDOCS", None) == "True" if on_rtd: html_theme = "default" else: html_theme = "nature" # 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 = "DjangoExtraViewsdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "DjangoExtraViews.tex", u"Django Extra Views Documentation", u"Andrew Ingram", "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 # 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", "djangoextraviews", u"Django Extra Views Documentation", [u"Andrew Ingram"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "DjangoExtraViews", u"Django Extra Views Documentation", u"Andrew Ingram", "DjangoExtraViews", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"http://docs.python.org/": None} django-extra-views-0.14.0/docs/index.rst000066400000000000000000000012711406010162500200770ustar00rootroot00000000000000============================================== django-extra-views ============================================== Django Extra Views provides a number of additional class-based generic views to complement those provide by Django itself. These mimic some of the functionality available through the standard admin interface, including Model, Inline and Generic Formsets. .. include:: ../README.rst :start-after: features-start :end-before: features-end Table of Contents ----------------- .. toctree:: :maxdepth: 2 pages/getting-started pages/formset-views pages/formset-customization pages/list-views Reference --------- .. toctree:: :maxdepth: 1 pages/changelog django-extra-views-0.14.0/docs/make.bat000066400000000000000000000117741406010162500176540ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) 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. texinfo to make Texinfo files echo. gettext to make PO message catalogs 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\DjangoExtraViews.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoExtraViews.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" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 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 django-extra-views-0.14.0/docs/pages/000077500000000000000000000000001406010162500173345ustar00rootroot00000000000000django-extra-views-0.14.0/docs/pages/changelog.rst000066400000000000000000000000411406010162500220100ustar00rootroot00000000000000.. include:: ../../CHANGELOG.rst django-extra-views-0.14.0/docs/pages/formset-customization.rst000066400000000000000000000157231406010162500244630ustar00rootroot00000000000000Formset Customization Examples ============================== Overriding formset_kwargs and factory_kwargs at run time ------------------------------------------------------------------------- If the values in :code:`formset_kwargs` and :code:`factory_kwargs` need to be modified at run time, they can be set by overloading the :code:`get_formset_kwargs()` and :code:`get_factory_kwargs()` methods on any formset view (model, inline or generic) and the :code:`InlineFormSetFactory` classes: .. code-block:: python class AddressFormSetView(FormSetView): ... def get_formset_kwargs(self): kwargs = super(AddressFormSetView, self).get_formset_kwargs() # modify kwargs here return kwargs def get_factory_kwargs(self): kwargs = super(AddressFormSetView, self).get_factory_kwargs() # modify kwargs here return kwargs Overriding the the base formset class ------------------------------------- The :code:`formset_class` option should be used if you intend to override the formset methods of a view or a subclass of :code:`InlineFormSetFactory`. For example, imagine you'd like to add your custom :code:`clean` method for an inline formset view. Then, define a custom formset class, a subclass of Django's :code:`BaseInlineFormSet`, like this: .. code-block:: python from django.forms.models import BaseInlineFormSet class ItemInlineFormSet(BaseInlineFormSet): def clean(self): # ... # Your custom clean logic goes here Now, in your :code:`InlineFormSetView` sub-class, use your formset class via :code:`formset_class` setting, like this: .. code-block:: python from extra_views import InlineFormSetView from my_app.models import Item from my_app.forms import ItemForm class ItemInlineView(InlineFormSetView): model = Item form_class = ItemForm formset_class = ItemInlineFormSet # enables our custom inline This will enable :code:`clean` method being executed on the formset used by :code:`ItemInlineView`. Initial data for ModelFormSet and InlineFormSet ----------------------------------------------- Passing initial data into ModelFormSet and InlineFormSet works slightly differently to a regular FormSet. The data passed in from :code:`initial` will be inserted into the :code:`extra` forms of the formset. Only the data from :code:`get_queryset()` will be inserted into the initial rows: .. code-block:: python from extra_views import ModelFormSetView from my_app.models import Item class ItemFormSetView(ModelFormSetView): template_name = 'item_formset.html' model = Item factory_kwargs = {'extra': 10} initial = [{'name': 'example1'}, {'name': 'example2'}] The above will result in a formset containing a form for each instance of :code:`Item` in the database, followed by 2 forms containing the extra initial data, followed by 8 empty forms. Altenatively, initial data can be determined at run time and passed in by overloading :code:`get_initial()`: .. code-block:: python ... class ItemFormSetView(ModelFormSetView): model = Item template_name = 'item_formset.html' ... def get_initial(self): # Get a list of initial values for the formset here initial = [...] return initial Passing arguments to the form constructor ----------------------------------------- In order to change the arguments which are passed into each form within the formset, this can be done by the 'form_kwargs' argument passed in to the FormSet constructor. For example, to give every form an initial value of 'example' in the 'name' field: .. code-block:: python from extra_views import InlineFormSetFactory class ItemInline(InlineFormSetFactory): model = Item formset_kwargs = {'form_kwargs': {'initial': {'name': 'example'}}} If these need to be modified at run time, it can be done by :code:`get_formset_kwargs()`: .. code-block:: python from extra_views import InlineFormSetFactory class ItemInline(InlineFormSetFactory): model = Item def get_formset_kwargs(self): kwargs = super(ItemInline, self).get_formset_kwargs() initial = get_some_initial_values() kwargs['form_kwargs'].update({'initial': initial}) return kwargs Named formsets -------------- If you want more control over the names of your formsets (as opposed to iterating over :code:`inlines`), you can use :code:`NamedFormsetsMixin`: .. code-block:: python from extra_views import NamedFormsetsMixin class CreateOrderView(NamedFormsetsMixin, CreateWithInlinesView): model = Order inlines = [ItemInline, TagInline] inlines_names = ['Items', 'Tags'] fields = '__all__' Then use the appropriate names to render them in the html template: .. code-block:: html ... {{ Tags }} ... {{ Items }} ... Success messages ---------------- When using Django's messages framework, mixins are available to send success messages in a similar way to ``django.contrib.messages.views.SuccessMessageMixin``. Ensure that :code:`'django.contrib.messages.middleware.MessageMiddleware'` is included in the ``MIDDLEWARE`` section of `settings.py`. :code:`extra_views.SuccessMessageMixin` is for use with views with multiple inline formsets. It is used in an identical manner to Django's SuccessMessageMixin_, making :code:`form.cleaned_data` available for string interpolation using the :code:`%(field_name)s` syntax: .. _SuccessMessageMixin: https://docs.djangoproject.com/en/dev/ref/contrib/messages/#django.contrib.messages.views.SuccessMessageMixin .. code-block:: python from extra_views import CreateWithInlinesView, SuccessMessageMixin ... class CreateOrderView(SuccessMessageMixin, CreateWithInlinesView): model = Order inlines = [ItemInline, ContactInline] success_message = 'Order %(name)s successfully created!' ... # or instead, set at runtime: def get_success_message(self, cleaned_data, inlines): return 'Order with id {} successfully created'.format(self.object.pk) Note that the success message mixins should be placed ahead of the main view in order of class inheritance. :code:`extra_views.FormSetSuccessMessageMixin` is for use with views which handle a single formset. In order to parse any data from the formset, you should override the :code:`get_success_message` method as below: .. code-block:: python from extra_views import FormSetView, FormSetSuccessMessageMixin from my_app.forms import AddressForm class AddressFormSetView(FormSetView): form_class = AddressForm success_url = 'success/' ... success_message = 'Addresses Updated!' # or instead, set at runtime def get_success_message(self, formset) # Here you can use the formset in the message if required return '{} addresses were updated.'.format(len(formset.forms)) django-extra-views-0.14.0/docs/pages/formset-views.rst000066400000000000000000000215531406010162500227060ustar00rootroot00000000000000Formset Views ============= For all of these views we've tried to mimic the API of Django's existing class-based views as closely as possible, so they should feel natural to anyone who's already familiar with Django's views. FormSetView ----------- This is the formset equivalent of Django's FormView. Use it when you want to display a single (non-model) formset on a page. A simple formset: .. code-block:: python from extra_views import FormSetView from my_app.forms import AddressForm class AddressFormSetView(FormSetView): template_name = 'address_formset.html' form_class = AddressForm success_url = 'success/' def get_initial(self): # return whatever you'd normally use as the initial data for your formset. return data def formset_valid(self, formset): # do whatever you'd like to do with the valid formset return super(AddressFormSetView, self).formset_valid(formset) and in ``address_formset.html``: .. code-block:: html
... {{ formset }} ...
This view will render the template ``address_formset.html`` with a context variable :code:`formset` representing the :code:`AddressFormSet`. Once POSTed and successfully validated, :code:`formset_valid` will be called (which is where your handling logic goes), then the view will redirect to :code:`success_url`. Formset constructor and factory kwargs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FormSetView exposes all the parameters you'd normally be able to pass to the :code:`django.forms.BaseFormSet` constructor and :code:`django.forms.formset_factory()`. This can be done by setting the respective attribute on the class, or :code:`formset_kwargs` and :code:`factory_kwargs` at the class level. Below is an exhaustive list of all formset-related attributes which can be set at the class level for :code:`FormSetView`: .. code-block:: python ... from my_app.forms import AddressForm, BaseAddressFormSet class AddressFormSetView(FormSetView): template_name = 'address_formset.html' form_class = AddressForm formset_class = BaseAddressFormSet initial = [{'type': 'home'}, {'type': 'work'}] prefix = 'address-form' success_url = 'success/' factory_kwargs = {'extra': 2, 'max_num': None, 'can_order': False, 'can_delete': False} formset_kwargs = {'auto_id': 'my_id_%s'} In the above example, BaseAddressFormSet would be a subclass of :code:`django.forms.BaseFormSet`. ModelFormSetView ---------------- ModelFormSetView makes use of :code:`django.forms.modelformset_factory()`, using the declarative syntax used in :code:`FormSetView` as well as Django's own class-based views. So as you'd expect, the simplest usage is as follows: .. code-block:: python from extra_views import ModelFormSetView from my_app.models import Item class ItemFormSetView(ModelFormSetView): model = Item fields = ['name', 'sku', 'price'] template_name = 'item_formset.html' Rather than setting :code:`fields`, :code:`exclude` can be defined at the class level as a list of fields to be excluded. It is not necessary to define :code:`fields` or :code:`exclude` if a :code:`form_class` is defined at the class level: .. code-block:: python ... from django.forms import ModelForm class ItemForm(ModelForm): # Custom form definition goes here fields = ['name', 'sku', 'price'] class ItemFormSetView(ModelFormSetView): model = Item form_class = ItemForm template_name = 'item_formset.html' Like :code:`FormSetView`, the :code:`formset` variable is made available in the template context. By default this will populate the formset with all the instances of :code:`Item` in the database. You can control this by overriding :code:`get_queryset` on the class, which could filter on a URL kwarg (:code:`self.kwargs`), for example: .. code-block:: python class ItemFormSetView(ModelFormSetView): model = Item template_name = 'item_formset.html' def get_queryset(self): sku = self.kwargs['sku'] return super(ItemFormSetView, self).get_queryset().filter(sku=sku) InlineFormSetView ----------------- When you want to edit instances of a particular model related to a parent model (using a ForeignKey), you'll want to use InlineFormSetView. An example use case would be editing addresses associated with a particular contact. .. code-block:: python from extra_views import InlineFormSetView class EditContactAddresses(InlineFormSetView): model = Contact inline_model = Address ... Aside from the use of :code:`model` and :code:`inline_model`, :code:`InlineFormSetView` works more-or-less in the same way as :code:`ModelFormSetView`, instead calling :code:`django.forms.inlineformset_factory()`. CreateWithInlinesView and UpdateWithInlinesView ----------------------------------------------- These are the most powerful views in the library, they are effectively replacements for Django's own :code:`CreateView` and :code:`UpdateView`. The key difference is that they let you include any number of inline formsets (as well as the parent model's form). This provides functionality much like the Django Admin change forms. The API should be fairly familiar as well. The list of the inlines will be passed to the template as context variable `inlines`. Here is a simple example that demonstrates the use of each view with normal inline relationships: .. code-block:: python from extra_views import CreateWithInlinesView, UpdateWithInlinesView, InlineFormSetFactory class ItemInline(InlineFormSetFactory): model = Item fields = ['sku', 'price', 'name'] class ContactInline(InlineFormSetFactory): model = Contact fields = ['name', 'email'] class CreateOrderView(CreateWithInlinesView): model = Order inlines = [ItemInline, ContactInline] fields = ['customer', 'name'] template_name = 'order_and_items.html' def get_success_url(self): return self.object.get_absolute_url() class UpdateOrderView(UpdateWithInlinesView): model = Order inlines = [ItemInline, ContactInline] fields = ['customer', 'name'] template_name = 'order_and_items.html' def get_success_url(self): return self.object.get_absolute_url() and in the html template: .. code-block:: html
... {{ form }} {% for formset in inlines %} {{ formset }} {% endfor %} ...
InlineFormSetFactory ^^^^^^^^^^^^^^^^^^^^ This class represents all the configuration necessary to generate an inline formset from :code:`django.inlineformset_factory()`. Each class within in :code:`CreateWithInlines.inlines` and :code:`UpdateWithInlines.inlines` should be a subclass of :code:`InlineFormSetFactory`. All the same methods and attributes as :code:`InlineFormSetView` are available, with the exception of any view-related attributes and methods, such as :code:`success_url` or :code:`formset_valid()`: .. code-block:: python from my_app.forms import ItemForm, BaseItemFormSet from extra_views import InlineFormSetFactory class ItemInline(InlineFormSetFactory): model = Item form_class = ItemForm formset_class = BaseItemFormSet initial = [{'name': 'example1'}, {'name', 'example2'}] prefix = 'item-form' factory_kwargs = {'extra': 2, 'max_num': None, 'can_order': False, 'can_delete': False} formset_kwargs = {'auto_id': 'my_id_%s'} **IMPORTANT**: Note that when using :code:`InlineFormSetFactory`, :code:`model` should be the *inline* model and **not** the parent model. GenericInlineFormSetView ------------------------ In the specific case when you would usually use Django's :code:`django.contrib.contenttypes.forms.generic_inlineformset_factory()`, you should use :code:`GenericInlineFormSetView`. The kwargs :code:`ct_field` and :code:`fk_field` should be set in :code:`factory_kwargs` if they need to be changed from their default values: .. code-block:: python from extra_views.generic import GenericInlineFormSetView class EditOrderTags(GenericInlineFormSetView): model = Order inline_model = Tag factory_kwargs = {'ct_field': 'content_type', 'fk_field': 'object_id', 'max_num': 1} formset_kwargs = {'save_as_new': True} ... There is a :code:`GenericInlineFormSetFactory` which is analogous to :code:`InlineFormSetFactory` for use with generic inline formsets. :code:`GenericInlineFormSetFactory` can be used in :code:`CreateWithInlines.inlines` and :code:`UpdateWithInlines.inlines` in the obvious way. django-extra-views-0.14.0/docs/pages/getting-started.rst000066400000000000000000000001431406010162500231710ustar00rootroot00000000000000Getting Started =============== .. include:: ./installation.rst .. include:: ./quick-examples.rstdjango-extra-views-0.14.0/docs/pages/installation.rst000066400000000000000000000001431406010162500225650ustar00rootroot00000000000000.. include:: ../../README.rst :start-after: installation-start :end-before: installation-end django-extra-views-0.14.0/docs/pages/list-views.rst000066400000000000000000000034441406010162500222010ustar00rootroot00000000000000List Views ========== Searchable List Views --------------------- You can add search functionality to your ListViews by adding SearchableListMixin and by setting search_fields: .. code-block:: python from django.views.generic import ListView from extra_views import SearchableListMixin class SearchableItemListView(SearchableListMixin, ListView): template_name = 'extra_views/item_list.html' search_fields = ['name', 'sku'] model = Item In this case ``object_list`` will be filtered if the 'q' query string is provided (like /searchable/?q=query), or you can manually override ``get_search_query`` method, to define your own search functionality. Also you can define some items in ``search_fields`` as tuple (e.g. ``[('name', 'iexact', ), 'sku']``) to provide custom lookups for searching. Default lookup is ``icontains``. We strongly recommend to use only string lookups, when number fields will convert to strings before comparison to prevent converting errors. This controlled by ``check_lookups`` setting of SearchableMixin. Sortable List View ------------------ .. code-block:: python from django.views.generic import ListView from extra_views import SortableListMixin class SortableItemListView(SortableListMixin, ListView): sort_fields_aliases = [('name', 'by_name'), ('id', 'by_id'), ] model = Item You can hide real field names in query string by define sort_fields_aliases attribute (see example) or show they as is by define sort_fields. SortableListMixin adds ``sort_helper`` variable of SortHelper class, then in template you can use helper functions: ``{{ sort_helper.get_sort_query_by_FOO }}``, ``{{ sort_helper.get_sort_query_by_FOO_asc }}``, ``{{ sort_helper.get_sort_query_by_FOO_desc }}`` and ``{{ sort_helper.is_sorted_by_FOO }}`` django-extra-views-0.14.0/docs/pages/quick-examples.rst000066400000000000000000000001461406010162500230170ustar00rootroot00000000000000.. include:: ../../README.rst :start-after: quick-examples-start :end-before: quick-examples-enddjango-extra-views-0.14.0/extra_views/000077500000000000000000000000001406010162500176455ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views/__init__.py000066400000000000000000000014141406010162500217560ustar00rootroot00000000000000from extra_views.advanced import ( CreateWithInlinesView, FormSetSuccessMessageMixin, InlineFormSetFactory, NamedFormsetsMixin, SuccessMessageMixin, UpdateWithInlinesView, ) from extra_views.contrib.mixins import SearchableListMixin, SortableListMixin from extra_views.dates import CalendarMonthView from extra_views.formsets import ( FormSetView, InlineFormSetView, ModelFormSetView, ) __version__ = "0.14.0" __all__ = [ "CreateWithInlinesView", "FormSetSuccessMessageMixin", "InlineFormSetFactory", "NamedFormsetsMixin", "SuccessMessageMixin", "UpdateWithInlinesView", "SearchableListMixin", "SortableListMixin", "CalendarMonthView", "FormSetView", "InlineFormSetView", "ModelFormSetView", ] django-extra-views-0.14.0/extra_views/advanced.py000066400000000000000000000200001406010162500217540ustar00rootroot00000000000000from django.contrib import messages from django.forms.formsets import all_valid from django.views.generic.base import ContextMixin from django.views.generic.detail import SingleObjectTemplateResponseMixin from django.views.generic.edit import FormView, ModelFormMixin from extra_views.formsets import BaseInlineFormSetFactory class InlineFormSetFactory(BaseInlineFormSetFactory): """ Class used to create an `InlineFormSet` from `inlineformset_factory` as one of multiple `InlineFormSet`s within a single view. Subclasses `BaseInlineFormSetFactory` and passes in the necessary view arguments. """ def __init__(self, parent_model, request, instance, view_kwargs=None, view=None): self.inline_model = self.model self.model = parent_model self.request = request self.object = instance self.kwargs = view_kwargs self.view = view def construct_formset(self): """ Overrides construct_formset to attach the model class as an attribute of the returned formset instance. """ formset = super().construct_formset() formset.model = self.inline_model return formset class ModelFormWithInlinesMixin(ModelFormMixin): """ A mixin that provides a way to show and handle a modelform and inline formsets in a request. The inlines should be subclasses of `InlineFormSetFactory`. """ inlines = [] def get_inlines(self): """ Returns the inline formset classes """ return self.inlines def forms_valid(self, form, inlines): """ If the form and formsets are valid, save the associated models. """ response = self.form_valid(form) for formset in inlines: formset.save() return response def forms_invalid(self, form, inlines): """ If the form or formsets are invalid, re-render the context data with the data-filled form and formsets and errors. """ return self.render_to_response( self.get_context_data(form=form, inlines=inlines) ) def construct_inlines(self): """ Returns the inline formset instances """ inline_formsets = [] for inline_class in self.get_inlines(): inline_instance = inline_class( self.model, self.request, self.object, self.kwargs, self ) inline_formset = inline_instance.construct_formset() inline_formsets.append(inline_formset) return inline_formsets class ProcessFormWithInlinesView(FormView): """ A mixin that renders a form and inline formsets on GET and processes it on POST. """ def get(self, request, *args, **kwargs): """ Handles GET requests and instantiates a blank version of the form and formsets. """ form_class = self.get_form_class() form = self.get_form(form_class) inlines = self.construct_inlines() return self.render_to_response( self.get_context_data(form=form, inlines=inlines, **kwargs) ) def post(self, request, *args, **kwargs): """ Handles POST requests, instantiating a form and formset instances with the passed POST variables and then checked for validity. """ form_class = self.get_form_class() form = self.get_form(form_class) initial_object = self.object if form.is_valid(): self.object = form.save(commit=False) form_validated = True else: form_validated = False inlines = self.construct_inlines() if all_valid(inlines) and form_validated: return self.forms_valid(form, inlines) self.object = initial_object return self.forms_invalid(form, inlines) # PUT is a valid HTTP verb for creating (with a known URL) or editing an # object, note that browsers only support POST for now. def put(self, *args, **kwargs): return self.post(*args, **kwargs) class BaseCreateWithInlinesView(ModelFormWithInlinesMixin, ProcessFormWithInlinesView): """ Base view for creating an new object instance with related model instances. Using this base class requires subclassing to provide a response mixin. """ def get(self, request, *args, **kwargs): self.object = None return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = None return super().post(request, *args, **kwargs) class CreateWithInlinesView( SingleObjectTemplateResponseMixin, BaseCreateWithInlinesView ): """ View for creating a new object instance with related model instances, with a response rendered by template. """ template_name_suffix = "_form" class BaseUpdateWithInlinesView(ModelFormWithInlinesMixin, ProcessFormWithInlinesView): """ Base view for updating an existing object with related model instances. Using this base class requires subclassing to provide a response mixin. """ def get(self, request, *args, **kwargs): self.object = self.get_object() return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() return super().post(request, *args, **kwargs) class UpdateWithInlinesView( SingleObjectTemplateResponseMixin, BaseUpdateWithInlinesView ): """ View for updating an object with related model instances, with a response rendered by template. """ template_name_suffix = "_form" class NamedFormsetsMixin(ContextMixin): """ A mixin for use with `CreateWithInlinesView` or `UpdateWithInlinesView` that lets you define the context variable for each inline. """ inlines_names = [] def get_inlines_names(self): """ Returns a list of names of context variables for each inline in `inlines`. """ return self.inlines_names def get_context_data(self, **kwargs): """ If `inlines_names` has been defined, add each formset to the context under its corresponding entry in `inlines_names` """ context = {} inlines_names = self.get_inlines_names() if inlines_names: # We have formset or inlines in context, but never both context.update(zip(inlines_names, kwargs.get("inlines", []))) if "formset" in kwargs: context[inlines_names[0]] = kwargs["formset"] context.update(kwargs) return super().get_context_data(**context) class SuccessMessageMixin(object): """ Adds success message on views with inlines if django.contrib.messages framework is used. In order to use just add mixin in to inheritance before main class, e.g.: class MyCreateWithInlinesView (SuccessMessageMixin, CreateWithInlinesView): success_message='Something was created!' """ success_message = "" def forms_valid(self, form, inlines): response = super().forms_valid(form, inlines) success_message = self.get_success_message(form.cleaned_data, inlines) if success_message: messages.success(self.request, success_message) return response def get_success_message(self, cleaned_data, inlines): return self.success_message % cleaned_data class FormSetSuccessMessageMixin(object): """ Adds success message on FormSet views if django.contrib.messages framework is used. In order to use just add mixin in to inheritance before main class, e.g.: class MyFormSetView (FormSetSuccessMessageMixin, ModelFormSetView): success_message='Something was created!' """ success_message = "" def formset_valid(self, formset): response = super().formset_valid(formset) success_message = self.get_success_message(formset) if success_message: messages.success(self.request, success_message) return response def get_success_message(self, formset): return self.success_message django-extra-views-0.14.0/extra_views/contrib/000077500000000000000000000000001406010162500213055ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views/contrib/__init__.py000066400000000000000000000000001406010162500234040ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views/contrib/mixins.py000066400000000000000000000205061406010162500231710ustar00rootroot00000000000000import datetime import functools import operator from django.contrib import messages from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.views.generic.base import ContextMixin VALID_STRING_LOOKUPS = ( "iexact", "contains", "icontains", "startswith", "istartswith", "endswith", "iendswith", "search", "regex", "iregex", ) class SearchableListMixin(object): """ Filter queryset like a django admin search_fields does, but with little more intelligence: if self.search_split is set to True (by default) it will split query to words (by whitespace) Also tries to convert each word to date with self.search_date_formats and then search each word in separate field e.g. with query 'foo bar' you can find object with obj.field1__icontains='foo' and obj.field2__icontains=='bar' To provide custom lookup just set one of the search_fields to tuple, e.g. search_fields = [('field1', 'iexact'), 'field2', ('field3', 'startswith')] This class is designed to be used with django.generic.ListView You could specify query by overriding get_search_query method by default this method will try to get 'q' key from request.GET (this can be disabled with search_use_q=False) """ search_fields = ["id"] search_date_fields = None search_date_formats = ["%d.%m.%y", "%d.%m.%Y"] search_split = True search_use_q = True check_lookups = True def get_words(self, query): if self.search_split: return query.split() return [query] def get_search_fields_with_filters(self): fields = [] for sf in self.search_fields: if isinstance(sf, str): fields.append((sf, "icontains")) else: if self.check_lookups and sf[1] not in VALID_STRING_LOOKUPS: raise ValueError("Invalid string lookup - %s" % sf[1]) fields.append(sf) return fields def try_convert_to_date(self, word): """ Tries to convert word to date(datetime) using search_date_formats Return None if word fits no one format """ for frm in self.search_date_formats: try: return datetime.datetime.strptime(word, frm).date() except ValueError: pass return None def get_search_query(self): """ Get query from request.GET 'q' parameter when search_use_q is set to True Override this method to provide your own query to search """ return self.search_use_q and self.request.GET.get("q", "").strip() def get_queryset(self): qs = super(SearchableListMixin, self).get_queryset() query = self.get_search_query() if query: w_qs = [] search_pairs = self.get_search_fields_with_filters() for word in self.get_words(query): filters = [ Q(**{"%s__%s" % (pair[0], pair[1]): word}) for pair in search_pairs ] if self.search_date_fields: dt = self.try_convert_to_date(word) if dt: filters.extend( [ Q(**{field_name: dt}) for field_name in self.search_date_fields ] ) w_qs.append(functools.reduce(operator.or_, filters)) qs = qs.filter(functools.reduce(operator.and_, w_qs)) qs = qs.distinct() return qs class SortHelper(object): def __init__( self, request, sort_fields_aliases, sort_param_name, sort_type_param_name ): # Create a list from sort_fields_aliases, in case it is a generator, # since we want to iterate through it multiple times. sort_fields_aliases = list(sort_fields_aliases) self.initial_params = request.GET.copy() self.sort_fields = dict(sort_fields_aliases) self.inv_sort_fields = dict((v, k) for k, v in sort_fields_aliases) self.initial_sort = self.inv_sort_fields.get( self.initial_params.get(sort_param_name), None ) self.initial_sort_type = self.initial_params.get(sort_type_param_name, "asc") self.sort_param_name = sort_param_name self.sort_type_param_name = sort_type_param_name for field, alias in self.sort_fields.items(): setattr( self, "get_sort_query_by_%s" % alias, functools.partial(self.get_params_for_field, field), ) setattr( self, "get_sort_query_by_%s_asc" % alias, functools.partial(self.get_params_for_field, field, "asc"), ) setattr( self, "get_sort_query_by_%s_desc" % alias, functools.partial(self.get_params_for_field, field, "desc"), ) setattr( self, "is_sorted_by_%s" % alias, functools.partial(self.is_sorted_by, field), ) def is_sorted_by(self, field_name): return field_name == self.initial_sort and self.initial_sort_type or False def get_params_for_field(self, field_name, sort_type=None): """ If sort_type is None - inverse current sort for field, if no sorted - use asc """ if not sort_type: if self.initial_sort == field_name: sort_type = "desc" if self.initial_sort_type == "asc" else "asc" else: sort_type = "asc" self.initial_params[self.sort_param_name] = self.sort_fields[field_name] self.initial_params[self.sort_type_param_name] = sort_type return "?%s" % self.initial_params.urlencode() def get_sort(self): if not self.initial_sort: return None sort = "%s" % self.initial_sort if self.initial_sort_type == "desc": sort = "-%s" % sort return sort class SortableListMixin(ContextMixin): """ You can provide either sort_fields as a plain list like ['id', 'some', 'foo__bar', ...] or, if you want to hide original field names you can provide list of tuples with alias that will be used: [('id', 'by_id'), ('some', 'show_this'), ('foo__bar', 'bar')] If sort_param_name exists in query but sort_type_param_name is omitted queryset will be sorted as 'asc' """ sort_fields = [] sort_fields_aliases = [] sort_param_name = "o" sort_type_param_name = "ot" def get_sort_fields(self): if self.sort_fields: return zip(self.sort_fields, self.sort_fields) return self.sort_fields_aliases def get_sort_helper(self): return SortHelper( self.request, self.get_sort_fields(), self.sort_param_name, self.sort_type_param_name, ) def _sort_queryset(self, queryset): self.sort_helper = self.get_sort_helper() sort = self.sort_helper.get_sort() if sort: queryset = queryset.order_by(sort) return queryset def get_queryset(self): qs = super(SortableListMixin, self).get_queryset() if self.sort_fields and self.sort_fields_aliases: raise ImproperlyConfigured( "You should provide sort_fields or sort_fields_aliaces but not both" ) return self._sort_queryset(qs) def get_context_data(self, **kwargs): context = {} if hasattr(self, "sort_helper"): context["sort_helper"] = self.sort_helper context.update(kwargs) return super(SortableListMixin, self).get_context_data(**context) class SuccessMessageWithInlinesMixin(object): """ Adds a success message on successful form submission. """ success_message = "" def forms_valid(self, form, inlines): response = super(SuccessMessageWithInlinesMixin, self).forms_valid( form, inlines ) success_message = self.get_success_message(form.cleaned_data) if success_message: messages.success(self.request, success_message) return response def get_success_message(self, cleaned_data): return self.success_message % cleaned_data django-extra-views-0.14.0/extra_views/dates.py000066400000000000000000000215431406010162500213240ustar00rootroot00000000000000import datetime import math from calendar import Calendar from collections import defaultdict from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.utils.translation import gettext_lazy as _ from django.views.generic.dates import ( DateMixin, MonthMixin, YearMixin, _date_from_string, ) from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin DAYS = ( _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"), _("Friday"), _("Saturday"), _("Sunday"), ) def daterange(start_date, end_date): """ Returns an iterator of dates between two provided ones """ for n in range(int((end_date - start_date).days + 1)): yield start_date + datetime.timedelta(n) class BaseCalendarMonthView(DateMixin, YearMixin, MonthMixin, BaseListView): """ A base view for displaying a calendar month """ first_of_week = 0 # 0 = Monday, 6 = Sunday paginate_by = None # We don't want to use this part of MultipleObjectMixin date_field = None end_date_field = None # For supporting events with duration def get_paginate_by(self, queryset): if self.paginate_by is not None: raise ImproperlyConfigured( "'%s' cannot be paginated, it is a calendar view" % self.__class__.__name__ ) return None def get_allow_future(self): return True def get_end_date_field(self): """ Returns the model field to use for end dates """ return self.end_date_field def get_start_date(self, obj): """ Returns the start date for a model instance """ obj_date = getattr(obj, self.get_date_field()) try: obj_date = obj_date.date() except AttributeError: # It's a date rather than datetime, so we use it as is pass return obj_date def get_end_date(self, obj): """ Returns the end date for a model instance """ obj_date = getattr(obj, self.get_end_date_field()) try: obj_date = obj_date.date() except AttributeError: # It's a date rather than datetime, so we use it as is pass return obj_date def get_first_of_week(self): """ Returns an integer representing the first day of the week. 0 represents Monday, 6 represents Sunday. """ if self.first_of_week is None: raise ImproperlyConfigured( "%s.first_of_week is required." % self.__class__.__name__ ) if self.first_of_week not in range(7): raise ImproperlyConfigured( "%s.first_of_week must be an integer between 0 and 6." % self.__class__.__name__ ) return self.first_of_week def get_queryset(self): """ Returns a queryset of models for the month requested """ qs = super().get_queryset() year = self.get_year() month = self.get_month() date_field = self.get_date_field() end_date_field = self.get_end_date_field() date = _date_from_string( year, self.get_year_format(), month, self.get_month_format() ) since = date until = self.get_next_month(date) # Adjust our start and end dates to allow for next and previous # month edges if since.weekday() != self.get_first_of_week(): diff = math.fabs(since.weekday() - self.get_first_of_week()) since = since - datetime.timedelta(days=diff) if until.weekday() != ((self.get_first_of_week() + 6) % 7): diff = math.fabs(((self.get_first_of_week() + 6) % 7) - until.weekday()) until = until + datetime.timedelta(days=diff) if end_date_field: # 5 possible conditions for showing an event: # 1) Single day event, starts after 'since' # 2) Multi-day event, starts after 'since' and ends before 'until' # 3) Starts before 'since' and ends after 'since' and before 'until' # 4) Starts after 'since' but before 'until' and ends after 'until' # 5) Starts before 'since' and ends after 'until' predicate1 = Q(**{"%s__gte" % date_field: since, end_date_field: None}) predicate2 = Q( **{"%s__gte" % date_field: since, "%s__lt" % end_date_field: until} ) predicate3 = Q( **{ "%s__lt" % date_field: since, "%s__gte" % end_date_field: since, "%s__lt" % end_date_field: until, } ) predicate4 = Q( **{ "%s__gte" % date_field: since, "%s__lt" % date_field: until, "%s__gte" % end_date_field: until, } ) predicate5 = Q( **{"%s__lt" % date_field: since, "%s__gte" % end_date_field: until} ) return qs.filter( predicate1 | predicate2 | predicate3 | predicate4 | predicate5 ) return qs.filter(**{"%s__gte" % date_field: since}) def get_context_data(self, **kwargs): """ Injects variables necessary for rendering the calendar into the context. Variables added are: `calendar`, `weekdays`, `month`, `next_month` and `previous_month`. """ data = super().get_context_data(**kwargs) year = self.get_year() month = self.get_month() date = _date_from_string( year, self.get_year_format(), month, self.get_month_format() ) cal = Calendar(self.get_first_of_week()) month_calendar = [] now = datetime.datetime.utcnow() date_lists = defaultdict(list) multidate_objs = [] for obj in data["object_list"]: obj_date = self.get_start_date(obj) end_date_field = self.get_end_date_field() if end_date_field: end_date = self.get_end_date(obj) if end_date and end_date != obj_date: multidate_objs.append( { "obj": obj, "range": [x for x in daterange(obj_date, end_date)], } ) continue # We don't put multi-day events in date_lists date_lists[obj_date].append(obj) for week in cal.monthdatescalendar(date.year, date.month): week_range = set(daterange(week[0], week[6])) week_events = [] for val in multidate_objs: intersect_length = len(week_range.intersection(val["range"])) if intersect_length: # Event happens during this week slot = 1 width = ( intersect_length ) # How many days is the event during this week? nowrap_previous = ( True ) # Does the event continue from the previous week? nowrap_next = True # Does the event continue to the next week? if val["range"][0] >= week[0]: slot = 1 + (val["range"][0] - week[0]).days else: nowrap_previous = False if val["range"][-1] > week[6]: nowrap_next = False week_events.append( { "event": val["obj"], "slot": slot, "width": width, "nowrap_previous": nowrap_previous, "nowrap_next": nowrap_next, } ) week_calendar = {"events": week_events, "date_list": []} for day in week: week_calendar["date_list"].append( { "day": day, "events": date_lists[day], "today": day == now.date(), "is_current_month": day.month == date.month, } ) month_calendar.append(week_calendar) data["calendar"] = month_calendar data["weekdays"] = [DAYS[x] for x in cal.iterweekdays()] data["month"] = date data["next_month"] = self.get_next_month(date) data["previous_month"] = self.get_previous_month(date) return data class CalendarMonthView(MultipleObjectTemplateResponseMixin, BaseCalendarMonthView): """ A view for displaying a calendar month, and rendering a template response """ template_name_suffix = "_calendar_month" django-extra-views-0.14.0/extra_views/formsets.py000066400000000000000000000212061406010162500220620ustar00rootroot00000000000000from django.forms.formsets import formset_factory from django.forms.models import inlineformset_factory, modelformset_factory from django.http import HttpResponseRedirect from django.views.generic.base import ContextMixin, TemplateResponseMixin, View from django.views.generic.detail import ( SingleObjectMixin, SingleObjectTemplateResponseMixin, ) from django.views.generic.list import ( MultipleObjectMixin, MultipleObjectTemplateResponseMixin, ) class BaseFormSetFactory(object): """ Base class for constructing a FormSet from `formset_factory` in a view. Calling `construct_formset` calls all other methods. """ initial = [] form_class = None formset_class = None prefix = None formset_kwargs = {} factory_kwargs = {} def construct_formset(self): """ Returns an instance of the formset """ formset_class = self.get_formset() return formset_class(**self.get_formset_kwargs()) def get_initial(self): """ Returns a copy of the initial data to use for formsets on this view. """ return self.initial[:] def get_prefix(self): """ Returns the prefix used for formsets on this view. """ return self.prefix def get_formset_class(self): """ Returns the formset class to use in the formset factory """ return self.formset_class def get_form_class(self): """ Returns the form class to use with the formset in this view """ return self.form_class def get_formset(self): """ Returns the formset class from the formset factory """ return formset_factory(self.get_form_class(), **self.get_factory_kwargs()) def get_formset_kwargs(self): """ Returns the keyword arguments for instantiating the formset. """ kwargs = self.formset_kwargs.copy() kwargs.update({"initial": self.get_initial(), "prefix": self.get_prefix()}) if self.request.method in ("POST", "PUT"): kwargs.update( {"data": self.request.POST.copy(), "files": self.request.FILES} ) return kwargs def get_factory_kwargs(self): """ Returns the keyword arguments for calling the formset factory """ kwargs = self.factory_kwargs.copy() if self.get_formset_class(): kwargs["formset"] = self.get_formset_class() return kwargs class FormSetMixin(BaseFormSetFactory, ContextMixin): """ A view mixin that provides a way to show and handle a single formset in a request. """ success_url = None def get_success_url(self): """ Returns the supplied URL. """ if self.success_url: url = self.success_url else: # Default to returning to the same page url = self.request.get_full_path() return url def formset_valid(self, formset): """ If the formset is valid redirect to the supplied URL """ return HttpResponseRedirect(self.get_success_url()) def formset_invalid(self, formset): """ If the formset is invalid, re-render the context data with the data-filled formset and errors. """ return self.render_to_response(self.get_context_data(formset=formset)) class ModelFormSetMixin(FormSetMixin, MultipleObjectMixin): """ A view mixin that provides a way to show and handle a single model formset in a request. Uses `modelformset_factory`. """ exclude = None fields = None def get_formset_kwargs(self): """ Returns the keyword arguments for instantiating the formset. """ kwargs = super().get_formset_kwargs() kwargs["queryset"] = self.get_queryset() return kwargs def get_factory_kwargs(self): """ Returns the keyword arguments for calling the formset factory """ kwargs = super().get_factory_kwargs() kwargs.setdefault("fields", self.fields) kwargs.setdefault("exclude", self.exclude) if self.get_form_class(): kwargs["form"] = self.get_form_class() return kwargs def get_formset(self): """ Returns the formset class from the model formset factory """ return modelformset_factory(self.model, **self.get_factory_kwargs()) def formset_valid(self, formset): """ If the formset is valid, save the associated models. """ self.object_list = formset.save() return super().formset_valid(formset) class BaseInlineFormSetFactory(BaseFormSetFactory): """ Base class for constructing a FormSet from `inlineformset_factory` in a view. Calling `construct_formset` calls all other methods. """ model = None inline_model = None exclude = None fields = None def get_inline_model(self): """ Returns the inline model to use with the inline formset """ return self.inline_model def get_formset_kwargs(self): """ Returns the keyword arguments for instantiating the formset. """ kwargs = super().get_formset_kwargs() kwargs["instance"] = self.object return kwargs def get_factory_kwargs(self): """ Returns the keyword arguments for calling the formset factory """ kwargs = super().get_factory_kwargs() kwargs.setdefault("fields", self.fields) kwargs.setdefault("exclude", self.exclude) if self.get_form_class(): kwargs["form"] = self.get_form_class() return kwargs def get_formset(self): """ Returns the formset class from the inline formset factory """ return inlineformset_factory( self.model, self.get_inline_model(), **self.get_factory_kwargs() ) class InlineFormSetMixin(BaseInlineFormSetFactory, SingleObjectMixin, FormSetMixin): """ A view mixin that provides a way to show and handle a single inline formset in a request. """ def formset_valid(self, formset): self.object_list = formset.save() return super().formset_valid(formset) class ProcessFormSetView(View): """ A mixin that processes a formset on POST. """ def get(self, request, *args, **kwargs): """ Handles GET requests and instantiates a blank version of the formset. """ formset = self.construct_formset() return self.render_to_response(self.get_context_data(formset=formset)) def post(self, request, *args, **kwargs): """ Handles POST requests, instantiating a formset instance with the passed POST variables and then checked for validity. """ formset = self.construct_formset() if formset.is_valid(): return self.formset_valid(formset) else: return self.formset_invalid(formset) # PUT is a valid HTTP verb for creating (with a known URL) or editing an # object, note that browsers only support POST for now. def put(self, *args, **kwargs): return self.post(*args, **kwargs) class BaseFormSetView(FormSetMixin, ProcessFormSetView): """ A base view for displaying a formset """ class FormSetView(TemplateResponseMixin, BaseFormSetView): """ A view for displaying a formset, and rendering a template response """ class BaseModelFormSetView(ModelFormSetMixin, ProcessFormSetView): """ A base view for displaying a model formset """ def get(self, request, *args, **kwargs): self.object_list = self.get_queryset() return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object_list = self.get_queryset() return super().post(request, *args, **kwargs) class ModelFormSetView(MultipleObjectTemplateResponseMixin, BaseModelFormSetView): """ A view for displaying a model formset, and rendering a template response """ class BaseInlineFormSetView(InlineFormSetMixin, ProcessFormSetView): """ A base view for displaying an inline formset for a queryset belonging to a parent model """ def get(self, request, *args, **kwargs): self.object = self.get_object() return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() return super().post(request, *args, **kwargs) class InlineFormSetView(SingleObjectTemplateResponseMixin, BaseInlineFormSetView): """ A view for displaying an inline formset for a queryset belonging to a parent model """ django-extra-views-0.14.0/extra_views/generic.py000066400000000000000000000034731406010162500216420ustar00rootroot00000000000000from django.contrib.contenttypes.forms import generic_inlineformset_factory from extra_views.formsets import ( BaseInlineFormSetFactory, BaseInlineFormSetView, InlineFormSetMixin, InlineFormSetView, ) class BaseGenericInlineFormSetFactory(BaseInlineFormSetFactory): """ Base class for constructing a GenericInlineFormSet from `generic_inlineformset_factory` in a view. """ def get_formset(self): """ Returns the final formset class from generic_inlineformset_factory. """ result = generic_inlineformset_factory( self.inline_model, **self.get_factory_kwargs() ) return result class GenericInlineFormSetFactory(BaseGenericInlineFormSetFactory): """ Class used to create a `GenericInlineFormSet` from `generic_inlineformset_factory` as one of multiple `GenericInlineFormSet`s within a single view. Subclasses `BaseGenericInlineFormSetFactory` and passes in the necessary view arguments. """ def __init__(self, parent_model, request, instance, view_kwargs=None, view=None): self.inline_model = self.model self.model = parent_model self.request = request self.object = instance self.kwargs = view_kwargs self.view = view class GenericInlineFormSetMixin(BaseGenericInlineFormSetFactory, InlineFormSetMixin): """ A mixin that provides a way to show and handle a generic inline formset in a request. """ class BaseGenericInlineFormSetView(GenericInlineFormSetMixin, BaseInlineFormSetView): """ A base view for displaying a generic inline formset """ class GenericInlineFormSetView(BaseGenericInlineFormSetView, InlineFormSetView): """ A view for displaying a generic inline formset for a queryset belonging to a parent model """ django-extra-views-0.14.0/extra_views/models.py000066400000000000000000000000001406010162500214700ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/000077500000000000000000000000001406010162500210675ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/__init__.py000066400000000000000000000000001406010162500231660ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/forms.py000066400000000000000000000016641406010162500225760ustar00rootroot00000000000000from django import forms from .models import Item, Order class OrderForm(forms.ModelForm): class Meta: model = Order fields = ["name"] def save(self, commit=True): instance = super().save(commit=commit) if commit: instance.action_on_save = True instance.save() return instance class ItemForm(forms.ModelForm): flag = forms.BooleanField(initial=True) class Meta: model = Item fields = ["name", "sku", "price", "order", "status"] class AddressForm(forms.Form): name = forms.CharField(max_length=255, required=True) line1 = forms.CharField(max_length=255, required=False) line2 = forms.CharField(max_length=255, required=False) city = forms.CharField(max_length=255, required=False) postcode = forms.CharField(max_length=10, required=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) django-extra-views-0.14.0/extra_views_tests/formsets.py000066400000000000000000000012501406010162500233010ustar00rootroot00000000000000from django import forms from django.forms.formsets import BaseFormSet from django.forms.models import BaseModelFormSet COUNTRY_CHOICES = ( ("gb", "Great Britain"), ("us", "United States"), ("ca", "Canada"), ("au", "Australia"), ("nz", "New Zealand"), ) class AddressFormSet(BaseFormSet): def add_fields(self, form, index): super().add_fields(form, index) form.fields["county"] = forms.ChoiceField(choices=COUNTRY_CHOICES, initial="gb") class BaseArticleFormSet(BaseModelFormSet): def add_fields(self, form, index): super().add_fields(form, index) form.fields["notes"] = forms.CharField(initial="Write notes here") django-extra-views-0.14.0/extra_views_tests/migrations/000077500000000000000000000000001406010162500232435ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/migrations/0001_initial.py000066400000000000000000000113611406010162500257100ustar00rootroot00000000000000# Generated by Django 2.1.13 on 2019-10-08 04:42 import django.db.models.deletion import django.utils.timezone from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [("contenttypes", "0002_remove_content_type_name")] operations = [ migrations.CreateModel( name="Contact", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=255)), ("email", models.CharField(max_length=255)), ], ), migrations.CreateModel( name="Event", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=255)), ("date", models.DateField()), ], ), migrations.CreateModel( name="Item", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=255)), ("sku", models.CharField(max_length=13)), ( "price", models.DecimalField(db_index=True, decimal_places=2, max_digits=12), ), ( "status", models.SmallIntegerField( choices=[ (0, "Placed"), (1, "Charged"), (2, "Shipped"), (3, "Cancelled"), ], db_index=True, default=0, ), ), ( "date_placed", models.DateField( blank=True, default=django.utils.timezone.now, null=True ), ), ], ), migrations.CreateModel( name="Order", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=255)), ("customer", models.CharField(blank=True, default="", max_length=255)), ("date_created", models.DateTimeField(auto_now_add=True)), ("date_modified", models.DateTimeField(auto_now=True)), ("action_on_save", models.BooleanField(default=False)), ], ), migrations.CreateModel( name="Tag", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=255)), ("object_id", models.PositiveIntegerField(null=True)), ( "content_type", models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, to="contenttypes.ContentType", ), ), ], ), migrations.AddField( model_name="item", name="order", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="items", to="extra_views_tests.Order", ), ), migrations.AddField( model_name="contact", name="order", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="contacts", to="extra_views_tests.Order", ), ), ] django-extra-views-0.14.0/extra_views_tests/migrations/__init__.py000066400000000000000000000000001406010162500253420ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/models.py000066400000000000000000000037731406010162500227360ustar00rootroot00000000000000import datetime from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models try: from django.utils.timezone import now except ImportError: now = datetime.datetime.now STATUS_CHOICES = ((0, "Placed"), (1, "Charged"), (2, "Shipped"), (3, "Cancelled")) class Order(models.Model): name = models.CharField(max_length=255) customer = models.CharField(max_length=255, default="", blank=True) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) action_on_save = models.BooleanField(default=False) def get_absolute_url(self): return "/inlines/%i/" % self.pk def __str__(self): return self.name class Item(models.Model): name = models.CharField(max_length=255) sku = models.CharField(max_length=13) price = models.DecimalField(decimal_places=2, max_digits=12, db_index=True) order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE) status = models.SmallIntegerField(default=0, choices=STATUS_CHOICES, db_index=True) date_placed = models.DateField(default=now, null=True, blank=True) def __str__(self): return "%s (%s)" % (self.name, self.sku) class Contact(models.Model): name = models.CharField(max_length=255) email = models.CharField(max_length=255) order = models.ForeignKey(Order, related_name="contacts", on_delete=models.CASCADE) def __str__(self): return self.name class Tag(models.Model): name = models.CharField(max_length=255) content_type = models.ForeignKey(ContentType, null=True, on_delete=models.CASCADE) object_id = models.PositiveIntegerField(null=True) content_object = GenericForeignKey("content_type", "object_id") def __str__(self): return self.name class Event(models.Model): name = models.CharField(max_length=255) date = models.DateField() def __str__(self): return self.name django-extra-views-0.14.0/extra_views_tests/settings.py000066400000000000000000000017211406010162500233020ustar00rootroot00000000000000import os PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(PROJECT_DIR) DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } TEMPLATES = [ {"BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True} ] INSTALLED_APPS = [ "django.contrib.contenttypes", "django.contrib.auth", "extra_views", "extra_views_tests", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "extra_views_tests.urls" SECRET_KEY = "something not very secret" django-extra-views-0.14.0/extra_views_tests/templates/000077500000000000000000000000001406010162500230655ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/templates/404.html000066400000000000000000000000031406010162500242530ustar00rootroot00000000000000404django-extra-views-0.14.0/extra_views_tests/templates/extra_views/000077500000000000000000000000001406010162500254255ustar00rootroot00000000000000django-extra-views-0.14.0/extra_views_tests/templates/extra_views/address_formset.html000066400000000000000000000003371406010162500315020ustar00rootroot00000000000000 Address Formset

Address Formset

{{ formset }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/event_calendar_month.html000066400000000000000000000002201406010162500324640ustar00rootroot00000000000000 Event Calendar

Event Calendar

{{ object_list }} django-extra-views-0.14.0/extra_views_tests/templates/extra_views/formsets_multiview.html000066400000000000000000000004731406010162500322660ustar00rootroot00000000000000 Order and Address MultiView
{{ order_form }}
{{ items_formset }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/inline_formset.html000066400000000000000000000003351406010162500313310ustar00rootroot00000000000000 Inline Formset

Inline Formset

{{ formset }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/item_formset.html000066400000000000000000000003311406010162500310050ustar00rootroot00000000000000 Item Formset

Item Formset

{{ formset }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/item_list.html000066400000000000000000000002531406010162500303040ustar00rootroot00000000000000 Item List

Item List

{% for object in object_list %} {{ object }} {% endfor %} django-extra-views-0.14.0/extra_views_tests/templates/extra_views/order_and_items.html000066400000000000000000000004271406010162500314540ustar00rootroot00000000000000 Order and Items

Order and Items

{{ form }} {% for formset in inlines %} {{ formset }} {% endfor %}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/orderaddress_multiview.html000066400000000000000000000004721406010162500331040ustar00rootroot00000000000000 Order and Address MultiView
{{ order_form }}
{{ address_form }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/orderitems_multiview.html000066400000000000000000000004711406010162500325770ustar00rootroot00000000000000 Order and Items MultiView
{{ order_form }}
{{ items_formset }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/paged_formset.html000066400000000000000000000003311406010162500311270ustar00rootroot00000000000000 Item Formset

Item Formset

{{ formset }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/sortable_item_list.html000066400000000000000000000016351406010162500322040ustar00rootroot00000000000000 Item List

Item List

{% for object in object_list %} {% endfor %}
Name asc name desc name {% if sort_helper.is_sorted_by_name %} ordered by name {{ sort_helper.is_sorted_by_name }} {% endif %} SKU
{{ object.name }} {{ object.sku }}
django-extra-views-0.14.0/extra_views_tests/templates/extra_views/success.html000066400000000000000000000001561406010162500277650ustar00rootroot00000000000000 Success

Success

django-extra-views-0.14.0/extra_views_tests/tests.py000066400000000000000000000610331406010162500226060ustar00rootroot00000000000000import datetime from decimal import Decimal as D from unittest import expectedFailure from django.contrib.messages import get_messages from django.core.exceptions import ImproperlyConfigured from django.forms import ValidationError from django.test import TestCase from .models import Event, Item, Order, Tag class FormSetViewTests(TestCase): management_data = { "form-TOTAL_FORMS": "2", "form-INITIAL_FORMS": "0", "form-MAX_NUM_FORMS": "", } def test_create(self): res = self.client.get("/formset/simple/") self.assertEqual(res.status_code, 200) self.assertTrue("formset" in res.context) self.assertFalse("form" in res.context) self.assertTemplateUsed(res, "extra_views/address_formset.html") self.assertEqual( res.context["formset"].__class__.__name__, "AddressFormFormSet" ) def test_formset_named(self): res = self.client.get("/formset/simple/named/") self.assertEqual(res.status_code, 200) self.assertEqual(res.context["formset"], res.context["AddressFormset"]) def test_missing_management(self): with self.assertRaises(ValidationError): self.client.post("/formset/simple/", {}) def test_success(self): res = self.client.post("/formset/simple/", self.management_data, follow=True) self.assertRedirects(res, "/formset/simple/", status_code=302) def test_success_message(self): res = self.client.post("/formset/simple/", self.management_data, follow=True) messages = [ message.__str__() for message in get_messages(res.context["view"].request) ] self.assertIn("Formset objects were created successfully!", messages) @expectedFailure def test_put(self): res = self.client.put("/formset/simple/", self.management_data, follow=True) self.assertRedirects(res, "/formset/simple/", status_code=302) def test_success_url(self): res = self.client.post( "/formset/simple_redirect/", self.management_data, follow=True ) self.assertRedirects(res, "/formset/simple_redirect/valid/") def test_invalid(self): data = { "form-0-name": "Joe Bloggs", "form-0-city": "", "form-0-line1": "", "form-0-line2": "", "form-0-postcode": "", } data.update(self.management_data) res = self.client.post("/formset/simple/", data, follow=True) self.assertEqual(res.status_code, 200) self.assertTrue("postcode" in res.context["formset"].errors[0]) def test_formset_class(self): res = self.client.get("/formset/custom/") self.assertEqual(res.status_code, 200) def test_inital(self): res = self.client.get("/formset/simple/kwargs/") self.assertEqual(res.status_code, 200) initial_forms = res.context["formset"].initial_forms self.assertTrue(initial_forms) self.assertEqual(initial_forms[0].initial, {"name": "address1"}) def test_prefix(self): res = self.client.get("/formset/simple/kwargs/") self.assertEqual(res.status_code, 200) self.assertEqual(res.context["formset"].management_form.prefix, "test_prefix") def test_factory_kwargs(self): res = self.client.get("/formset/simple/kwargs/") self.assertEqual(res.status_code, 200) self.assertEqual( res.context["formset"].management_form.initial["MAX_NUM_FORMS"], 27 ) def test_formset_kwargs(self): res = self.client.get("/formset/simple/kwargs/") self.assertEqual(res.status_code, 200) self.assertEqual(res.context["formset"].management_form.auto_id, "id_test_%s") initial_forms = res.context["formset"].initial_forms self.assertTrue(initial_forms) self.assertTrue(initial_forms[0].empty_permitted) class ModelFormSetViewTests(TestCase): management_data = { "form-TOTAL_FORMS": "2", "form-INITIAL_FORMS": "0", "form-MAX_NUM_FORMS": "", } def test_create(self): res = self.client.get("/modelformset/simple/") self.assertEqual(res.status_code, 200) self.assertTrue("formset" in res.context) self.assertFalse("form" in res.context) self.assertTemplateUsed(res, "extra_views/item_formset.html") self.assertEqual(res.context["formset"].__class__.__name__, "ItemFormFormSet") def test_override(self): res = self.client.get("/modelformset/custom/") self.assertEqual(res.status_code, 200) form = res.context["formset"].forms[0] self.assertEqual(form["flag"].value(), True) self.assertEqual(form["notes"].value(), "Write notes here") def test_post(self): order = Order(name="Dummy Order") order.save() data = { "form-0-name": "Bubble Bath", "form-0-sku": "1234567890123", "form-0-price": D("9.99"), "form-0-order": order.id, "form-0-status": 0, } data.update(self.management_data) data["form-TOTAL_FORMS"] = "1" res = self.client.post("/modelformset/simple/", data, follow=True) self.assertEqual(res.status_code, 200) self.assertEqual(Item.objects.all().count(), 1) def test_context(self): order = Order(name="Dummy Order") order.save() for i in range(10): item = Item( name="Item %i" % i, sku=str(i) * 13, price=D("9.99"), order=order, status=0, ) item.save() res = self.client.get("/modelformset/simple/") self.assertTrue("object_list" in res.context) self.assertEqual(len(res.context["object_list"]), 10) def test_fields_is_used(self): res = self.client.get("/modelformset/simple/") self.assertEqual(res.status_code, 200) fields = res.context["formset"].empty_form.fields self.assertIn("sku", fields) self.assertNotIn("date_placed", fields) def test_exclude_is_used(self): res = self.client.get("/modelformset/exclude/") self.assertEqual(res.status_code, 200) fields = res.context["formset"].empty_form.fields self.assertIn("date_placed", fields) self.assertNotIn("sku", fields) class InlineFormSetViewTests(TestCase): management_data = { "items-TOTAL_FORMS": "2", "items-INITIAL_FORMS": "0", "items-MAX_NUM_FORMS": "", } def test_create(self): order = Order(name="Dummy Order") order.save() for i in range(10): item = Item( name="Item %i" % i, sku=str(i) * 13, price=D("9.99"), order=order, status=0, ) item.save() res = self.client.get("/inlineformset/{}/".format(order.id)) self.assertTrue("object" in res.context) self.assertTrue("order" in res.context) self.assertEqual(res.status_code, 200) self.assertTrue("formset" in res.context) self.assertFalse("form" in res.context) def test_post(self): order = Order(name="Dummy Order") order.save() data = {} data.update(self.management_data) res = self.client.post("/inlineformset/{}/".format(order.id), data, follow=True) self.assertEqual(res.status_code, 200) self.assertTrue("formset" in res.context) self.assertFalse("form" in res.context) def test_save(self): order = Order(name="Dummy Order") order.save() data = { "items-0-name": "Bubble Bath", "items-0-sku": "1234567890123", "items-0-price": D("9.99"), "items-0-status": 0, "items-1-DELETE": True, } data.update(self.management_data) self.assertEqual(0, order.items.count()) res = self.client.post("/inlineformset/{}/".format(order.id), data, follow=True) order = Order.objects.get(id=order.id) context_instance = res.context["formset"][0].instance self.assertEqual("Bubble Bath", context_instance.name) self.assertEqual("", res.context["formset"][1].instance.name) self.assertEqual(1, order.items.count()) class GenericInlineFormSetViewTests(TestCase): def test_get(self): order = Order(name="Dummy Order") order.save() order2 = Order(name="Other Order") order2.save() tag = Tag(name="Test", content_object=order) tag.save() tag = Tag(name="Test2", content_object=order2) tag.save() res = self.client.get("/genericinlineformset/{}/".format(order.id)) self.assertEqual(res.status_code, 200) self.assertTrue("formset" in res.context) self.assertFalse("form" in res.context) self.assertEqual("Test", res.context["formset"].forms[0]["name"].value()) def test_post(self): order = Order(name="Dummy Order") order.save() tag = Tag(name="Test", content_object=order) tag.save() data = { "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 3, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 1, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Updated", "extra_views_tests-tag-content_type-object_id-0-id": 1, "extra_views_tests-tag-content_type-object_id-1-DELETE": True, "extra_views_tests-tag-content_type-object_id-2-DELETE": True, } res = self.client.post( "/genericinlineformset/{}/".format(order.id), data, follow=True ) self.assertEqual(res.status_code, 200) self.assertEqual("Updated", res.context["formset"].forms[0]["name"].value()) self.assertEqual(1, Tag.objects.count()) def test_post2(self): order = Order(name="Dummy Order") order.save() tag = Tag(name="Test", content_object=order) tag.save() data = { "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 3, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 1, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Updated", "extra_views_tests-tag-content_type-object_id-0-id": tag.id, "extra_views_tests-tag-content_type-object_id-1-name": "Tag 2", "extra_views_tests-tag-content_type-object_id-2-name": "Tag 3", } res = self.client.post( "/genericinlineformset/{}/".format(order.id), data, follow=True ) self.assertEqual(res.status_code, 200) self.assertEqual(3, Tag.objects.count()) def test_intial_data_is_used(self): # Specific test for initial data in genericinlineformset order = Order(name="Dummy Order") order.save() res = self.client.get("/genericinlineformset/{}/".format(order.id)) self.assertEqual(res.status_code, 200) extra_forms = res.context["formset"].extra_forms self.assertTrue(extra_forms) self.assertEqual(extra_forms[0].initial, {"name": "test_tag_name"}) class ModelWithInlinesTests(TestCase): def test_create(self): res = self.client.get("/inlines/new/") self.assertEqual(res.status_code, 200) self.assertEqual(0, Tag.objects.count()) data = { "name": "Dummy Order", "items-TOTAL_FORMS": "2", "items-INITIAL_FORMS": "0", "items-MAX_NUM_FORMS": "", "items-0-name": "Bubble Bath", "items-0-sku": "1234567890123", "items-0-price": D("9.99"), "items-0-status": 0, "items-1-DELETE": True, "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 2, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 0, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Test", "extra_views_tests-tag-content_type-object_id-1-DELETE": True, } res = self.client.post("/inlines/new/", data, follow=True) self.assertTrue("object" in res.context) self.assertTrue("order" in res.context) self.assertEqual(res.status_code, 200) self.assertEqual(1, Tag.objects.count()) # Check that form_valid has been called. self.assertRedirects(res, "/inlines/1/?form_valid_called=1") def test_create_success_message(self): res = self.client.get("/inlines/new/") self.assertEqual(res.status_code, 200) self.assertEqual(0, Tag.objects.count()) data = { "name": "Dummy Order", "items-TOTAL_FORMS": "2", "items-INITIAL_FORMS": "0", "items-MAX_NUM_FORMS": "", "items-0-name": "Bubble Bath", "items-0-sku": "1234567890123", "items-0-price": D("9.99"), "items-0-status": 0, "items-1-DELETE": True, "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 2, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 0, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Test", "extra_views_tests-tag-content_type-object_id-1-DELETE": True, } res = self.client.post("/inlines/new/", data, follow=True) messages = [ message.__str__() for message in get_messages(res.context["view"].request) ] self.assertIn("Order Dummy Order was created successfully!", messages) def test_named_create(self): res = self.client.get("/inlines/new/named/") self.assertEqual(res.status_code, 200) self.assertEqual(res.context["Items"], res.context["inlines"][0]) self.assertEqual(res.context["Tags"], res.context["inlines"][1]) def test_validation(self): data = { "items-TOTAL_FORMS": "2", "items-INITIAL_FORMS": "0", "items-MAX_NUM_FORMS": "", "items-0-name": "Test Item 1", "items-0-sku": "", "items-0-price": "", "items-0-status": 0, "items-1-name": "", "items-1-sku": "", "items-1-price": "", "items-1-status": "", "items-1-DELETE": True, "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 2, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 0, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Test", "extra_views_tests-tag-content_type-object_id-1-DELETE": True, } res = self.client.post("/inlines/new/", data, follow=True) self.assertEqual(len(res.context["form"].errors), 1) self.assertEqual(len(res.context["inlines"][0].errors[0]), 2) def test_view_object_is_none_after_failed_validation_for_createview(self): # We are testing that view.object = None even if the form validates # but one of the inline formsets does not. data = { "name": "Dummy Order", "items-TOTAL_FORMS": "2", "items-INITIAL_FORMS": "0", "items-MAX_NUM_FORMS": "", "items-0-name": "Test Item 1", "items-0-sku": "", "items-0-price": "", "items-0-status": 0, "items-1-name": "", "items-1-sku": "", "items-1-price": "", "items-1-status": "", "items-1-DELETE": True, "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 2, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 0, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Test", "extra_views_tests-tag-content_type-object_id-1-DELETE": True, } res = self.client.post("/inlines/new/", data, follow=True) self.assertEqual(len(res.context["form"].errors), 0) self.assertEqual(len(res.context["inlines"][0].errors[0]), 2) self.assertEqual(res.context["view"].object, None) def test_update(self): order = Order(name="Dummy Order") order.save() item_ids = [] for i in range(2): item = Item( name="Item %i" % i, sku=str(i) * 13, price=D("9.99"), order=order, status=0, ) item.save() item_ids.append(item.id) tag = Tag(name="Test", content_object=order) tag.save() res = self.client.get("/inlines/{}/".format(order.id)) self.assertEqual(res.status_code, 200) order = Order.objects.get(id=order.id) self.assertEqual(2, order.items.count()) self.assertEqual("Item 0", order.items.all()[0].name) data = { "name": "Dummy Order", "items-TOTAL_FORMS": "4", "items-INITIAL_FORMS": "2", "items-MAX_NUM_FORMS": "", "items-0-name": "Bubble Bath", "items-0-sku": "1234567890123", "items-0-price": D("9.99"), "items-0-status": 0, "items-0-id": item_ids[0], "items-1-name": "Bubble Bath", "items-1-sku": "1234567890123", "items-1-price": D("9.99"), "items-1-status": 0, "items-1-id": item_ids[1], "items-2-name": "Bubble Bath", "items-2-sku": "1234567890123", "items-2-price": D("9.99"), "items-2-status": 0, "items-3-DELETE": True, "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": 3, "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": 1, "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-0-name": "Test", "extra_views_tests-tag-content_type-object_id-0-id": tag.id, "extra_views_tests-tag-content_type-object_id-0-DELETE": True, "extra_views_tests-tag-content_type-object_id-1-name": "Test 2", "extra_views_tests-tag-content_type-object_id-2-name": "Test 3", } res = self.client.post("/inlines/{}/".format(order.id), data) self.assertEqual(res.status_code, 302) # Test that the returned url is the same as the instances absolute url. self.assertEqual(res.url, order.get_absolute_url()) order = Order.objects.get(id=order.id) self.assertEqual(3, order.items.count()) self.assertEqual(2, Tag.objects.count()) self.assertEqual("Bubble Bath", order.items.all()[0].name) def test_parent_instance_saved_in_form_save(self): order = Order(name="Dummy Order") order.save() data = { "name": "Dummy Order", "items-TOTAL_FORMS": "0", "items-INITIAL_FORMS": "0", "items-MAX_NUM_FORMS": "", "extra_views_tests-tag-content_type-object_id-TOTAL_FORMS": "0", "extra_views_tests-tag-content_type-object_id-INITIAL_FORMS": "0", "extra_views_tests-tag-content_type-object_id-MAX_NUM_FORMS": "", } res = self.client.post("/inlines/{}/".format(order.id), data) self.assertEqual(res.status_code, 302) order = Order.objects.get(id=order.id) self.assertTrue(order.action_on_save) def test_url_arg(self): """ Regression test for #122: get_context_data should not be called with *args """ res = self.client.get("/inlines/123/new/") self.assertEqual(res.status_code, 200) class CalendarViewTests(TestCase): def test_create(self): event = Event(name="Test Event", date=datetime.date(2012, 1, 1)) event.save() res = self.client.get("/events/2012/jan/") self.assertEqual(res.status_code, 200) class SearchableListTests(TestCase): def setUp(self): order = Order(name="Dummy Order") order.save() Item.objects.create( sku="1A", name="test A", order=order, price=0, date_placed=datetime.date(2012, 1, 1), ) Item.objects.create( sku="1B", name="test B", order=order, price=0, date_placed=datetime.date(2012, 2, 1), ) Item.objects.create( sku="C", name="test", order=order, price=0, date_placed=datetime.date(2012, 3, 1), ) def test_search(self): res = self.client.get("/searchable/", data={"q": "1A test"}) self.assertEqual(res.status_code, 200) self.assertEqual(1, len(res.context["object_list"])) res = self.client.get("/searchable/", data={"q": "1Atest"}) self.assertEqual(res.status_code, 200) self.assertEqual(0, len(res.context["object_list"])) # date search res = self.client.get("/searchable/", data={"q": "01.01.2012"}) self.assertEqual(res.status_code, 200) self.assertEqual(1, len(res.context["object_list"])) res = self.client.get("/searchable/", data={"q": "02.01.2012"}) self.assertEqual(res.status_code, 200) self.assertEqual(0, len(res.context["object_list"])) # search query provided by view's get_search_query method res = self.client.get( "/searchable/predefined_query/", data={"q": "idoesntmatter"} ) self.assertEqual(res.status_code, 200) self.assertEqual(1, len(res.context["object_list"])) # exact search query res = self.client.get("/searchable/exact_query/", data={"q": "test"}) self.assertEqual(res.status_code, 200) self.assertEqual(1, len(res.context["object_list"])) # search query consists only of spaces res = self.client.get("/searchable/", data={"q": " "}) self.assertEqual(res.status_code, 200) self.assertEqual(3, len(res.context["object_list"])) # wrong lookup try: self.assertRaises( self.client.get("/searchable/wrong_lookup/", data={"q": "test"}) ) error = False except ValueError: error = True self.assertTrue(error) class SortableViewTest(TestCase): def setUp(self): order = Order(name="Dummy Order") order.save() Item.objects.create(sku="1A", name="test A", order=order, price=0) Item.objects.create(sku="1B", name="test B", order=order, price=0) def test_sort(self): res = self.client.get("/sortable/fields/") self.assertEqual(res.status_code, 200) self.assertFalse(res.context["sort_helper"].is_sorted_by_name()) asc_url = res.context["sort_helper"].get_sort_query_by_name_asc() res = self.client.get("/sortable/fields/%s" % asc_url) self.assertEqual(res.context["object_list"][0].name, "test A") self.assertEqual(res.context["object_list"][1].name, "test B") self.assertTrue(res.context["sort_helper"].is_sorted_by_name()) desc_url = res.context["sort_helper"].get_sort_query_by_name_desc() res = self.client.get("/sortable/fields/%s" % desc_url) self.assertEqual(res.context["object_list"][0].name, "test B") self.assertEqual(res.context["object_list"][1].name, "test A") self.assertTrue(res.context["sort_helper"].is_sorted_by_name()) # reversed sorting sort_url = res.context["sort_helper"].get_sort_query_by_name() res = self.client.get("/sortable/fields/%s" % sort_url) self.assertEqual(res.context["object_list"][0].name, "test A") sort_url = res.context["sort_helper"].get_sort_query_by_name() res = self.client.get("/sortable/fields/%s" % sort_url) self.assertEqual(res.context["object_list"][0].name, "test B") # can't use fields and aliases in same time self.assertRaises( ImproperlyConfigured, lambda: self.client.get("/sortable/fields_and_aliases/"), ) # check that aliases included in params res = self.client.get("/sortable/aliases/") self.assertIn( "o=by_name", res.context["sort_helper"].get_sort_query_by_by_name() ) self.assertIn("o=by_sku", res.context["sort_helper"].get_sort_query_by_by_sku()) django-extra-views-0.14.0/extra_views_tests/urls.py000066400000000000000000000044231406010162500224310ustar00rootroot00000000000000from django.conf.urls import url from django.views.generic import TemplateView from .formsets import AddressFormSet from .views import ( AddressFormSetView, AddressFormSetViewKwargs, AddressFormSetViewNamed, EventCalendarView, FormAndFormSetOverrideView, ItemModelFormSetExcludeView, ItemModelFormSetView, OrderCreateNamedView, OrderCreateView, OrderItemFormSetView, OrderTagsView, OrderUpdateView, PagedModelFormSetView, SearchableItemListView, SortableItemListView, ) urlpatterns = [ url(r"^formset/simple/$", AddressFormSetView.as_view()), url(r"^formset/simple/named/$", AddressFormSetViewNamed.as_view()), url(r"^formset/simple/kwargs/$", AddressFormSetViewKwargs.as_view()), url( r"^formset/simple_redirect/$", AddressFormSetView.as_view(success_url="/formset/simple_redirect/valid/"), ), url( r"^formset/simple_redirect/valid/$", TemplateView.as_view(template_name="extra_views/success.html"), ), url(r"^formset/custom/$", AddressFormSetView.as_view(formset_class=AddressFormSet)), url(r"^modelformset/simple/$", ItemModelFormSetView.as_view()), url(r"^modelformset/exclude/$", ItemModelFormSetExcludeView.as_view()), url(r"^modelformset/custom/$", FormAndFormSetOverrideView.as_view()), url(r"^modelformset/paged/$", PagedModelFormSetView.as_view()), url(r"^inlineformset/(?P\d+)/$", OrderItemFormSetView.as_view()), url(r"^inlines/(\d+)/new/$", OrderCreateView.as_view()), url(r"^inlines/new/$", OrderCreateView.as_view()), url(r"^inlines/new/named/$", OrderCreateNamedView.as_view()), url(r"^inlines/(?P\d+)/$", OrderUpdateView.as_view()), url(r"^genericinlineformset/(?P\d+)/$", OrderTagsView.as_view()), url(r"^sortable/(?P\w+)/$", SortableItemListView.as_view()), url(r"^events/(?P\d{4})/(?P\w+)/$", EventCalendarView.as_view()), url(r"^searchable/$", SearchableItemListView.as_view()), url( r"^searchable/predefined_query/$", SearchableItemListView.as_view(define_query=True), ), url(r"^searchable/exact_query/$", SearchableItemListView.as_view(exact_query=True)), url( r"^searchable/wrong_lookup/$", SearchableItemListView.as_view(wrong_lookup=True) ), ] django-extra-views-0.14.0/extra_views_tests/views.py000066400000000000000000000113001406010162500225710ustar00rootroot00000000000000from django.views import generic from extra_views import ( CalendarMonthView, CreateWithInlinesView, FormSetSuccessMessageMixin, FormSetView, InlineFormSetFactory, InlineFormSetView, ModelFormSetView, NamedFormsetsMixin, SearchableListMixin, SortableListMixin, SuccessMessageMixin, UpdateWithInlinesView, ) from extra_views.generic import GenericInlineFormSetFactory, GenericInlineFormSetView from .forms import AddressForm, ItemForm, OrderForm from .formsets import BaseArticleFormSet from .models import Event, Item, Order, Tag class AddressFormSetView(FormSetSuccessMessageMixin, FormSetView): form_class = AddressForm template_name = "extra_views/address_formset.html" success_message = "Formset objects were created successfully!" class AddressFormSetViewNamed(NamedFormsetsMixin, AddressFormSetView): inlines_names = ["AddressFormset"] class AddressFormSetViewKwargs(FormSetView): # Used for testing class level kwargs form_class = AddressForm template_name = "extra_views/address_formset.html" formset_kwargs = {"auto_id": "id_test_%s", "form_kwargs": {"empty_permitted": True}} factory_kwargs = {"max_num": 27} prefix = "test_prefix" initial = [{"name": "address1"}] class ItemModelFormSetView(ModelFormSetView): model = Item fields = ["name", "sku", "price", "order", "status"] template_name = "extra_views/item_formset.html" class ItemModelFormSetExcludeView(ModelFormSetView): model = Item exclude = ["sku", "price"] template_name = "extra_views/item_formset.html" class FormAndFormSetOverrideView(ModelFormSetView): model = Item form_class = ItemForm formset_class = BaseArticleFormSet template_name = "extra_views/item_formset.html" class OrderItemFormSetView(InlineFormSetView): model = Order fields = ["name", "sku", "price", "order", "status"] inline_model = Item template_name = "extra_views/inline_formset.html" class PagedModelFormSetView(ModelFormSetView): paginate_by = 2 model = Item template_name = "extra_views/paged_formset.html" class ItemsInline(InlineFormSetFactory): model = Item fields = ["name", "sku", "price", "order", "status"] class TagsInline(GenericInlineFormSetFactory): model = Tag fields = ["name"] class OrderCreateView(SuccessMessageMixin, CreateWithInlinesView): model = Order fields = ["name"] context_object_name = "order" inlines = [ItemsInline, TagsInline] template_name = "extra_views/order_and_items.html" success_message = "Order %(name)s was created successfully!" def form_valid(self, form): response = super().form_valid(form) # Update the response url to indicate that form_valid was called response["Location"] += "?form_valid_called=1" return response class OrderCreateNamedView(NamedFormsetsMixin, OrderCreateView): inlines_names = ["Items", "Tags"] class OrderUpdateView(UpdateWithInlinesView): model = Order form_class = OrderForm inlines = [ItemsInline, TagsInline] template_name = "extra_views/order_and_items.html" class OrderTagsView(GenericInlineFormSetView): model = Order inline_model = Tag template_name = "extra_views/inline_formset.html" initial = [{"name": "test_tag_name"}] class EventCalendarView(CalendarMonthView): template_name = "extra_views/event_calendar_month.html" model = Event month_format = "%b" date_field = "date" class SearchableItemListView(SearchableListMixin, generic.ListView): template_name = "extra_views/item_list.html" search_fields = ["name", "sku"] search_date_fields = ["date_placed"] model = Item define_query = False exact_query = False wrong_lookup = False def get_search_query(self): if self.define_query: return "test B" else: return super().get_search_query() def get(self, request, *args, **kwargs): if self.exact_query: self.search_fields = [("name", "iexact"), "sku"] elif self.wrong_lookup: self.search_fields = [("name", "gte"), "sku"] return super().get(request, *args, **kwargs) class SortableItemListView(SortableListMixin, generic.ListView): template_name = "extra_views/sortable_item_list.html" sort_fields = ["name", "sku"] model = Item def get(self, request, *args, **kwargs): if kwargs["flag"] == "fields_and_aliases": self.sort_fields_aliases = [("name", "by_name"), ("sku", "by_sku")] elif kwargs["flag"] == "aliases": self.sort_fields_aliases = [("name", "by_name"), ("sku", "by_sku")] self.sort_fields = [] return super().get(request, *args, **kwargs) django-extra-views-0.14.0/manage.py000077500000000000000000000010451406010162500171120ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "extra_views_tests.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) django-extra-views-0.14.0/setup.cfg000066400000000000000000000000341406010162500171230ustar00rootroot00000000000000[bdist_wheel] universal = 1 django-extra-views-0.14.0/setup.py000077500000000000000000000016621406010162500170270ustar00rootroot00000000000000import re from setuptools import setup # get version without importing with open("extra_views/__init__.py", "rb") as f: VERSION = str(re.search('__version__ = "(.+?)"', f.read().decode("utf-8")).group(1)) setup( name="django-extra-views", version=VERSION, url="https://github.com/AndrewIngram/django-extra-views", install_requires=["Django >=2.1"], description="Extra class-based views for Django", long_description=open("README.rst", "r").read(), license="MIT", author="Andrew Ingram", author_email="andy@andrewingram.net", packages=["extra_views", "extra_views.contrib"], classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", ], ) django-extra-views-0.14.0/tox.ini000066400000000000000000000027451406010162500166300ustar00rootroot00000000000000[tox] envlist = py35-django{21,22} py36-django{21,22,30,31,master} py37-django{21,22,30,31,master} py38-django{22,30,31,master} docs [testenv] setenv = PYTHONPATH = {toxinidir} PYTHONWARNINGS = all PYTEST_ADDOPTS = --cov --cov-fail-under=85 --cov-report=html --cov-report=term commands = pytest {posargs} deps = django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 django30: Django>=3.0a1,<3.1 django31: Django>=3.1,<3.2 djangomaster: https://github.com/django/django/archive/main.tar.gz pytest-django pytest-cov [testenv:lint] skip_install = True deps = flake8 isort black commands = flake8 {toxinidir} {posargs} isort --check-only black {toxinidir} --check [testenv:docs] whitelist_externals=make changedir = docs deps = sphinx commands = make html [pytest] DJANGO_SETTINGS_MODULE = extra_views_tests.settings python_files = tests.py test_*.py *_tests.py [coverage:run] branch = True source = extra_views [flake8] max-complexity = 10 exclude = extra_views_tests/migrations/, .tox/, build/lib/, docs ignore = W191, W503, E203 max-line-length = 88 [isort] default_section = THIRDPARTY known_first_party = extra_views skip = .tox/ # black compatibility, as per # https://black.readthedocs.io/en/stable/the_black_code_style.html?highlight=.isort.cfg#how-black-wraps-lines multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True line_length = 88