pax_global_header00006660000000000000000000000064133631233570014520gustar00rootroot0000000000000052 comment=903251b6efa8e5c8d0bf92336f964732da3c97b4 django-extra-views-0.12.0/000077500000000000000000000000001336312335700153165ustar00rootroot00000000000000django-extra-views-0.12.0/.github/000077500000000000000000000000001336312335700166565ustar00rootroot00000000000000django-extra-views-0.12.0/.github/SUPPORT.md000066400000000000000000000012221336312335700203510ustar00rootroot00000000000000# 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.12.0/.gitignore000066400000000000000000000002141336312335700173030ustar00rootroot00000000000000*.pyc .project .pydevproject .coverage .DS_Store django_extra_views.egg-info/ htmlcov/ build/ docs/_build/ dist/ .idea nosetests.xml /.tox/django-extra-views-0.12.0/.travis.yml000066400000000000000000000022071336312335700174300ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.4" - "3.5" - "3.6" # Travis does not currently support Python 3.7 globally, see below. # - "3.7" env: - DJANGO=django111 - DJANGO=django20 - DJANGO=django21 - DJANGO=djangomaster matrix: # Workaround to include Python 3.7 include: - python: "3.7" dist: xenial sudo: true env: DJANGO=django111 - python: "3.7" dist: xenial sudo: true env: DJANGO=django20 - python: "3.7" dist: xenial sudo: true env: DJANGO=django21 - python: "3.7" dist: xenial sudo: true env: DJANGO=djangomaster exclude: - python: "2.7" env: DJANGO=django20 - python: "2.7" env: DJANGO=django21 - python: "2.7" env: DJANGO=djangomaster - python: "3.4" env: DJANGO=django21 - python: "3.4" env: DJANGO=djangomaster 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.12.0/AUTHORS.rst000066400000000000000000000007201336312335700171740ustar00rootroot00000000000000Primary 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.12.0/CHANGELOG.rst000066400000000000000000000067541336312335700173530ustar00rootroot00000000000000Change History ============== 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``. 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) ------------------ Begin of this changelog. django-extra-views-0.12.0/LICENSE000066400000000000000000000020611336312335700163220ustar00rootroot00000000000000The 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.12.0/MANIFEST.in000066400000000000000000000004171336312335700170560ustar00rootroot00000000000000include 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.12.0/README.rst000066400000000000000000000131201336312335700170020ustar00rootroot00000000000000|travis| |codecov| django-extra-views - The missing class-based generic views for Django ======================================================================== Django's class-based generic views are great, they let you accomplish a large number of web application design patterns in relatively few lines of code. They do have their limits though, and that's what this library of views aims to overcome. .. |travis| image:: https://secure.travis-ci.org/AndrewIngram/django-extra-views.svg?branch=master :target: https://travis-ci.org/AndrewIngram/django-extra-views .. |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 Installation ------------ Installing from pypi (using pip). :: pip install django-extra-views Installing from github. :: pip install -e git://github.com/AndrewIngram/django-extra-views.git#egg=django-extra-views See the `documentation here`_ .. _documentation here: https://django-extra-views.readthedocs.org/en/latest/ Features so far ------------------ - FormSet and ModelFormSet views - The formset equivalents of FormView and ModelFormView. - InlineFormSetView - Lets you edit formsets related to a model (uses inlineformset_factory) - CreateWithInlinesView and UpdateWithInlinesView - Lets you edit a model and its relations - GenericInlineFormSetView, the equivalent of InlineFormSetView but for GenericForeignKeys - Support for generic inlines in CreateWithInlinesView and UpdateWithInlinesView - Support for naming each inline or formset with NamedFormsetsMixin - SortableListMixin - Generic mixin for sorting functionality in your views - SearchableListMixin - Generic mixin for search functionality in your views Still to do ----------- I'd like to 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. Examples -------- Defining a FormSetView. .. code-block:: python from extra_views import FormSetView class AddressFormSet(FormSetView): form_class = AddressForm template_name = 'address_formset.html' Defining a ModelFormSetView. .. code-block:: python from extra_views import ModelFormSetView class ItemFormSetView(ModelFormSetView): model = Item template_name = 'item_formset.html' fields = '__all__' Defining a CreateWithInlinesView and an UpdateWithInlinesView. .. code-block:: python from extra_views import CreateWithInlinesView, UpdateWithInlinesView, InlineFormSetFactory from extra_views.generic import GenericInlineFormSetFactory class ItemInline(InlineFormSetFactory): model = Item fields = '__all__' class TagInline(GenericInlineFormSetFactory): model = Tag fields = '__all__' class CreateOrderView(CreateWithInlinesView): model = Order inlines = [ItemInline, TagInline] fields = '__all__' class UpdateOrderView(UpdateWithInlinesView): model = Order inlines = [ItemInline, TagInline] fields = '__all__' # Example URLs. urlpatterns = [ url(r'^orders/new/$', CreateOrderView.as_view()), url(r'^orders/(?P\d+)/$', UpdateOrderView.as_view()), ] Other bits of functionality --------------------------- If you want more control over the names of your formsets (as opposed to iterating over inlines), you can use NamedFormsetsMixin. .. code-block:: python from extra_views import NamedFormsetsMixin class CreateOrderView(NamedFormsetsMixin, CreateWithInlinesView): model = Order inlines = [ItemInline, TagInline] inlines_names = ['Items', 'Tags'] fields = '__all__' You can add search functionality to your ListViews by adding SearchableMixin 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. Define sorting in 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 }}`` More descriptive examples to come. django-extra-views-0.12.0/docs/000077500000000000000000000000001336312335700162465ustar00rootroot00000000000000django-extra-views-0.12.0/docs/Makefile000066400000000000000000000127441336312335700177160ustar00rootroot00000000000000# 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.12.0/docs/changelog.rst000066400000000000000000000000361336312335700207260ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst django-extra-views-0.12.0/docs/conf.py000066400000000000000000000176631336312335700175620ustar00rootroot00000000000000# -*- 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 sys, os # 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. # # The short X.Y version. version = '0.5.4' # The full version, including alpha/beta/rc tags. release = '0.5.4' # 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.12.0/docs/index.rst000066400000000000000000000011521336312335700201060ustar00rootroot00000000000000Welcome to Django Extra Views' documentation! ============================================== Django Extra Views provides a number of additional class-based generic views to complement those provide by Django itself. Installation ------------ Installing from pypi (using pip). :: pip install django-extra-views Installing from github. :: pip install -e git://github.com/AndrewIngram/django-extra-views.git#egg=django-extra-views Contents -------- .. toctree:: :maxdepth: 2 views misc changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-extra-views-0.12.0/docs/make.bat000066400000000000000000000117741336312335700176650ustar00rootroot00000000000000@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.12.0/docs/misc.rst000066400000000000000000000017741336312335700177440ustar00rootroot00000000000000Miscellaneous ============= formset_class ------------- The :code:`formset_class` option should be used if you intend to customize your :code:`InlineFormSetFactory` and its associated formset methods. For example, imagine you'd like to add your custom :code:`clean` method for a formset. Then, define a custom formset class, a subclass of Django :code:`BaseInlineFormSet`, like this:: from django.forms.models import BaseInlineFormSet class MyCustomInlineFormSet(BaseInlineFormSet): def clean(self): # ... # Your custom clean logic goes here Now, in your :code:`InlineFormSetFactory` sub-class, use your formset class via :code:`formset_class` setting, like this:: from extra_views import InlineFormSetFactory class MyInline(InlineFormSetFactory): model = SomeModel form_class = SomeForm formset_class = MyCustomInlineFormSet # enables our custom inline This will enable :code:`clean` method being executed on your :code:`MyInline`. django-extra-views-0.12.0/docs/views.rst000066400000000000000000000130601336312335700201350ustar00rootroot00000000000000Views ===== 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:: from extra_views import FormSetView from foo.forms import MyForm class MyFormSetView(FormSetView): template_name = 'myformset.html' form_class = MyForm 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 stuff return super(MyFormSetView, self).formset_valid(formset) This view will render the template :code:`myformset.html` with a context variable :code:`formset` representing the formset of MyForm. Once POSTed and successfully validated, :code:`formset_valid` will be called which is where your handling logic goes, then it redirects to :code:`success_url`. FormSetView exposes all the parameters you'd normally be able to pass to the :code:`FormSet` constructor and formset_factory. Example (using the default settings):: class MyFormSetView(FormSetView): template_name = 'myformset.html' form_class = MyForm success_url = 'success/' factory_kwargs = {'extra': 2, 'max_num': None, 'can_order': False, 'can_delete': False} formset_kwargs = {'auto_id': 'my_id_%s'} ... ModelFormSetView ---------------- ModelFormSetView makes use of Django's modelformset_factory, using the declarative syntax used in FormSetView as well as Django's own class-based views. So as you'd expect, the simplest usage is as follows:: from extra_views import ModelFormSetView from foo.models import MyModel class MyModelFormSetView(ModelFormSetView): template_name = 'mymodelformset.html' model = MyModel 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:`MyModel` 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:: class MyModelFormSetView(ModelFormSetView): template_name = 'mymodelformset.html' model = MyModel def get_queryset(self): slug = self.kwargs['slug'] return super(MyModelFormSetView, self).get_queryset().filter(slug=slug) Optionally, :code:`fields` can be used to define the fields displayed in the formset:: class MyModelFormSetView(ModelFormSetView): template_name = 'mymodelformset.html' model = MyModel fields = ['name', 'date', 'slug'] :code:`exclude` can be set in an analogous way. InlineFormSetView ----------------- When you want to edit models related to a parent model (using a ForeignKey), you'll want to use InlineFormSetView. An example use case would be editing user reviews related to a product:: from extra_views import InlineFormSetView class EditProductReviewsView(InlineFormSetView): model = Product inline_model = Review ... Aside from the use of :code:`model` and :code:`inline_model`, :code:`InlineFormSetView` works more-or-less in the same way as :code:`ModelFormSetView`. GenericInlineFormSetView ------------------------ You can also use generic relationships for your inline formsets, this makes use of Django's :code:`generic_inlineformset_factory`. :code:`ct_field` and :code:`fk_field` should be set in :code:`factory_kwargs` if they need to be changed from their default values:: from extra_views.generic import GenericInlineFormSetView class EditProductReviewsView(GenericInlineFormSetView): model = Product inline_model = Review factory_kwargs = {'ct_field': 'content_type', 'fk_field': 'object_id', 'max_num': 1} formset_kwargs = {'save_as_new': True} ... 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 both normal inline relationships and generic inlines:: from extra_views import InlineFormSetFactory, CreateWithInlinesView, UpdateWithInlinesView from extra_views.generic import GenericInlineFormSetFactory class ItemsInline(InlineFormSetFactory): model = Item class TagsInline(GenericInlineFormSetFactory): model = Tag class OrderCreateView(CreateWithInlinesView): model = Order inlines = [ItemsInline, TagsInline] def get_success_url(self): return self.object.get_absolute_url() class OrderUpdateView(UpdateWithInlinesView): model = Order form_class = OrderForm inlines = [ItemsInline, TagsInline] def get_success_url(self): return self.object.get_absolute_url() django-extra-views-0.12.0/extra_views/000077500000000000000000000000001336312335700176565ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views/__init__.py000066400000000000000000000006161336312335700217720ustar00rootroot00000000000000from extra_views.formsets import FormSetView, ModelFormSetView, \ InlineFormSetView, BaseFormSetMixin, BaseInlineFormSetMixin from extra_views.advanced import CreateWithInlinesView, \ UpdateWithInlinesView, InlineFormSetFactory, NamedFormsetsMixin, InlineFormSet from extra_views.dates import CalendarMonthView from extra_views.contrib.mixins import SearchableListMixin, SortableListMixin django-extra-views-0.12.0/extra_views/advanced.py000066400000000000000000000157101336312335700220010ustar00rootroot00000000000000import django from django.views.generic.base import ContextMixin from django.views.generic.edit import FormView, ModelFormMixin from django.views.generic.detail import SingleObjectTemplateResponseMixin from django.http import HttpResponseRedirect from django.forms.formsets import all_valid 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(InlineFormSetFactory, self).construct_formset() formset.model = self.inline_model return formset class InlineFormSet(InlineFormSetFactory): def __init__(self, *args, **kwargs): from warnings import warn warn('`extra_views.InlineFormSet` has been renamed to `InlineFormSetFactory`. ' '`InlineFormSet` will be removed in a future release.', DeprecationWarning) super(InlineFormSet, self).__init__(*args, **kwargs) 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. """ self.object = form.save() for formset in inlines: formset.save() return HttpResponseRedirect(self.get_success_url()) 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) 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) 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(BaseCreateWithInlinesView, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = None return super(BaseCreateWithInlinesView, self).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(BaseUpdateWithInlinesView, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() return super(BaseUpdateWithInlinesView, self).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(NamedFormsetsMixin, self).get_context_data(**context) django-extra-views-0.12.0/extra_views/contrib/000077500000000000000000000000001336312335700213165ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views/contrib/__init__.py000066400000000000000000000000001336312335700234150ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views/contrib/mixins.py000066400000000000000000000174121336312335700232040ustar00rootroot00000000000000from __future__ import unicode_literals import datetime import functools import operator from django.views.generic.base import ContextMixin from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.contrib import messages import six from six.moves import reduce 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, six.string_types): 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(reduce(operator.or_, filters)) qs = qs.filter(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 aliace 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.12.0/extra_views/dates.py000066400000000000000000000210751336312335700213350ustar00rootroot00000000000000from __future__ import unicode_literals from calendar import Calendar from collections import defaultdict import datetime import math from django.views.generic.dates import DateMixin, YearMixin, MonthMixin, _date_from_string from django.views.generic.list import MultipleObjectTemplateResponseMixin, BaseListView from django.db. models import Q from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ 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(BaseCalendarMonthView, self).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(BaseCalendarMonthView, self).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.12.0/extra_views/formsets.py000066400000000000000000000254751336312335700221070ustar00rootroot00000000000000import django from django.views.generic.base import TemplateResponseMixin, View, ContextMixin from django.http import HttpResponseRedirect from django.forms.formsets import formset_factory from django.forms.models import modelformset_factory, inlineformset_factory 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() if hasattr(self, 'get_extra_form_kwargs'): klass = type(self).__name__ raise DeprecationWarning( 'Calling {0}.get_extra_form_kwargs is no longer supported. ' 'Set `form_kwargs` in {0}.formset_kwargs or override ' '{0}.get_formset_kwargs() directly.'.format(klass), ) 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 """ # Perform deprecation check for attr in ['extra', 'max_num', 'can_order', 'can_delete', 'ct_field', 'formfield_callback', 'fk_name', 'widgets', 'ct_fk_field']: if hasattr(self, attr): klass = type(self).__name__ raise DeprecationWarning( 'Setting `{0}.{1}` at the class level is now deprecated. ' 'Set `{0}.factory_kwargs` instead.'.format(klass, attr) ) kwargs = self.factory_kwargs.copy() if self.get_formset_class(): kwargs['formset'] = self.get_formset_class() return kwargs class BaseFormSetMixin(BaseFormSetFactory): def __init__(self, *args, **kwargs): from warnings import warn warn('`extra_views.BaseFormSetMixin` has been renamed to ' '`BaseFormSetFactory`. `BaseFormSetMixin` will be removed in ' 'a future release.', DeprecationWarning) super(BaseFormSetMixin, self).__init__(*args, **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(ModelFormSetMixin, self).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(ModelFormSetMixin, self).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(ModelFormSetMixin, self).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. """ # Perform deprecation check if hasattr(self, 'save_as_new'): klass = type(self).__name__ raise DeprecationWarning( 'Setting `{0}.save_as_new` at the class level is now ' 'deprecated. Set `{0}.formset_kwargs` instead.'.format(klass) ) kwargs = super(BaseInlineFormSetFactory, self).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(BaseInlineFormSetFactory, self).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 BaseInlineFormSetMixin(BaseInlineFormSetFactory): def __init__(self, *args, **kwargs): from warnings import warn warn('`extra_views.BaseInlineFormSetMixin` has been renamed to ' '`BaseInlineFormSetFactory`. `BaseInlineFormSetMixin` will be removed in ' 'a future release.', DeprecationWarning) super(BaseInlineFormSetMixin, self).__init__(*args, **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(InlineFormSetMixin, self).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(BaseModelFormSetView, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object_list = self.get_queryset() return super(BaseModelFormSetView, self).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(BaseInlineFormSetView, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() return super(BaseInlineFormSetView, self).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.12.0/extra_views/generic.py000066400000000000000000000052041336312335700216450ustar00rootroot00000000000000import django from django.contrib.contenttypes.forms import generic_inlineformset_factory from extra_views.formsets import BaseInlineFormSetFactory, InlineFormSetMixin, \ BaseInlineFormSetView, 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 BaseGenericInlineFormSetMixin(BaseGenericInlineFormSetFactory): def __init__(self, *args, **kwargs): from warnings import warn warn('`extra_views.BaseGenericInlineFormSetMixin` has been renamed to ' '`BaseGenericInlineFormSetFactory`. `BaseGenericInlineFormSetMixin` ' 'will be removed in a future release.', DeprecationWarning) super(BaseGenericInlineFormSetMixin, self).__init__(*args, **kwargs) 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 GenericInlineFormSet(GenericInlineFormSetFactory): def __init__(self, *args, **kwargs): from warnings import warn warn('`extra_views.GenericInlineFormSet` has been renamed to ' '`GenericInlineFormSetFactory`. `GenericInlineFormSet` ' 'will be removed in a future release.', DeprecationWarning) super(GenericInlineFormSet, self).__init__(*args, **kwargs) 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.12.0/extra_views/models.py000066400000000000000000000000001336312335700215010ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views_tests/000077500000000000000000000000001336312335700211005ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views_tests/__init__.py000066400000000000000000000000001336312335700231770ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views_tests/forms.py000066400000000000000000000017231336312335700226030ustar00rootroot00000000000000from django import forms from .models import Order, Item class OrderForm(forms.ModelForm): class Meta: model = Order fields = ['name'] def save(self, commit=True): instance = super(OrderForm, self).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(AddressForm, self).__init__(*args, **kwargs) django-extra-views-0.12.0/extra_views_tests/formsets.py000066400000000000000000000013251336312335700233150ustar00rootroot00000000000000from django.forms.formsets import BaseFormSet from django.forms.models import BaseModelFormSet from django import forms 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(AddressFormSet, self).add_fields(form, index) form.fields['county'] = forms.ChoiceField(choices=COUNTRY_CHOICES, initial='gb') class BaseArticleFormSet(BaseModelFormSet): def add_fields(self, form, index): super(BaseArticleFormSet, self).add_fields(form, index) form.fields["notes"] = forms.CharField(initial="Write notes here") django-extra-views-0.12.0/extra_views_tests/models.py000066400000000000000000000033171336312335700227410ustar00rootroot00000000000000import datetime try: from django.utils.timezone import now except ImportError: now = datetime.datetime.now import django from django.db import models from django.contrib.contenttypes.models import ContentType if django.VERSION < (1, 8): from django.contrib.contenttypes.generic import GenericForeignKey else: from django.contrib.contenttypes.fields import GenericForeignKey STATUS_CHOICES = ( (0, 'Placed'), (1, 'Charged'), (2, 'Shipped'), (3, 'Cancelled'), ) class Order(models.Model): name = models.CharField(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) 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 __unicode__(self): return '%s (%s)' % (self.name, self.sku) 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 __unicode__(self): return self.name class Event(models.Model): name = models.CharField(max_length=255) date = models.DateField() def __unicode__(self): return self.name django-extra-views-0.12.0/extra_views_tests/templates/000077500000000000000000000000001336312335700230765ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views_tests/templates/404.html000066400000000000000000000000031336312335700242640ustar00rootroot00000000000000404django-extra-views-0.12.0/extra_views_tests/templates/extra_views/000077500000000000000000000000001336312335700254365ustar00rootroot00000000000000django-extra-views-0.12.0/extra_views_tests/templates/extra_views/address_formset.html000066400000000000000000000003371336312335700315130ustar00rootroot00000000000000 Address Formset

Address Formset

{{ formset }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/event_calendar_month.html000066400000000000000000000002201336312335700324750ustar00rootroot00000000000000 Event Calendar

Event Calendar

{{ object_list }} django-extra-views-0.12.0/extra_views_tests/templates/extra_views/formsets_multiview.html000066400000000000000000000004731336312335700322770ustar00rootroot00000000000000 Order and Address MultiView
{{ order_form }}
{{ items_formset }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/inline_formset.html000066400000000000000000000003351336312335700313420ustar00rootroot00000000000000 Inline Formset

Inline Formset

{{ formset }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/item_formset.html000066400000000000000000000003311336312335700310160ustar00rootroot00000000000000 Item Formset

Item Formset

{{ formset }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/item_list.html000066400000000000000000000002531336312335700303150ustar00rootroot00000000000000 Item List

Item List

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

Order and Items

{{ form }} {% for formset in inlines %} {{ formset }} {% endfor %}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/orderaddress_multiview.html000066400000000000000000000004721336312335700331150ustar00rootroot00000000000000 Order and Address MultiView
{{ order_form }}
{{ address_form }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/orderitems_multiview.html000066400000000000000000000004711336312335700326100ustar00rootroot00000000000000 Order and Items MultiView
{{ order_form }}
{{ items_formset }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/paged_formset.html000066400000000000000000000003311336312335700311400ustar00rootroot00000000000000 Item Formset

Item Formset

{{ formset }}
django-extra-views-0.12.0/extra_views_tests/templates/extra_views/sortable_item_list.html000066400000000000000000000016351336312335700322150ustar00rootroot00000000000000 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.12.0/extra_views_tests/templates/extra_views/success.html000066400000000000000000000001561336312335700277760ustar00rootroot00000000000000 Success

Success

django-extra-views-0.12.0/extra_views_tests/tests.py000066400000000000000000000517301336312335700226220ustar00rootroot00000000000000from __future__ import unicode_literals import datetime from decimal import Decimal as D import django from django.core.exceptions import ImproperlyConfigured from django.forms import ValidationError from django.test import TestCase if django.VERSION < (1, 8): from django.utils.unittest import expectedFailure else: from unittest import expectedFailure from .models import Item, Order, Tag, Event 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) @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()) 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_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) 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.12.0/extra_views_tests/urls.py000066400000000000000000000042261336312335700224430ustar00rootroot00000000000000from django.conf.urls import url from django.views.generic import TemplateView from .formsets import AddressFormSet from .views import AddressFormSetView, AddressFormSetViewNamed, ItemModelFormSetView, \ FormAndFormSetOverrideView, PagedModelFormSetView, OrderItemFormSetView, \ OrderCreateView, OrderUpdateView, OrderTagsView, EventCalendarView, OrderCreateNamedView, \ SortableItemListView, SearchableItemListView, AddressFormSetViewKwargs, \ ItemModelFormSetExcludeView 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.12.0/extra_views_tests/views.py000066400000000000000000000106531336312335700226140ustar00rootroot00000000000000from extra_views import FormSetView, ModelFormSetView, InlineFormSetView, InlineFormSetFactory, CreateWithInlinesView, UpdateWithInlinesView, CalendarMonthView, NamedFormsetsMixin, SortableListMixin, SearchableListMixin from extra_views.generic import GenericInlineFormSetFactory, GenericInlineFormSetView from django.views import generic from .forms import AddressForm, ItemForm, OrderForm from .formsets import BaseArticleFormSet from .models import Item, Order, Tag, Event class AddressFormSetView(FormSetView): form_class = AddressForm template_name = 'extra_views/address_formset.html' 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(CreateWithInlinesView): model = Order fields = ['name'] context_object_name = 'order' inlines = [ItemsInline, TagsInline] template_name = 'extra_views/order_and_items.html' def get_success_url(self): return '/inlines/%i/' % self.object.pk 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' def get_success_url(self): return '' 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(SearchableItemListView, self).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(SearchableItemListView, self).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(SortableItemListView, self).get(request, *args, **kwargs) django-extra-views-0.12.0/runtests.py000077500000000000000000000040011336312335700175550ustar00rootroot00000000000000#!/usr/bin/env python import sys import logging from optparse import OptionParser from django.conf import settings logging.disable(logging.CRITICAL) def configure(nose_args=None): if not settings.configured: settings.configure( DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, TEMPLATES=[{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, }], INSTALLED_APPS=[ 'django.contrib.contenttypes', 'django.contrib.auth', 'extra_views', 'extra_views_tests', ], ROOT_URLCONF='extra_views_tests.urls', NOSE_ARGS=nose_args ) def runtests(*test_args): from django_nose import NoseTestSuiteRunner runner = NoseTestSuiteRunner() if not test_args: test_args = ['extra_views_tests'] num_failures = runner.run_tests(test_args) if num_failures: sys.exit(num_failures) if __name__ == '__main__': parser = OptionParser() parser.add_option('--with-coverage', dest='coverage', default=False, action='store_true') parser.add_option('--with-xunit', dest='xunit', default=False, action='store_true') parser.add_option('--with-spec', dest='with_spec', default=False, action='store_true') parser.add_option('--pdb', dest='pdb', default=False, action='store_true') options, args = parser.parse_args() nose_args = [] if options.pdb: nose_args.append('--pdb') if options.coverage: # Nose automatically uses any options passed to runtests.py, which is # why the coverage trigger uses '--with-coverage' and why we don't need # to explicitly include it here. nose_args.extend([ '--cover-package=extra_views', '--cover-branch', '--cover-html', '--cover-html-dir=htmlcov']) configure(nose_args) runtests() django-extra-views-0.12.0/setup.cfg000066400000000000000000000000341336312335700171340ustar00rootroot00000000000000[bdist_wheel] universal = 1 django-extra-views-0.12.0/setup.py000077500000000000000000000014311336312335700170320ustar00rootroot00000000000000from setuptools import setup setup( name='django-extra-views', version='0.12.0', url='https://github.com/AndrewIngram/django-extra-views', install_requires=[ 'Django >=1.11', 'six>=1.5.2', ], 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', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3'] ) django-extra-views-0.12.0/tox.ini000066400000000000000000000010331336312335700166260ustar00rootroot00000000000000[tox] envlist = py{27}-django{111} py{34}-django{111,20} py{35,36,37}-django{111,20,21,master} docs [testenv] setenv = PYTHONPATH = {toxinidir} commands = {toxinidir}/runtests.py --with-coverage deps = coverage django-nose django111: Django>=1.11,<1.12 django20: Django>=2.0<2.1 django21: Django>=2.1<2.2 djangomaster: https://github.com/django/django/archive/master.tar.gz [testenv:docs] whitelist_externals=make changedir = docs deps = sphinx commands = make html