├── .github └── workflows │ ├── ci.yml │ └── make-requirements.txt.sh ├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_addanother ├── __init__.py ├── contrib │ ├── __init__.py │ └── select2.py ├── static │ └── django_addanother │ │ ├── addanother.css │ │ └── django_jquery.js ├── templates │ └── django_addanother │ │ └── related_widget_wrapper.html ├── views.py └── widgets.py ├── docs ├── Makefile ├── conf.py ├── demo.rst ├── django_settings.py ├── edit-related.rst ├── howitworks.rst ├── index.rst ├── installation.rst ├── make.bat ├── reference │ ├── index.rst │ ├── select2_widgets.rst │ ├── views.rst │ └── widgets.rst ├── select2.rst └── usage.rst ├── requirements.readthedocs.txt ├── setup.py └── test_project ├── __init__.py ├── manage.py ├── settings.py ├── testapp ├── __init__.py ├── admin.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── testapp │ │ ├── main.html │ │ └── team_form.html ├── tests.py └── views.py ├── urls.py └── wsgi.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | Build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | include: 15 | - python-version: "2.7" 16 | django-version: "1.11" 17 | select2: false 18 | browser: false 19 | - python-version: "2.7" 20 | django-version: "1.11" 21 | select2: true 22 | browser: true 23 | - python-version: "3.5" 24 | django-version: "2.0" 25 | select2: true 26 | browser: false 27 | - python-version: "3.6" 28 | django-version: "2.1" 29 | select2: false 30 | browser: false 31 | - python-version: "3.7" 32 | django-version: "3.0" 33 | select2: true 34 | browser: true 35 | - python-version: "3.8" 36 | django-version: "3.1" 37 | select2: true 38 | browser: false 39 | - python-version: "3.9" 40 | django-version: "4.0" 41 | select2: false 42 | browser: false 43 | - python-version: "3.10" 44 | django-version: "4.0" 45 | select2: true 46 | browser: true 47 | - python-version: "3.11.0-rc.1" 48 | django-version: "4.1" 49 | select2: true 50 | browser: true 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v3 54 | - name: Make requirements.txt 55 | run: | 56 | .github/workflows/make-requirements.txt.sh | tee requirements.txt 57 | env: 58 | PYTHON_VERSION: ${{ matrix.python-version }} 59 | DJANGO_VERSION: ${{ matrix.django-version }} 60 | SELECT2: ${{ matrix.select2 }} 61 | BROWSER: ${{ matrix.browser }} 62 | - name: Set up Python ${{ matrix.python-version }} 63 | uses: actions/setup-python@v4 64 | with: 65 | python-version: ${{ matrix.python-version }} 66 | cache: pip 67 | - name: Install pip deps 68 | run: pip install --upgrade -r requirements.txt 69 | - name: Install apt deps 70 | run: sudo apt-get update && sudo apt-get install -y firefox-geckodriver xvfb 71 | if: matrix.browser == true 72 | - name: Run pytest 73 | run: | 74 | select2_exclude=${{ matrix.select2 != true && 'select2' || '' }} 75 | browser_exclude=${{ matrix.browser != true && 'test_browser' || '' }} 76 | runner=${{ matrix.browser == true && 'xvfb-run' || '' }} 77 | $runner python -m pytest \ 78 | -vv \ 79 | --cov django_addanother \ 80 | --create-db \ 81 | -k "not ${select2_exclude:-____} and not ${browser_exclude:-____}" \ 82 | test_project/testapp/tests.py 83 | env: 84 | DJANGO_SETTINGS_MODULE: test_project.settings 85 | -------------------------------------------------------------------------------- /.github/workflows/make-requirements.txt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | DEPS="wheel pytest pytest-django pytest-cov" 4 | 5 | if [ $PYTHON_VERSION = 2.7 ]; then 6 | DEPS="$DEPS django-appconf<=1.0 pytest-splinter<3 splinter<0.17" 7 | else 8 | DEPS="$DEPS django-appconf git+https://github.com/pytest-dev/pytest-splinter" # TODO: Switch back to PyPI https://github.com/pytest-dev/pytest-splinter/commit/0f9cae60055399bff9e3f3ce2c039d9c38f28e21 9 | fi 10 | 11 | if [ $DJANGO_VERSION = 1.11 ]; then DEPS="$DEPS django>=1.11,<2 django-select2<7" 12 | elif [ $DJANGO_VERSION = 2.0 ]; then DEPS="$DEPS django>=2.0,<2.1 django-select2<7.2" 13 | elif [ $DJANGO_VERSION = 2.1 ]; then DEPS="$DEPS django>=2.1,<3 django-select2<7.2" 14 | elif [ $DJANGO_VERSION = 3.0 ]; then DEPS="$DEPS django>=3.0,<3.1 django-select2<7.5" 15 | elif [ $DJANGO_VERSION = 3.1 ]; then DEPS="$DEPS django>=3.1,<4 django-select2" 16 | elif [ $DJANGO_VERSION = 4.0 ]; then DEPS="$DEPS django>=4.0,<4.1 django-select2" 17 | elif [ $DJANGO_VERSION = 4.1 ]; then DEPS="$DEPS django>=4.1,<5 django-select2" 18 | else echo "Unknown Django version $DJANGO_VERSION"; exit 1 19 | fi 20 | 21 | DEPS="$(echo "$DEPS" | tr ' ' '\n')" 22 | if [ ! $SELECT2 = true ]; then DEPS="$(echo "$DEPS" | grep -v select2)"; fi 23 | if [ ! $BROWSER = true ]; then DEPS="$(echo "$DEPS" | grep -v splinter)"; fi 24 | echo "$DEPS" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/_build/ 2 | __pycache__ 3 | *.pyc 4 | *.egg-info 5 | .tox 6 | .coverage 7 | .cache 8 | test_project/db.sqlite3 9 | dist/ 10 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2.1.0 (Feb 20, 2022) 2 | -------------------- 3 | Update to Django 4. 4 | 5 | Remove empty label duplicates with select2. 6 | 7 | 2.0.2 (May 3, 2020) 8 | ------------------- 9 | Fix shared widget state (e.g., error messages) between multiple forms/widget instances. 10 | 11 | 2.0.1 (Dec 7, 2019) 12 | -------------------- 13 | Add Django 3.0 support 14 | 15 | 2.0.0 (Sep 6, 2016) 16 | -------------------- 17 | New feature: Edit-related buttons next to add-another buttons, to allow for quick 18 | modification of related objects in a popup. 19 | 20 | **Backwards incompatible:** ``PopupMixin`` 21 | has been renamed to ``CreatePopupMixin`` without a deprecation path. (Olivier Dalang) 22 | 23 | 1.0.0 (Mar 17, 2016) 24 | -------------------- 25 | Rewrite/merge with @yourlabs implementation. Not backwards compatible! 26 | 27 | 0.1.2 (Oct 22, 2015) 28 | -------------------- 29 | Initial release 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jonas Haag , James Pic . 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include django_addanother *.js 2 | recursive-include django_addanother *.html 3 | recursive-include django_addanother *.css 4 | recursive-exclude test_project * 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/jonashaag/django-addanother.svg?branch=master 2 | :target: https://travis-ci.org/jonashaag/django-addanother 3 | 4 | django-addanother provides you with add-another buttons for forms outside the Django administration interface. It also provides an optional integration with django-select2_. 5 | 6 | See documentation_ for details. 7 | 8 | django-addanother has been extracted from django-autocomplete-light v2, after noticing that other apps copied the same code, to ease community maintenance. 9 | 10 | .. _django-select2: http://django-select2.readthedocs.org/ 11 | .. _documentation: http://django-addanother.readthedocs.org/ 12 | -------------------------------------------------------------------------------- /django_addanother/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (2, 0, 0) 2 | -------------------------------------------------------------------------------- /django_addanother/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashaag/django-addanother/e5c6c8c7461493e0e4594aff1fa2658e6ffce963/django_addanother/contrib/__init__.py -------------------------------------------------------------------------------- /django_addanother/contrib/select2.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | import django_select2.forms # NOQA 4 | import django_addanother.widgets # NOQA 5 | 6 | 7 | def _gen_classes(globals_, locals_): 8 | wrapper_classes = [ 9 | 'AddAnotherWidgetWrapper', 10 | 'EditSelectedWidgetWrapper', 11 | 'AddAnotherEditSelectedWidgetWrapper', 12 | ] 13 | select2_widgets = [ 14 | 'Select2Widget', 15 | 'HeavySelect2Widget', 16 | 'Select2MultipleWidget', 17 | 'HeavySelect2MultipleWidget', 18 | 'HeavySelect2TagWidget', 19 | 'ModelSelect2Widget', 20 | 'ModelSelect2MultipleWidget', 21 | 'ModelSelect2TagWidget', 22 | ] 23 | cls_template = textwrap.dedent(''' 24 | class {new_widget_cls}({widget_cls}): 25 | """:class:`~{widget_cls}` wrapped to remove empty option duplication.""" 26 | 27 | def optgroups(self, name, value, attrs=None): 28 | optgroups = super({new_widget_cls}, self).optgroups( 29 | name, value, attrs=attrs) 30 | if not self.is_required and not self.allow_multiple_selected: 31 | # In this case select2 widget adds one more option. 32 | # We can just drop it now 33 | optgroups = optgroups[1:] 34 | return optgroups 35 | 36 | class {new_cls_name}({wrapper_cls}): 37 | """:class:`~{widget_cls}` wrapped with :class:`~{wrapper_cls}`.""" 38 | def __init__(self, *args, **kwargs): 39 | super({new_cls_name}, self).__init__({new_widget_cls}, *args, **kwargs) 40 | ''') 41 | 42 | for wrapper_cls in wrapper_classes: 43 | for widget_cls in select2_widgets: 44 | new_cls_name = widget_cls[:-len("Widget")] + wrapper_cls[:-len("WidgetWrapper")] 45 | code = cls_template.format( 46 | new_cls_name=new_cls_name, 47 | widget_cls="django_select2.forms.%s" % widget_cls, 48 | new_widget_cls="Fixed%s" % widget_cls, 49 | wrapper_cls="django_addanother.widgets.%s" % wrapper_cls, 50 | ) 51 | exec(code, globals_, locals_) 52 | yield new_cls_name 53 | 54 | 55 | __all__ = list(_gen_classes(globals(), locals())) 56 | 57 | -------------------------------------------------------------------------------- /django_addanother/static/django_addanother/addanother.css: -------------------------------------------------------------------------------- 1 | /* copied from admin/css/widgets.css */ 2 | 3 | .related-widget-wrapper-link { 4 | opacity: 0.3; 5 | } 6 | 7 | .related-widget-wrapper-link:link { 8 | opacity: .8; 9 | } -------------------------------------------------------------------------------- /django_addanother/static/django_addanother/django_jquery.js: -------------------------------------------------------------------------------- 1 | if (typeof django == 'undefined') django = {}; 2 | if (typeof django.jQuery == 'undefined') django.jQuery = $; 3 | 4 | /* this function is defined in admin/jsi18n, if this wasn't loaded, we define it */ 5 | if (typeof interpolate == 'undefined'){ 6 | interpolate = function(fmt, obj, named) { 7 | if (named) { 8 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); 9 | } else { 10 | return fmt.replace(/%s/g, function(match){return String(obj.shift())}); 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /django_addanother/templates/django_addanother/related_widget_wrapper.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 24 | -------------------------------------------------------------------------------- /django_addanother/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | import django 5 | from django.contrib.admin.options import IS_POPUP_VAR 6 | from django.template.response import SimpleTemplateResponse 7 | try: 8 | from django.utils.encoding import force_text 9 | except ImportError: 10 | from django.utils.encoding import force_str as force_text 11 | 12 | 13 | PY2 = sys.version_info[0] == 2 14 | 15 | 16 | def text_type(value): 17 | return unicode(value) if PY2 else str(value) 18 | 19 | 20 | class BasePopupMixin(object): 21 | """Base mixin for generic views classes that handles the case of the view 22 | being opened in a popup window. 23 | 24 | Don't call this directly, use some of the subclasses instead. 25 | 26 | .. versionadded:: 2.0.0 27 | Factored from the original ``PopupMixin`` class. 28 | """ 29 | 30 | def is_popup(self): 31 | return self.request.GET.get(IS_POPUP_VAR, False) 32 | 33 | def form_valid(self, form): 34 | if self.is_popup(): 35 | # If this view is only used with addanother, never as a standalone, 36 | # then the user may not have set a success url, which causes an 37 | # ImproperlyConfigured error. (We never use the success url for the 38 | # addanother popup case anyways, since we always directly close the 39 | # popup window.) 40 | self.success_url = '/' 41 | response = super(BasePopupMixin, self).form_valid(form) 42 | if self.is_popup(): 43 | return self.respond_script(self.object) 44 | else: 45 | return response 46 | 47 | def respond_script(self, created_obj): 48 | ctx = { 49 | 'action': self.POPUP_ACTION, 50 | 'value': text_type(self._get_created_obj_pk(created_obj)), 51 | 'obj': text_type(self.label_from_instance(created_obj)), 52 | 'new_value': text_type(self._get_created_obj_pk(created_obj)) 53 | } 54 | if django.VERSION >= (1, 10): 55 | ctx = {'popup_response_data': json.dumps(ctx)} 56 | return SimpleTemplateResponse('admin/popup_response.html', ctx) 57 | 58 | def _get_created_obj_pk(self, created_obj): 59 | pk_name = created_obj._meta.pk.attname 60 | return created_obj.serializable_value(pk_name) 61 | 62 | def label_from_instance(self, related_instance): 63 | """Return the label to show in the "main form" for the 64 | newly created object. 65 | 66 | Overwrite this to customize the label that is being shown. 67 | """ 68 | return force_text(related_instance) 69 | 70 | 71 | class CreatePopupMixin(BasePopupMixin): 72 | """Mixin for :class:`~django.views.generic.edit.CreateView` classes that 73 | handles the case of the view being opened in an add-another popup window. 74 | 75 | .. versionchanged:: 2.0.0 76 | This used to be called ``PopupMixin`` and has been renamed with the 77 | introduction of edit-related buttons and :class:`UpdatePopupMixin`. 78 | """ 79 | 80 | POPUP_ACTION = 'add' 81 | 82 | 83 | class UpdatePopupMixin(BasePopupMixin): 84 | """Mixin for :class:`~django.views.generic.edit.UpdateView` classes that 85 | handles the case of the view being opened in an edit-related popup window. 86 | 87 | .. versionadded:: 2.0.0 88 | """ 89 | 90 | POPUP_ACTION = 'change' 91 | -------------------------------------------------------------------------------- /django_addanother/widgets.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import django 4 | from django import forms 5 | from django.contrib.admin.views.main import IS_POPUP_VAR 6 | from django.template.loader import render_to_string 7 | from django.utils.safestring import mark_safe 8 | 9 | 10 | if django.VERSION < (1, 9): 11 | DEFAULT_ADD_ICON = 'admin/img/icon_addlink.gif' 12 | DEFAULT_EDIT_ICON = 'admin/img/icon_changelink.gif' 13 | else: 14 | DEFAULT_ADD_ICON = 'admin/img/icon-addlink.svg' 15 | DEFAULT_EDIT_ICON = 'admin/img/icon-changelink.svg' 16 | 17 | 18 | # Most of the wrapper code that follows is copied/inspired by Django's 19 | # RelatedFieldWidgetWrapper. 20 | 21 | class WidgetWrapperMixin(object): 22 | @property 23 | def is_hidden(self): 24 | return self.widget.is_hidden 25 | 26 | @property 27 | def media(self): 28 | return self.widget.media 29 | 30 | def build_attrs(self, extra_attrs=None, **kwargs): 31 | "Helper function for building an attribute dictionary." 32 | self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs) 33 | return self.attrs 34 | 35 | def value_from_datadict(self, data, files, name): 36 | return self.widget.value_from_datadict(data, files, name) 37 | 38 | def id_for_label(self, id_): 39 | return self.widget.id_for_label(id_) 40 | 41 | 42 | class BaseRelatedWidgetWrapper(WidgetWrapperMixin, forms.Widget): 43 | """Basis for the specialised wrappers below. 44 | 45 | Don't call this directly, use some of the subclasses instead. 46 | """ 47 | 48 | #: The template that is used to render the add-another button. 49 | #: Overwrite this to customize the rendering. 50 | template = 'django_addanother/related_widget_wrapper.html' 51 | 52 | class Media: 53 | css = { 54 | 'all': ('django_addanother/addanother.css',) 55 | } 56 | js = ( 57 | 'django_addanother/django_jquery.js', 58 | 'admin/js/admin/RelatedObjectLookups.js', 59 | ) 60 | if django.VERSION < (1, 9): 61 | # This is part of "RelatedObjectLookups.js" in Django 1.9 62 | js += ('admin/js/related-widget-wrapper.js',) 63 | 64 | def __init__(self, widget, add_related_url, 65 | edit_related_url, add_icon=None, edit_icon=None): 66 | if isinstance(widget, type): 67 | widget = widget() 68 | if add_icon is None: 69 | add_icon = DEFAULT_ADD_ICON 70 | if edit_icon is None: 71 | edit_icon = DEFAULT_EDIT_ICON 72 | self.widget = widget 73 | self.attrs = widget.attrs 74 | self.add_related_url = add_related_url 75 | self.add_icon = add_icon 76 | self.edit_related_url = edit_related_url 77 | self.edit_icon = edit_icon 78 | 79 | def __deepcopy__(self, memo): 80 | obj = super(BaseRelatedWidgetWrapper, self).__deepcopy__(memo) 81 | obj.widget = copy.deepcopy(self.widget) 82 | return obj 83 | 84 | def render(self, name, value, *args, **kwargs): 85 | self.widget.choices = self.choices 86 | 87 | url_params = "%s=%s" % (IS_POPUP_VAR, 1) 88 | context = { 89 | 'widget': self.widget.render(name, value, *args, **kwargs), 90 | 'name': name, 91 | 'url_params': url_params, 92 | 'add_related_url': self.add_related_url, 93 | 'add_icon': self.add_icon, 94 | 'edit_related_url': self.edit_related_url, 95 | 'edit_icon': self.edit_icon, 96 | } 97 | return mark_safe(render_to_string(self.template, context)) 98 | 99 | 100 | class AddAnotherWidgetWrapper(BaseRelatedWidgetWrapper): 101 | """Widget wrapper that adds an add-another button next to the original widget.""" 102 | 103 | def __init__(self, widget, add_related_url, add_icon=None): 104 | super(AddAnotherWidgetWrapper, self).__init__( 105 | widget, add_related_url, None, add_icon, None 106 | ) 107 | 108 | 109 | class EditSelectedWidgetWrapper(BaseRelatedWidgetWrapper): 110 | """Widget wrapper that adds an edit-related button next to the original widget.""" 111 | 112 | def __init__(self, widget, edit_related_url, edit_icon=None): 113 | super(EditSelectedWidgetWrapper, self).__init__( 114 | widget, None, edit_related_url, None, edit_icon 115 | ) 116 | 117 | 118 | class AddAnotherEditSelectedWidgetWrapper(BaseRelatedWidgetWrapper): 119 | """Widget wrapper that adds both add-another and edit-related button 120 | next to the original widget. 121 | """ 122 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-addanother.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-addanother.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-addanother" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-addanother" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-addanother documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Oct 19 16:00:06 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.intersphinx', 37 | 'sphinx.ext.todo', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.mathjax', 40 | 'sphinx.ext.viewcode', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # source_suffix = ['.rst', '.md'] 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | #source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = 'django-addanother' 59 | copyright = '2015, Jonas Haag, James Pic' 60 | author = 'Jonas Haag, James Pic' 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | # The short X.Y version. 67 | version = '0.1' 68 | # The full version, including alpha/beta/rc tags. 69 | release = '0.1.2' 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = None 77 | 78 | # There are two options for replacing |today|: either, you set today to some 79 | # non-false value, then it is used: 80 | #today = '' 81 | # Else, today_fmt is used as the format for a strftime call. 82 | #today_fmt = '%B %d, %Y' 83 | 84 | # List of patterns, relative to source directory, that match files and 85 | # directories to ignore when looking for source files. 86 | exclude_patterns = ['_build'] 87 | 88 | # The reST default role (used for this markup: `text`) to use for all 89 | # documents. 90 | #default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | #add_function_parentheses = True 94 | 95 | # If true, the current module name will be prepended to all description 96 | # unit titles (such as .. function::). 97 | #add_module_names = True 98 | 99 | # If true, sectionauthor and moduleauthor directives will be shown in the 100 | # output. They are ignored by default. 101 | #show_authors = False 102 | 103 | # The name of the Pygments (syntax highlighting) style to use. 104 | pygments_style = 'sphinx' 105 | 106 | # A list of ignored prefixes for module index sorting. 107 | #modindex_common_prefix = [] 108 | 109 | # If true, keep warnings as "system message" paragraphs in the built documents. 110 | #keep_warnings = False 111 | 112 | # If true, `todo` and `todoList` produce output, else they produce nothing. 113 | todo_include_todos = True 114 | 115 | 116 | # -- Options for HTML output ---------------------------------------------- 117 | 118 | # The theme to use for HTML and HTML Help pages. See the documentation for 119 | # a list of builtin themes. 120 | #html_theme = 'alabaster' 121 | 122 | # Theme options are theme-specific and customize the look and feel of a theme 123 | # further. For a list of options available for each theme, see the 124 | # documentation. 125 | #html_theme_options = {} 126 | 127 | # Add any paths that contain custom themes here, relative to this directory. 128 | #html_theme_path = [] 129 | 130 | # The name for this set of Sphinx documents. If None, it defaults to 131 | # " v documentation". 132 | #html_title = None 133 | 134 | # A shorter title for the navigation bar. Default is the same as html_title. 135 | #html_short_title = None 136 | 137 | # The name of an image file (relative to this directory) to place at the top 138 | # of the sidebar. 139 | #html_logo = None 140 | 141 | # The name of an image file (within the static path) to use as favicon of the 142 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 143 | # pixels large. 144 | #html_favicon = None 145 | 146 | # Add any paths that contain custom static files (such as style sheets) here, 147 | # relative to this directory. They are copied after the builtin static files, 148 | # so a file named "default.css" will overwrite the builtin "default.css". 149 | html_static_path = ['_static'] 150 | 151 | # Add any extra paths that contain custom files (such as robots.txt or 152 | # .htaccess) here, relative to this directory. These files are copied 153 | # directly to the root of the documentation. 154 | #html_extra_path = [] 155 | 156 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 157 | # using the given strftime format. 158 | #html_last_updated_fmt = '%b %d, %Y' 159 | 160 | # If true, SmartyPants will be used to convert quotes and dashes to 161 | # typographically correct entities. 162 | #html_use_smartypants = True 163 | 164 | # Custom sidebar templates, maps document names to template names. 165 | #html_sidebars = {} 166 | 167 | # Additional templates that should be rendered to pages, maps page names to 168 | # template names. 169 | #html_additional_pages = {} 170 | 171 | # If false, no module index is generated. 172 | #html_domain_indices = True 173 | 174 | # If false, no index is generated. 175 | #html_use_index = True 176 | 177 | # If true, the index is split into individual pages for each letter. 178 | #html_split_index = False 179 | 180 | # If true, links to the reST sources are added to the pages. 181 | #html_show_sourcelink = True 182 | 183 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 184 | #html_show_sphinx = True 185 | 186 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 187 | #html_show_copyright = True 188 | 189 | # If true, an OpenSearch description file will be output, and all pages will 190 | # contain a tag referring to it. The value of this option must be the 191 | # base URL from which the finished HTML is served. 192 | #html_use_opensearch = '' 193 | 194 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 195 | #html_file_suffix = None 196 | 197 | # Language to be used for generating the HTML full-text search index. 198 | # Sphinx supports the following languages: 199 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 200 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 201 | #html_search_language = 'en' 202 | 203 | # A dictionary with options for the search language support, empty by default. 204 | # Now only 'ja' uses this config value 205 | #html_search_options = {'type': 'default'} 206 | 207 | # The name of a javascript file (relative to the configuration directory) that 208 | # implements a search results scorer. If empty, the default will be used. 209 | #html_search_scorer = 'scorer.js' 210 | 211 | # Output file base name for HTML help builder. 212 | htmlhelp_basename = 'django-addanotherdoc' 213 | 214 | # -- Options for LaTeX output --------------------------------------------- 215 | 216 | latex_elements = { 217 | # The paper size ('letterpaper' or 'a4paper'). 218 | #'papersize': 'letterpaper', 219 | 220 | # The font size ('10pt', '11pt' or '12pt'). 221 | #'pointsize': '10pt', 222 | 223 | # Additional stuff for the LaTeX preamble. 224 | #'preamble': '', 225 | 226 | # Latex figure (float) alignment 227 | #'figure_align': 'htbp', 228 | } 229 | 230 | # Grouping the document tree into LaTeX files. List of tuples 231 | # (source start file, target name, title, 232 | # author, documentclass [howto, manual, or own class]). 233 | latex_documents = [ 234 | (master_doc, 'django-addanother.tex', 'django-addanother Documentation', 235 | 'Jonas Haag', 'manual'), 236 | ] 237 | 238 | # The name of an image file (relative to this directory) to place at the top of 239 | # the title page. 240 | #latex_logo = None 241 | 242 | # For "manual" documents, if this is true, then toplevel headings are parts, 243 | # not chapters. 244 | #latex_use_parts = False 245 | 246 | # If true, show page references after internal links. 247 | #latex_show_pagerefs = False 248 | 249 | # If true, show URL addresses after external links. 250 | #latex_show_urls = False 251 | 252 | # Documents to append as an appendix to all manuals. 253 | #latex_appendices = [] 254 | 255 | # If false, no module index is generated. 256 | #latex_domain_indices = True 257 | 258 | 259 | # -- Options for manual page output --------------------------------------- 260 | 261 | # One entry per manual page. List of tuples 262 | # (source start file, name, description, authors, manual section). 263 | man_pages = [ 264 | (master_doc, 'django-addanother', 'django-addanother Documentation', 265 | [author], 1) 266 | ] 267 | 268 | # If true, show URL addresses after external links. 269 | #man_show_urls = False 270 | 271 | 272 | # -- Options for Texinfo output ------------------------------------------- 273 | 274 | # Grouping the document tree into Texinfo files. List of tuples 275 | # (source start file, target name, title, author, 276 | # dir menu entry, description, category) 277 | texinfo_documents = [ 278 | (master_doc, 'django-addanother', 'django-addanother Documentation', 279 | author, 'django-addanother', 'One line description of project.', 280 | 'Miscellaneous'), 281 | ] 282 | 283 | # Documents to append as an appendix to all manuals. 284 | #texinfo_appendices = [] 285 | 286 | # If false, no module index is generated. 287 | #texinfo_domain_indices = True 288 | 289 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 290 | #texinfo_show_urls = 'footnote' 291 | 292 | # If true, do not generate a @detailmenu in the "Top" node's menu. 293 | #texinfo_no_detailmenu = False 294 | 295 | 296 | # Example configuration for intersphinx: refer to the Python standard library. 297 | intersphinx_mapping = { 298 | 'https://docs.python.org/': None, 299 | 'http://django.readthedocs.io/en/stable/': None, 300 | 'https://django-select2.readthedocs.io/en/stable/': None, 301 | } 302 | 303 | sys.path.append(os.path.dirname(__file__)) 304 | os.environ['DJANGO_SETTINGS_MODULE'] = "django_settings" 305 | -------------------------------------------------------------------------------- /docs/demo.rst: -------------------------------------------------------------------------------- 1 | .. _demo: 2 | 3 | Demo 4 | ==== 5 | 6 | To run the demo, clone the repository, run ``test_project/manage.py migrate`` 7 | and ``test_project/manage.py runserver`` and then go to ``http://localhost:8000``. 8 | -------------------------------------------------------------------------------- /docs/django_settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'docs' 2 | -------------------------------------------------------------------------------- /docs/edit-related.rst: -------------------------------------------------------------------------------- 1 | .. _edit-related: 2 | 3 | Edit-related buttons 4 | ==================== 5 | 6 | Similarly to add-another buttons (see :ref:`usage`), to add edit-related buttons to your widget, proceed with the following steps: 7 | 8 | 1. Wrap your widget with the :class:`~django_addanother.widgets.AddAnotherEditSelectedWidgetWrapper` class, and provide an edit URL in addition to the add URL. 9 | 2. Make your edit view popup-compatible by having it inherit the :class:`~django_addanother.views.CreatePopupMixin` class. 10 | 11 | The edit URL must contain the ``__fk__`` string as a placeholder for the actual object's primary key. Example:: 12 | 13 | # forms.py 14 | from django.core.urlresolvers import reverse_lazy 15 | from django_addanother.widgets import AddAnotherEditSelectedWidgetWrapper 16 | 17 | class FooForm(forms.ModelForm): 18 | class Meta: 19 | ... 20 | widgets = { 21 | 'sender': AddAnotherEditSelectedWidgetWrapper( 22 | forms.Select, 23 | reverse_lazy('person_create'), 24 | reverse_lazy('person_update', args=['__fk__']), 25 | ) 26 | } 27 | 28 | 29 | # views.py 30 | from django_addanother.views import UpdatePopupMixin 31 | 32 | class PersonUpdate(UpdatePopupMixin, UpdateView): 33 | model = Foo 34 | ... 35 | 36 | If you need the edit-related button only, but not the add-another, wrap your widget with the :class:`~django_addanother.widgets.EditSelectedWidgetWrapper` class and remove the add URL. 37 | -------------------------------------------------------------------------------- /docs/howitworks.rst: -------------------------------------------------------------------------------- 1 | How it Works 2 | ============ 3 | .. note:: django-addanother works exactly like the add-another and edit-related features in Django's admin. 4 | 5 | django-addanother works twofold: Firstly, it adds an add-another and an edit-related button next to your form fields. When one of these buttons is clicked, a special popup window with the "inline" creation form is opened. 6 | 7 | The popup window isn't much different from your usual form handling views. The main difference is that when the form has been submitted and validated successfully, after saving the newly created object, the user is not redirected to the view's :attr:`~django.views.generic.edit.FormMixin.success_url`. Instead, a special JavaScript-only response is being sent to the browser, adding the new object to the selection (or modifying the option) in the original window and closing the popup window. 8 | 9 | Any :class:`~django.views.generic.edit.CreateView` or :class:`~django.views.generic.edit.UpdateView` can be made compatible with django-addanother. When opened in a popup, the view gets appended the ``?_popup=1`` GET parameter, which is how the view knows when to respond with its special JavaScript response. This special handling is taken care of in :class:`django_addanother.views.CreatePopupMixin` and :class:`django_addanother.views.UpdatePopupMixin`, whose usage is explained in :ref:`usage`. 10 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-addanother documentation 2 | =============================== 3 | 4 | django-addanother provides you with add-another and edit-related buttons for forms outside the Django administration interface. It also provides an optional integration with django-select2_. 5 | 6 | Supported Django versions: 1.8, 1.9, 1.10 (others may work well though) 7 | 8 | .. _django-select2: http://django-select2.readthedocs.org/ 9 | 10 | Getting started 11 | --------------- 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | installation 16 | demo 17 | usage 18 | edit-related 19 | 20 | 21 | Advanced topics 22 | --------------- 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | select2 27 | howitworks 28 | reference/index 29 | 30 | 31 | 32 | Indices and tables 33 | ================== 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | How to Install 2 | ============== 3 | 4 | 1. ``pip install django-addanother``. 5 | 2. Add ``'django_addanother'`` to your ``INSTALLED_APPS``. 6 | 3. Make sure static ``'django.contrib.staticfiles'`` and ``'django.contrib.admin'`` 7 | are part of your ``INSTALLED_APPS``. 8 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-addanother.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-addanother.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | .. toctree:: 4 | :maxdepth: 1 5 | 6 | views 7 | widgets 8 | select2_widgets 9 | 10 | -------------------------------------------------------------------------------- /docs/reference/select2_widgets.rst: -------------------------------------------------------------------------------- 1 | .. _select2ref: 2 | 3 | Select2 Widgets Reference 4 | ========================= 5 | 6 | ========================================================= ======================================= ========================================= =================================================== 7 | django-select2 Widget ``...AddAnother`` ``...EditSelected`` ``...AddAnotherEditSelected`` 8 | ========================================================= ======================================= ========================================= =================================================== 9 | :class:`~django_select2.forms.Select2Widget` :class:`Select2AddAnother` :class:`Select2EditSelected` :class:`Select2AddAnotherEditSelected` 10 | :class:`~django_select2.forms.HeavySelect2Widget` :class:`HeavySelect2AddAnother` :class:`HeavySelect2EditSelected` :class:`HeavySelect2AddAnotherEditSelected` 11 | :class:`~django_select2.forms.Select2MultipleWidget` :class:`Select2MultipleAddAnother` :class:`Select2MultipleEditSelected` :class:`Select2MultipleAddAnotherEditSelected` 12 | :class:`~django_select2.forms.HeavySelect2MultipleWidget` :class:`HeavySelect2MultipleAddAnother` :class:`HeavySelect2MultipleEditSelected` :class:`HeavySelect2MultipleAddAnotherEditSelected` 13 | :class:`~django_select2.forms.HeavySelect2TagWidget` :class:`HeavySelect2TagAddAnother` :class:`HeavySelect2TagEditSelected` :class:`HeavySelect2TagAddAnotherEditSelected` 14 | :class:`~django_select2.forms.ModelSelect2Widget` :class:`ModelSelect2AddAnother` :class:`ModelSelect2EditSelected` :class:`ModelSelect2AddAnotherEditSelected` 15 | :class:`~django_select2.forms.ModelSelect2MultipleWidget` :class:`ModelSelect2MultipleAddAnother` :class:`ModelSelect2MultipleEditSelected` :class:`ModelSelect2MultipleAddAnotherEditSelected` 16 | :class:`~django_select2.forms.ModelSelect2TagWidget` :class:`ModelSelect2TagAddAnother` :class:`ModelSelect2TagEditSelected` :class:`ModelSelect2TagAddAnotherEditSelected` 17 | ========================================================= ======================================= ========================================= =================================================== 18 | -------------------------------------------------------------------------------- /docs/reference/views.rst: -------------------------------------------------------------------------------- 1 | Views Reference 2 | =============== 3 | .. currentmodule:: django_addanother.views 4 | 5 | .. autoclass:: CreatePopupMixin 6 | :members: 7 | 8 | .. autoclass:: UpdatePopupMixin 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/reference/widgets.rst: -------------------------------------------------------------------------------- 1 | Widgets Reference 2 | ================= 3 | .. currentmodule:: django_addanother.widgets 4 | 5 | .. autoclass:: AddAnotherWidgetWrapper 6 | :members: 7 | 8 | .. autoclass:: EditSelectedWidgetWrapper 9 | :members: 10 | 11 | .. autoclass:: AddAnotherEditSelectedWidgetWrapper 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/select2.rst: -------------------------------------------------------------------------------- 1 | Select2 Integration 2 | =================== 3 | django-addanother provides optional lightweight integration with django-select2_. 4 | 5 | Usage example: 6 | 7 | .. code-block:: python 8 | 9 | from django_addanother.contrib.select2 import Select2AddAnother 10 | 11 | class FooForm(forms.ModelForm): 12 | class Meta: 13 | ... 14 | widgets = { 15 | 'sender': Select2AddAnother(reverse_lazy('person_create')), 16 | } 17 | 18 | 19 | See :ref:`select2ref` for a list of provided widgets. 20 | 21 | .. _django-select2: http://django-select2.readthedocs.org/ 22 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | How to Use 4 | ========== 5 | 6 | 1. Add the add-another button 7 | ------------------------------- 8 | Wrap :class:`django_addanother.widgets.AddAnotherWidgetWrapper` around your widget to show the add-another button next to it. 9 | 10 | For example, let's say we want to add add-another buttons to a model form:: 11 | 12 | from django.urls import reverse_lazy 13 | from django_addanother.widgets import AddAnotherWidgetWrapper 14 | 15 | class FooForm(forms.ModelForm): 16 | class Meta: 17 | ... 18 | widgets = { 19 | 'sender': AddAnotherWidgetWrapper( 20 | forms.Select, 21 | reverse_lazy('person_create'), 22 | ), 23 | 'recipients': AddAnotherWidgetWrapper( 24 | forms.SelectMultiple, 25 | reverse_lazy('person_create'), 26 | ) 27 | } 28 | 29 | This will add an add-another button next to the ``sender`` and ``recipients`` fields. When clicked, these will open the ``'person_create'`` URL in a popup. 30 | 31 | .. important:: 32 | Be sure to include form media and jQuery in your templates: 33 | 34 | .. code-block:: django 35 | 36 | {{ form }} 37 | 38 | {{ form.media }} 39 | 40 | 41 | 2. Make your view popup-compatible 42 | ---------------------------------- 43 | .. note:: This assumes you're using Django's generic :class:`~django.views.generic.edit.CreateView`. django-addanother doesn't support function-based views at the point of writing. You'll have to convert any function-based views to Class Based Views first. 44 | 45 | Making your ``CreateView`` compatible with django-addanother is as simple as making it inherit the :class:`django_addanother.views.CreatePopupMixin` class:: 46 | 47 | from django_addanother.views import CreatePopupMixin 48 | 49 | class PersonCreate(CreatePopupMixin, CreateView): 50 | model = Foo 51 | ... 52 | 53 | This overwrites your view's :meth:`~django.views.generic.edit.FormMixin.form_valid` method to return a special JavaScript response in case a form has been submitted from a popup. 54 | 55 | You may want to hide header, footer and navigation elements for the popups. When the create view is opened in a popup, the ``view.is_popup`` template variable is set: 56 | 57 | .. code-block:: django 58 | 59 | {% if not view.is_popup %} 60 | 61 | {% endif %} 62 | 63 | 64 | 3. Profit 65 | --------- 66 | That's it! 67 | 68 | See :ref:`edit-related` on how to add edit buttons too. 69 | -------------------------------------------------------------------------------- /requirements.readthedocs.txt: -------------------------------------------------------------------------------- 1 | django-select2 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | setup( 7 | name='django_addanother', 8 | version='2.1.0', 9 | author='Jonas Haag, James Pic', 10 | author_email='jonas@lophus.org, jamespic@gmail.com', 11 | packages=find_packages(exclude=['test_project']), 12 | zip_safe=False, 13 | include_package_data=True, 14 | url='https://github.com/jonashaag/django-addanother', 15 | description='"Add another" buttons outside the Django admin', 16 | classifiers=[ 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: ISC License (ISCL)", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 2.7", 21 | "Programming Language :: Python :: 3.5", 22 | "Programming Language :: Python :: 3.6", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashaag/django-addanother/e5c6c8c7461493e0e4594aff1fa2658e6ffce963/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = '_sn_k6)(m!#3*)_)(@5ji2+cl)gc=i)_$bx!flc=9k1l08styo' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = ["localhost"] 17 | 18 | 19 | # Application definition 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 'django_addanother', 29 | 'testapp', 30 | ] 31 | 32 | STATIC_ROOT = 'static' 33 | 34 | MIDDLEWARE = [ 35 | 'django.middleware.security.SecurityMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.middleware.common.CommonMiddleware', 38 | 'django.middleware.csrf.CsrfViewMiddleware', 39 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 40 | 'django.contrib.messages.middleware.MessageMiddleware', 41 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 42 | ] 43 | 44 | ROOT_URLCONF = 'urls' 45 | 46 | TEMPLATES = [ 47 | { 48 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 49 | 'DIRS': [ 50 | os.path.join(BASE_DIR, 'templates'), 51 | ], 52 | 'APP_DIRS': True, 53 | 'OPTIONS': { 54 | 'context_processors': [ 55 | 'django.template.context_processors.debug', 56 | 'django.template.context_processors.request', 57 | 'django.contrib.auth.context_processors.auth', 58 | 'django.contrib.messages.context_processors.messages', 59 | ], 60 | }, 61 | }, 62 | ] 63 | 64 | WSGI_APPLICATION = 'wsgi.application' 65 | 66 | 67 | # Database 68 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 69 | 70 | DATABASES = { 71 | 'default': { 72 | 'ENGINE': 'django.db.backends.sqlite3', 73 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 74 | } 75 | } 76 | 77 | DEFAULT_AUTO_FIELD = 'django.db.models.SmallAutoField' 78 | 79 | # Password validation 80 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 81 | 82 | AUTH_PASSWORD_VALIDATORS = [ 83 | { 84 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 85 | }, 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 94 | }, 95 | ] 96 | 97 | 98 | # Internationalization 99 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 100 | 101 | LANGUAGE_CODE = 'en-us' 102 | 103 | TIME_ZONE = 'UTC' 104 | 105 | USE_I18N = True 106 | 107 | USE_L10N = True 108 | 109 | USE_TZ = True 110 | 111 | 112 | # Static files (CSS, JavaScript, Images) 113 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 114 | 115 | STATIC_URL = '/static/' 116 | 117 | LOGGING = { 118 | 'version': 1, 119 | 'disable_existing_loggers': True, 120 | 'handlers': { 121 | 'console': { 122 | 'level': 'DEBUG', 123 | 'class': 'logging.StreamHandler', 124 | }, 125 | 'file': { 126 | 'level': 'DEBUG', 127 | 'class': 'logging.FileHandler', 128 | 'filename': 'debug.log', 129 | } 130 | }, 131 | 'loggers': { 132 | 'django': { 133 | 'level': 'ERROR', 134 | 'handlers': ['console', 'file'], 135 | 'propagate': False, 136 | }, 137 | }, 138 | } 139 | -------------------------------------------------------------------------------- /test_project/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashaag/django-addanother/e5c6c8c7461493e0e4594aff1fa2658e6ffce963/test_project/testapp/__init__.py -------------------------------------------------------------------------------- /test_project/testapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Player, Team 3 | 4 | admin.site.register(Player) 5 | admin.site.register(Team) -------------------------------------------------------------------------------- /test_project/testapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | try: 3 | from django.core.urlresolvers import reverse_lazy 4 | except ImportError: 5 | from django.urls import reverse_lazy 6 | 7 | from django_addanother.widgets import AddAnotherWidgetWrapper, AddAnotherEditSelectedWidgetWrapper 8 | 9 | from .models import Player 10 | 11 | 12 | class PlayerForm(forms.ModelForm): 13 | class Meta: 14 | model = Player 15 | fields = ['name', 'current_team', 'future_team', 'previous_teams'] 16 | widgets = { 17 | 'current_team': AddAnotherWidgetWrapper( 18 | forms.Select(), 19 | reverse_lazy('add_team'), 20 | ), 21 | 'future_team': AddAnotherEditSelectedWidgetWrapper( 22 | forms.Select, 23 | reverse_lazy('add_team'), 24 | reverse_lazy('edit_team',args=['__fk__']), 25 | ), 26 | 'previous_teams': AddAnotherWidgetWrapper( 27 | forms.SelectMultiple, 28 | reverse_lazy('add_team'), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /test_project/testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-06-08 16:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Player', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=20)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Team', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=20)), 29 | ], 30 | ), 31 | migrations.AddField( 32 | model_name='player', 33 | name='current_team', 34 | field=models.ForeignKey(help_text='This demonstrate the wrapper adding a create button only', on_delete=django.db.models.deletion.CASCADE, related_name='current_players', to='testapp.Team'), 35 | ), 36 | migrations.AddField( 37 | model_name='player', 38 | name='future_team', 39 | field=models.ForeignKey(help_text='This demonstrate the wrapper adding both a create and an edit button', on_delete=django.db.models.deletion.CASCADE, related_name='future_players', to='testapp.Team'), 40 | ), 41 | migrations.AddField( 42 | model_name='player', 43 | name='previous_teams', 44 | field=models.ManyToManyField(help_text='This demonstrate the wrapper on a ManyToMany field', related_name='ancient_players', to='testapp.Team'), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /test_project/testapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashaag/django-addanother/e5c6c8c7461493e0e4594aff1fa2658e6ffce963/test_project/testapp/migrations/__init__.py -------------------------------------------------------------------------------- /test_project/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Team(models.Model): 5 | 6 | name = models.CharField(max_length=20) 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | 12 | class Player(models.Model): 13 | 14 | name = models.CharField(max_length=20) 15 | current_team = models.ForeignKey( 16 | "Team", related_name="current_players", 17 | on_delete=models.CASCADE, 18 | help_text='This demonstrates the wrapper adding an "add" button only' 19 | ) 20 | future_team = models.ForeignKey( 21 | "Team", related_name="future_players", 22 | on_delete=models.CASCADE, 23 | help_text='This demonstrates the wrapper adding both an "add" and an "edit" button' 24 | ) 25 | previous_teams = models.ManyToManyField( 26 | "Team", related_name="ancient_players", 27 | help_text="This demonstrates the wrapper on a ManyToMany field" 28 | ) 29 | 30 | def __str__(self): 31 | return self.name 32 | -------------------------------------------------------------------------------- /test_project/testapp/templates/testapp/main.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load static %} 3 | 4 | {% block branding %} 5 |

django-addanother demo

6 | {% endblock %} 7 | 8 | {% block extrahead %} 9 | 10 | {{ form.media }} 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 |

16 | django-addanother is a small app to enable the "add another" popup 17 | feature outside Django's admin. 18 |

19 |

20 | If you want to replace select fields with autocompletes, try 21 | django-autocomplete-light. 22 |

23 | 24 |

AddAnotherWidgetWrapper

25 |
26 | 27 | {{ form.as_table }} 28 |
29 | {% csrf_token %} 30 | 31 |
32 | 33 |
34 |
35 |

Need help?

36 |

37 | If you can reproduce a bug in the demo project, it will be made a 38 | priority to fix it, and add regression test to prevent it from occuring 39 | again in the future. In this case, please clone the 40 | GitHub repository, configure the simplest example you can in a new app in 44 | test_project and submit your pull request. 45 |

46 |

47 | The easier you make it for us to reproduce your issue in an isolated 48 | use-case, the best are your chances to get a satisfying answer. 49 |

50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /test_project/testapp/templates/testapp/team_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load static %} 3 | 4 | {% block branding %} 5 |

django-addanother demo

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 12 | {{ form.as_table }} 13 |
14 | {% csrf_token %} 15 | 16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /test_project/testapp/tests.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import pytest 3 | from django import forms 4 | from testapp.models import Team, Player 5 | from testapp.forms import PlayerForm 6 | 7 | 8 | def test_import(): 9 | import django_addanother.widgets 10 | import django_addanother.views 11 | import django_addanother.contrib 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_widget_deepcopy(): 16 | form1 = PlayerForm() 17 | form2 = PlayerForm() 18 | assert form1.fields['current_team'].widget.widget is not form2.fields['current_team'].widget.widget 19 | assert form1.fields['future_team'].widget.widget is not form2.fields['future_team'].widget.widget 20 | 21 | 22 | @pytest.mark.django_db 23 | def test_smoke_select2(): 24 | """Some basic tests to verify the select2 integration works.""" 25 | from django_addanother.contrib import select2 as da_select2 26 | for widget_cls_name in da_select2.__all__: 27 | if 'Heavy' in widget_cls_name: 28 | # Need extra select2 specific arguments 29 | continue 30 | widget_cls = getattr(da_select2, widget_cls_name) 31 | if 'AddAnotherEditSelected' in widget_cls_name: 32 | widget = widget_cls(add_related_url='x', edit_related_url='x', add_icon='x', edit_icon='x') 33 | elif 'AddAnother' in widget_cls_name: 34 | widget = widget_cls(add_related_url='x', add_icon='x') 35 | else: 36 | widget = widget_cls(edit_related_url='x', edit_icon='x') 37 | widget.widget.data_url = 'fake-url' 38 | widget.widget.queryset = Player.objects.all() 39 | 40 | class TestForm(forms.ModelForm): 41 | class Meta: 42 | model = Player 43 | fields = ['current_team'] 44 | widgets = {'current_team': widget} 45 | 46 | html = TestForm().as_p() 47 | 48 | # https://github.com/jonashaag/django-addanother/pull/47 49 | assert not '