├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── demo ├── demo │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── library │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20170919_0319.py │ │ ├── 0003_book_isbn.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── library │ │ │ ├── book_detail.html │ │ │ ├── book_detail_inner.html │ │ │ ├── form.html │ │ │ └── rating.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py └── templates │ ├── base.html │ ├── home.html │ └── registration │ ├── logged_out.html │ ├── login.html │ ├── password_change_done.html │ ├── password_change_form.html │ ├── password_reset_complete.html │ ├── password_reset_confirm.html │ ├── password_reset_done.html │ ├── password_reset_email.html │ └── password_reset_form.html ├── docs ├── Makefile ├── conf.py ├── demo.rst ├── howto.rst ├── index.rst ├── make.bat ├── quickstart.rst ├── reference.rst └── settings.rst ├── manage.py ├── popupcrud.code-workspace ├── popupcrud ├── __init__.py ├── static │ └── popupcrud │ │ ├── css │ │ └── popupcrud.css │ │ ├── img │ │ └── sorting-icons.svg │ │ └── js │ │ ├── jquery.formset.js │ │ └── popupcrud.js ├── templates │ └── popupcrud │ │ ├── 403.html │ │ ├── _pagination.html │ │ ├── confirm_delete.html │ │ ├── detail.html │ │ ├── detail_inner.html │ │ ├── empty_list.html │ │ ├── form.html │ │ ├── form_inner.html │ │ ├── list.html │ │ ├── list_content.html │ │ └── modal.html ├── templatetags │ ├── __init__.py │ ├── bsmodal.py │ └── popupcrud_list.py ├── views.py └── widgets.py ├── requirements.dev.txt ├── requirements.txt ├── setup.py ├── test ├── __init__.py ├── models.py ├── templates │ └── test │ │ └── base.html ├── tests.py ├── urls.py └── views.py ├── testsettings.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # sqlite db 7 | db.sqlite* 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # python virtualenv 107 | pyenv 108 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Python: Attach", 10 | "type": "python", 11 | "request": "attach", 12 | "localRoot": "${workspaceFolder}", 13 | "remoteRoot": "${workspaceFolder}", 14 | "port": 3000, 15 | "secret": "my_secret", 16 | "host": "localhost" 17 | }, 18 | { 19 | "name": "Python: Terminal (integrated)", 20 | "type": "python", 21 | "request": "launch", 22 | "stopOnEntry": true, 23 | "pythonPath": "${config:python.pythonPath}", 24 | "program": "${file}", 25 | "cwd": "", 26 | "console": "integratedTerminal", 27 | "env": {}, 28 | "envFile": "${workspaceFolder}/.env", 29 | "debugOptions": [], 30 | "internalConsoleOptions": "neverOpen" 31 | }, 32 | { 33 | "name": "Python: Terminal (external)", 34 | "type": "python", 35 | "request": "launch", 36 | "stopOnEntry": true, 37 | "pythonPath": "${config:python.pythonPath}", 38 | "program": "${file}", 39 | "cwd": "", 40 | "console": "externalTerminal", 41 | "env": {}, 42 | "envFile": "${workspaceFolder}/.env", 43 | "debugOptions": [], 44 | "internalConsoleOptions": "neverOpen" 45 | }, 46 | { 47 | "name": "Python: Django Shell", 48 | "type": "python", 49 | "request": "launch", 50 | "stopOnEntry": true, 51 | "pythonPath": "${config:python.pythonPath}", 52 | "program": "${workspaceFolder}/demo/manage.py", 53 | "cwd": "${workspaceFolder}/demo/", 54 | "args": [ 55 | "shell", 56 | "--settings", 57 | "demo.settings", 58 | ], 59 | "env": {}, 60 | "envFile": "${workspaceFolder}/.env", 61 | "debugOptions": [ 62 | "RedirectOutput", 63 | ] 64 | }, 65 | { 66 | "name": "Python: Django Server", 67 | "type": "python", 68 | "request": "launch", 69 | "stopOnEntry": true, 70 | "pythonPath": "${config:python.pythonPath}", 71 | "program": "${workspaceFolder}/demo/manage.py", 72 | "cwd": "${workspaceFolder}/demo/", 73 | "args": [ 74 | "runserver", 75 | "--settings", 76 | "demo.settings", 77 | "--noreload", 78 | "--nothreading" 79 | ], 80 | "env": {}, 81 | "envFile": "${workspaceFolder}/.env", 82 | "debugOptions": [ 83 | "RedirectOutput", 84 | "DjangoDebugging" 85 | ] 86 | } 87 | ] 88 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/hari/.virtualenvs/qqden/bin/python", 3 | "python.linting.pylintArgs": [ 4 | "--load-plugins=pylint_django", 5 | "--disable=C0103, C0301, C0111, R0901, R0903, R0904, R0201, W0613" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "runserver", 8 | "type": "shell", 9 | "command": "${config:python.pythonPath}", 10 | "args": [ 11 | "${workspaceFolder}/demo/manage.py", 12 | "runserver", 13 | "--settings", 14 | "demo.settings" 15 | ], 16 | "problemMatcher": [ 17 | "$eslint-compact" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ------- 3 | 4 | 0.1.0 (2017-09-25) 5 | ++++++++++++++++++ 6 | 7 | * Initial release 8 | 9 | 0.1.2 (2017-09-26) 10 | ++++++++++++++++++ 11 | 12 | * Merge Quickstart section into README 13 | 14 | 0.1.3 (2017-09-26) 15 | ++++++++++++++++++ 16 | 17 | * Add missing HISTORY.rst to manifst 18 | 19 | 0.1.4 (2017-09-26) 20 | ++++++++++++++++++ 21 | 22 | * Support for ``order_field`` attribute for ``list_display`` method fields. 23 | This works similar to ``ModelAdmin`` method fields' ``admin_order_field`` 24 | property. 25 | 26 | 0.1.5 (2017-09-26) 27 | ++++++++++++++++++ 28 | 29 | * Better unicode support 30 | 31 | 0.1.6 (2017-09-27) 32 | ++++++++++++++++++ 33 | 34 | * Better access control support through 'login_url' & 'raise_exception' 35 | PopupCrudViewSet properties 36 | 37 | 0.1.7 (2017-10-13) 38 | ++++++++++++++++++ 39 | 40 | * Object detail view support 41 | 42 | 0.1.8 (2017-10-16) 43 | ++++++++++++++++++ 44 | 45 | * Add PopupCrudViewSet.urls() -- a single method to return all the CRUD urls 46 | that can be added to urlpatterns[]. 47 | * When related object popup is activated on a multiselect select and it adds a 48 | new object, the object is added to the existing list of selections. (old code 49 | used to replace all the current selections with the newly added item) 50 | * Insert all form media into ListView through ListView.media property. 51 | * Fix broken support for django-select2 in modals by setting control's 52 | dropdownParent to the modal (rather than parent window) 53 | * Use the 'create-edit-modal' modal as the template for secondary modals 54 | activated through related-model modal popups. This ensures consistent modal 55 | look and feel if the user customized the modal template by overriding 56 | popupcrud/modal.html template. 57 | * Fix ALLOWED_HOSTS in settings - issue #1 58 | 59 | 0.2.0 (2017-10-18) 60 | ++++++++++++++++++ 61 | * Bumping minor version as reflection of new features legacy_crud dict, media 62 | & out-of-the-box django_select2 support in previous release 63 | * Added 'crudform.ready' JavaScript event, which is triggered when 64 | create/update form is activated. This event provides clients an uniform way to 65 | apply their own optional initialization code to the CRUD forms. 66 | * Added 6 more tests to cover new legacy_crud dict value support & form media 67 | injection. 68 | 69 | 0.3.0 (2017-10-26) 70 | ++++++++++++++++++ 71 | * List view content is rendered in its own block, popupcrud_list, in the 72 | template file. This allows the list content to be relocated to different 73 | parts of the base template. 74 | * Add ViewSet.empty_list_icon and ViewSet.empty_list_message properties. These 75 | properties provide for prettier rendering of empty table states. 76 | 77 | 0.3.1 (2017-10-26) 78 | ++++++++++++++++++ 79 | * Use custom style for empty-list-state icon sizing. Earlier code was using font 80 | awesome style. 81 | 82 | 0.4.0 (2017-11-2) 83 | +++++++++++++++++ 84 | * Breadcrumbs support 85 | * ListView queryset custom filtering through ``PopupCrudViewSet.get_queryset()`` 86 | * Support custom form init args through ``PopupCrudViewSet.get_form_kwargs()`` 87 | * ``PopupCrudViewSet.new_url`` and ``PopupCrudViewSet.list_url`` are determined 88 | through ``PopupCrudViewSet.get_new_url()`` and 89 | ``PopupCrudViewSet.get_list_url()`` throughout the code. 90 | 91 | 0.4.1 (2017-11-6) 92 | +++++++++++++++++ 93 | * Fix an issue where when form with errors is rendered select2 and add-related 94 | widgets are not bound correctly 95 | 96 | 0.5.0 (2017-11-10) 97 | ++++++++++++++++++ 98 | * Add custom item action support 99 | * Clean up JavaScript by encapsulating all methods in its own namespace & 100 | reducing code duplication 101 | * Add missing CSS styles to popupcrud.css 102 | * Empty_list_message class variable now allows embedded html tags (value is 103 | wrapped in mark_safe() before placing in template context) 104 | 105 | 0.6.0 (2018-03-15) 106 | ++++++++++++++++++ 107 | * Add formset support in CRUD create/update views 108 | * Add size option to bsmodal template tags 109 | * Fixes to some minor bugs 110 | 111 | 0.6.1 (2018-03-16) 112 | ++++++++++++++++++ 113 | * Make formset alignment consistent with bootstrap3 settings 114 | horizontal_label_class & horizontal_field_class. 115 | 116 | 0.6.2 (2018-03-17) 117 | ++++++++++++++++++ 118 | * Fix bug where forms with m2m fields were not saved 119 | * Reflect formset form field 'required' status in field column header 120 | * Make formsets work in legacy crud mode 121 | * django-select2 support in formset forms 122 | * Minor formset layout formatting improvements 123 | 124 | 0.6.3 (2018-03-18) 125 | ++++++++++++++++++ 126 | * Fix incorrect formset detection logic 127 | 128 | 0.6.4 (2018-03-26) 129 | ++++++++++++++++++ 130 | * Optimize listview media when create & edit are set to legacy 131 | * Breadcrumbs obeys custom page title 132 | * Fix bug in ListView.media optimization 133 | * Introduce permissions_required attribute 134 | * PopupCrudViewSet.get_page_title now used in for all CRUD(legacy) views 135 | 136 | 0.7.0 (2018-06-20) 137 | ++++++++++++++++++ 138 | * Add support for ``pk_url_kwarg``, ``slug_field``, ``slug_url_kwarg`` & 139 | ``context_object_name`` ViewSet attributes. 140 | * Improve documentation 141 | 142 | 0.7.1 (2018-06-20) 143 | ++++++++++++++++++ 144 | * Update release history 145 | 146 | 0.8.0 (2018-10-31) 147 | ++++++++++++++++++ 148 | * Allow html tags in custom column headers; hide Action column if there're 149 | no item actions 150 | * Support view template context data in ViewSet 151 | 152 | 0.9.0 (2019-12-25) 153 | ++++++++++++++++++ 154 | * Django 3.0 support 155 | 156 | 0.10.0 (2019-12-26) 157 | +++++++++++++++++++ 158 | * Fix rendering bugs owing to changes in Django 3.0 159 | 160 | 0.11.0 (2019-12-26) 161 | +++++++++++++++++++ 162 | * Bump min Django ver to 2.2.8 163 | 164 | 0.12.0 (2019-12-26) 165 | +++++++++++++++++++ 166 | * Fix README formatting errors -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017 Hari Mahadevan(何瑞理) 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include HISTORY.rst 4 | recursive-include popupcrud * 5 | global-exclude *.pyc 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | django-popupcrud 3 | ================ 4 | 5 | A CRUD framework leveraging Django's generic views that implements CRUD 6 | operations through HTML popups. 7 | 8 | .. image:: https://img.shields.io/pypi/v/django-popupcrud.svg 9 | :target: https://pypi.python.org/pypi/django-popupcrud 10 | :alt: Latest PyPI version 11 | 12 | .. image:: https://img.shields.io/pypi/dm/django-popupcrud.svg 13 | :target: https://pypi.python.org/pypi/django-popupcrud 14 | :alt: Number of PyPI downloads per month 15 | 16 | Requirements 17 | ------------ 18 | 19 | - Python >= 3.4 20 | - Django >= 2.2.8 21 | - django-bootstrap3 22 | - django-pure-pagination 23 | 24 | Documentation 25 | ------------- 26 | 27 | Available at `django-popupcrud.readthedocs.io 28 | `_. 29 | 30 | Quickstart 31 | ---------- 32 | 33 | 1. Install ``django-popupcrud`` using pip: 34 | 35 | ``pip install django-popupcrud`` 36 | 37 | Or install it directly from the source repository: 38 | 39 | ``pip install git+https://github.com/harikvpy/django-popupcrud.git`` 40 | 41 | Yet another way would be to clone this repository and install from the cloned 42 | root folder via ``pip install -e .``. 43 | 44 | 2. Install the dependencies - ``django-bootstrap3`` and 45 | ``django-pure-pagination``. Add the dependencies and ``popupcrud`` to 46 | ``INSTALLED_APPS`` in your project's ``settings.py``:: 47 | 48 | INSTALLED_APPS = [ 49 | ... 50 | 'bootstrap3', 51 | 'pure_pagination', 52 | 'popupcrud', 53 | ... 54 | ] 55 | 56 | 3. Let ``PopupCrudViewSet`` know of your base template file name. This defaults 57 | to ``base.html``, but if your project uses a different base template 58 | filename, inform ``PopupCrudViewSet`` about it in ``settings.py``:: 59 | 60 | POPUPCRUD = { 61 | 'base_template': 'mybase.html', 62 | } 63 | 64 | Include Bootstrap CSS & JS resources in this base template. 65 | If you were to use ``django-bootstrap3`` tags for these, your base 66 | template should look something like this:: 67 | 68 | 69 | {% bootstrap_css %} 70 | 71 | {% bootstrap_javascript %} 72 | {% block extrahead %}{% endblock extrahead %} 73 | 74 | 75 | Also, define a block named ``extrahead`` within the ```` element. 76 | ``PopupCrudViewSet`` views use a few custom CSS styles to show column 77 | sorting options and sort priority. These styles are defined in 78 | ``static/popupcrud/css/popupcrud.css`` which is inserted into 79 | the ``extrahead`` block. If you don't declare this block, 80 | you will have to explicitly load the stylesheet into your base template. 81 | 82 | 4. In your app's ``views.py``, create a ``ViewSet`` for each model for which you 83 | want to support CRUD operations. 84 | 85 | Models.py:: 86 | 87 | from django.db import models 88 | 89 | class Author(models.Model): 90 | name = models.CharField("Name", max_length=128) 91 | penname = models.CharField("Pen Name", max_length=128) 92 | age = models.SmallIntegerField("Age", null=True, blank=True) 93 | 94 | class Meta: 95 | ordering = ('name',) 96 | verbose_name = "Author" 97 | verbose_name_plural = "Authors" 98 | 99 | def __str__(self): 100 | return self.name 101 | 102 | Views.py:: 103 | 104 | from django.core.urlresolvers import reverse_lazy, reverse 105 | from popupcrud.views import PopupCrudViewSet 106 | 107 | class AuthorViewSet(PopupCrudViewSet): 108 | model = Author 109 | fields = ('name', 'penname', 'age') 110 | list_display = ('name', 'penname', 'age') 111 | list_url = reverse_lazy("library:authors:list") 112 | new_url = reverse_lazy("library:authors:create") 113 | 114 | def get_edit_url(self, obj): 115 | return reverse("library:authors:update", kwargs={'pk': obj.pk}) 116 | 117 | def get_delete_url(self, obj): 118 | return reverse("library:authors:delete", kwargs={'pk': obj.pk}) 119 | 120 | 5. Wire up the CRUD views generated by the viewset to the URLconf:: 121 | 122 | urlpatterns= [ 123 | url(r'^authors/', views.AuthorCrudViewset.urls()), 124 | ] 125 | 126 | This will register the following urls: 127 | 128 | * ``authors/`` - list view 129 | * ``authors/create/`` - create view 130 | * ``authors//`` - detail view 131 | * ``authors//update/`` - update view 132 | * ``authors//delete/`` - delete view 133 | 134 | The urls are registered under its own namespace, which defaults to the 135 | model's ``verbose_name_plural`` meta value. 136 | 137 | 6. Thats it! Your modern HTML popup based CRUD for your table is up and running. 138 | 139 | PopupCrudViewSet has many options to customize the fields displayed in list 140 | view, form used for create/update operations, permission control and more. 141 | Refer to the Reference and How-to sections of the documentation for more 142 | details. 143 | 144 | License 145 | ------- 146 | Distributed under BSD 3-Clause License. See `LICENSE 147 | `_ file for details. 148 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harikvpy/django-popupcrud/08d212aecee5401206e5206d5f39912af82c1b1d/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | import sys 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | PACKAGE_DIR = os.path.dirname(BASE_DIR) 20 | if PACKAGE_DIR not in sys.path: 21 | sys.path.insert(0, PACKAGE_DIR) 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = '%llej&p5fuljmvgm=jzl31l0g(ogw8j&m#e7xp(tqdmmguizdr' 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | 32 | ALLOWED_HOSTS = ['localhost', '127.0.0.1'] 33 | 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | 'bootstrap3', 45 | 'pure_pagination', 46 | 47 | 'popupcrud', 48 | 'library', 49 | ] 50 | 51 | try: 52 | import django_select2 53 | INSTALLED_APPS += [ 54 | 'django_select2' 55 | ] 56 | except ImportError: 57 | pass 58 | 59 | 60 | MIDDLEWARE = [ 61 | 'django.middleware.security.SecurityMiddleware', 62 | 'django.contrib.sessions.middleware.SessionMiddleware', 63 | 'django.middleware.common.CommonMiddleware', 64 | 'django.middleware.csrf.CsrfViewMiddleware', 65 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 66 | 'django.contrib.messages.middleware.MessageMiddleware', 67 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 68 | ] 69 | 70 | ROOT_URLCONF = 'demo.urls' 71 | 72 | TEMPLATES = [ 73 | { 74 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 75 | 'DIRS': [ 76 | os.path.join(BASE_DIR, 'templates'), 77 | ], 78 | 'APP_DIRS': True, 79 | 'OPTIONS': { 80 | 'context_processors': [ 81 | 'django.template.context_processors.debug', 82 | 'django.template.context_processors.request', 83 | 'django.contrib.auth.context_processors.auth', 84 | 'django.contrib.messages.context_processors.messages', 85 | ], 86 | }, 87 | }, 88 | ] 89 | 90 | WSGI_APPLICATION = 'demo.wsgi.application' 91 | 92 | 93 | # Database 94 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 95 | 96 | DATABASES = { 97 | 'default': { 98 | 'ENGINE': 'django.db.backends.sqlite3', 99 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 100 | } 101 | } 102 | 103 | 104 | # Password validation 105 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 106 | 107 | AUTH_PASSWORD_VALIDATORS = [ 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 119 | }, 120 | ] 121 | 122 | 123 | # Internationalization 124 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 125 | 126 | LANGUAGE_CODE = 'en-us' 127 | 128 | TIME_ZONE = 'UTC' 129 | 130 | USE_I18N = True 131 | 132 | USE_L10N = True 133 | 134 | USE_TZ = True 135 | 136 | 137 | # Static files (CSS, JavaScript, Images) 138 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 139 | 140 | STATIC_URL = '/static/' 141 | 142 | # for livereload to work 143 | INTERNAL_IPS = ['127.0.0.1', '10.0.3.2', '192.168.225.101', '192.168.225.1'] 144 | #INSTALLED_APPS = ['livereload'] + INSTALLED_APPS 145 | #MIDDLEWARE += ['livereload.middleware.LiveReloadScript'] 146 | import socket 147 | if socket.gethostname() == 'kaveri': 148 | LIVERELOAD_HOST = '192.168.225.101' 149 | 150 | # DJANGO_SELECT2 settings 151 | SELECT2_BOOTSTRAP = True 152 | # TODO: auto rendering of select2 media components onnly seems to work 153 | # the first time the server loads the files. Therefore we disable it 154 | # and explicitly specify the associated media resources from the relevant 155 | # form using a Media stub. 156 | # Verify if this behavior is only in development server and if production 157 | # does not exhibit this problem, we should go back to using AUTO_RENDER 158 | # as it yields minified media resources for non-DEBUG code. 159 | AUTO_RENDER_SELECT2_STATICS = True 160 | 161 | POPUPCRUD = { 162 | 'base_template': 'base.html' 163 | } 164 | 165 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """testapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from django.views.generic import TemplateView 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', admin.site.urls), 22 | url(r'^accounts/', include(('django.contrib.auth.urls', 'accounts'), namespace='accounts')), 23 | url(r'^library/', include(('library.urls', 'library'), namespace='library')), 24 | url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'), 25 | ] 26 | 27 | try: 28 | import django_select2 29 | urlpatterns += [ 30 | url(r'^select2/', include('django_select2.urls')), 31 | ] 32 | except ImportError: 33 | pass 34 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harikvpy/django-popupcrud/08d212aecee5401206e5206d5f39912af82c1b1d/demo/library/__init__.py -------------------------------------------------------------------------------- /demo/library/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | from .models import Author, Book 7 | 8 | # Register your models here. 9 | @admin.register(Author) 10 | class AuthorAdmin(admin.ModelAdmin): 11 | list_display = ('name', 'age', 'double_age') 12 | 13 | def double_age(self, obj): 14 | return obj.age*2 15 | double_age.short_description = "Double Age" 16 | 17 | @admin.register(Book) 18 | class BookAdmin(admin.ModelAdmin): 19 | pass 20 | -------------------------------------------------------------------------------- /demo/library/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class LibraryConfig(AppConfig): 8 | name = 'library' 9 | -------------------------------------------------------------------------------- /demo/library/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-09-05 03:03 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='Author', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=128)), 22 | ('penname', models.CharField(max_length=128)), 23 | ('age', models.SmallIntegerField(blank=True, null=True)), 24 | ], 25 | options={ 26 | 'ordering': ('name',), 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='Book', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('title', models.CharField(max_length=128)), 34 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.Author')), 35 | ], 36 | options={ 37 | 'ordering': ('title',), 38 | }, 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /demo/library/migrations/0002_auto_20170919_0319.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-09-19 03:19 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 | dependencies = [ 12 | ('library', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='AuthorRating', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('rating', models.CharField(choices=[('1', '1 Star'), ('2', '2 Stars'), ('3', '3 Stars'), ('4', '4 Stars')], max_length=1, verbose_name='Rating')), 21 | ], 22 | options={ 23 | 'ordering': ('author',), 24 | 'verbose_name': 'Author Ratings', 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='BookRating', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('rating', models.CharField(choices=[('1', '1 Star'), ('2', '2 Stars'), ('3', '3 Stars'), ('4', '4 Stars')], max_length=1, verbose_name='Rating')), 32 | ], 33 | options={ 34 | 'ordering': ('book',), 35 | 'verbose_name': 'Book Ratings', 36 | }, 37 | ), 38 | migrations.AlterModelOptions( 39 | name='author', 40 | options={'ordering': ('name',), 'verbose_name': 'Author', 'verbose_name_plural': 'Authors'}, 41 | ), 42 | migrations.AlterModelOptions( 43 | name='book', 44 | options={'ordering': ('title',), 'verbose_name': 'Book', 'verbose_name_plural': 'Books'}, 45 | ), 46 | migrations.AlterField( 47 | model_name='author', 48 | name='age', 49 | field=models.SmallIntegerField(blank=True, null=True, verbose_name='Age'), 50 | ), 51 | migrations.AlterField( 52 | model_name='author', 53 | name='name', 54 | field=models.CharField(max_length=128, verbose_name='Name'), 55 | ), 56 | migrations.AlterField( 57 | model_name='author', 58 | name='penname', 59 | field=models.CharField(max_length=128, verbose_name='Pen Name'), 60 | ), 61 | migrations.AlterField( 62 | model_name='book', 63 | name='title', 64 | field=models.CharField(max_length=128, verbose_name='Title'), 65 | ), 66 | migrations.AddField( 67 | model_name='bookrating', 68 | name='book', 69 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.Book'), 70 | ), 71 | migrations.AddField( 72 | model_name='authorrating', 73 | name='author', 74 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.Author'), 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /demo/library/migrations/0003_book_isbn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-03-13 14:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('library', '0002_auto_20170919_0319'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='book', 17 | name='isbn', 18 | field=models.CharField(default=' ', max_length=12, verbose_name='ISBN'), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /demo/library/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harikvpy/django-popupcrud/08d212aecee5401206e5206d5f39912af82c1b1d/demo/library/migrations/__init__.py -------------------------------------------------------------------------------- /demo/library/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.utils.translation import ugettext_lazy as _ 5 | from django.db import models 6 | 7 | from six import python_2_unicode_compatible 8 | 9 | # Create your models here. 10 | 11 | @python_2_unicode_compatible 12 | class Author(models.Model): 13 | name = models.CharField("Name", max_length=128) 14 | penname = models.CharField("Pen Name", max_length=128) 15 | age = models.SmallIntegerField("Age", null=True, blank=True) 16 | 17 | class Meta: 18 | ordering = ('name',) 19 | verbose_name = "Author" 20 | verbose_name_plural = "Authors" 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | def double_age(self): 26 | return self.age*2 if self.age else '' 27 | double_age.label = "Double Age" 28 | 29 | 30 | @python_2_unicode_compatible 31 | class Book(models.Model): 32 | title = models.CharField(_("Title"), max_length=128) 33 | isbn = models.CharField("ISBN", max_length=12) 34 | author = models.ForeignKey(Author, on_delete=models.CASCADE) 35 | 36 | class Meta: 37 | ordering = ('title',) 38 | verbose_name = "Book" 39 | verbose_name_plural = "Books" 40 | 41 | def __str__(self): 42 | return self.title 43 | 44 | 45 | @python_2_unicode_compatible 46 | class AuthorRating(models.Model): 47 | author = models.ForeignKey(Author, on_delete=models.CASCADE) 48 | rating = models.CharField("Rating", max_length=1, choices=( 49 | ('1', '1 Star'), 50 | ('2', '2 Stars'), 51 | ('3', '3 Stars'), 52 | ('4', '4 Stars'), 53 | )) 54 | 55 | class Meta: 56 | ordering = ('author',) 57 | verbose_name = "Author Rating" 58 | verbose_name = "Author Ratings" 59 | 60 | def __str__(self): 61 | return "%s - %s" % (self.author.name, self.rating) 62 | 63 | 64 | @python_2_unicode_compatible 65 | class BookRating(models.Model): 66 | book = models.ForeignKey(Book, on_delete=models.CASCADE) 67 | rating = models.CharField("Rating", max_length=1, choices=( 68 | ('1', '1 Star'), 69 | ('2', '2 Stars'), 70 | ('3', '3 Stars'), 71 | ('4', '4 Stars'), 72 | )) 73 | 74 | class Meta: 75 | ordering = ('book',) 76 | verbose_name = "Book Rating" 77 | verbose_name = "Book Ratings" 78 | 79 | def __str__(self): 80 | return "%s - %s" % (self.book.title, self.rating) 81 | -------------------------------------------------------------------------------- /demo/library/templates/library/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n bootstrap3 %} 3 | {% block content %} 4 | {% include "library/book_detail_inner.html" %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /demo/library/templates/library/book_detail_inner.html: -------------------------------------------------------------------------------- 1 | {{ object.title }}
2 | {{ object.author }} 3 | -------------------------------------------------------------------------------- /demo/library/templates/library/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n bootstrap3 bsmodal %} 3 | {% block extrahead %} 4 | {{ form.media.css }} 5 | {{ form.media.js }} 6 | {% endblock extrahead %} 7 | {% block content %} 8 |
9 | {% csrf_token %} 10 | {% bootstrap_form form %} 11 | {% trans "Save" as submit %} 12 | {% bootstrap_button submit button_type="submit" button_class="btn-primary" %} 13 |
14 | {% bsmodal '??' 'add-related-modal' close_title_button=Yes %} 15 | {# modal body will be filled in by jQuery.load() return value #} 16 | ?? 17 | {% endbsmodal %} 18 | {% endblock content %} 19 | -------------------------------------------------------------------------------- /demo/library/templates/library/rating.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n bootstrap3 bsmodal %} 3 | {% block extrahead %} 4 | {{ form.media.css }} 5 | {{ form.media.js }} 6 | {% endblock extrahead %} 7 | {% block content %} 8 |
9 | {% csrf_token %} 10 | {% bootstrap_form form %} 11 | {% trans "Save" as submit %} 12 | {% bootstrap_button submit button_type="submit" button_class="btn-primary" %} 13 |
14 | {% bsmodal '??' 'add-related-modal' close_title_button=Yes %} 15 | {# modal body will be filled in by jQuery.load() return value #} 16 | ?? 17 | {% endbsmodal %} 18 | {% endblock content %} 19 | -------------------------------------------------------------------------------- /demo/library/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /demo/library/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.contrib import admin 3 | 4 | from library.models import Author 5 | from library.admin import AuthorAdmin 6 | from library import views 7 | 8 | author_admin = AuthorAdmin(Author, admin.site) 9 | 10 | urlpatterns = [ 11 | url(r'^authors/$', views.AuthorCrudViewset.list(), name='authors'), 12 | url(r'^authors/new/$', views.AuthorCrudViewset.create(), name='new-author'), 13 | url(r'^authors/(?P\d+)/edit/$', views.AuthorCrudViewset.update(), name='edit-author'), 14 | url(r'^authors/(?P\d+)/delete/$', views.AuthorCrudViewset.delete(), name='delete-author'), 15 | 16 | url(r'^rating/author/$', views.AuthorRatingView.as_view(), name='author-rating'), 17 | url(r'^rating/book/$', views.BookRatingView.as_view(), name='book-rating'), 18 | url(r'^mro/$', views.MultipleRelatedObjectDemoView.as_view(), name='multi-related-object-demo'), 19 | 20 | url(r'^books/', views.BookCrudViewset.urls()), 21 | url(r'^formsetauthors/', views.FormsetAuthorCrudViewset.urls(namespace='formset-authors')), 22 | ] 23 | 24 | # url(r'^books/$', views.BookCrudViewset.list(), name='books'), 25 | # url(r'^books/new/$', views.BookCrudViewset.create(), name='new-book'), 26 | # url(r'^books/(?P\d+)/$', views.BookCrudViewset.detail(), name='book-detail'), 27 | # url(r'^books/(?P\d+)/edit/$', views.BookCrudViewset.update(), name='edit-book'), 28 | # url(r'^books/(?P\d+)/delete/$', views.BookCrudViewset.delete(), name='delete-book'), 29 | 30 | urlpatterns += [ 31 | url(r'^writers/', include((author_admin.get_urls(), 'library'), namespace='writers')), 32 | ] 33 | -------------------------------------------------------------------------------- /demo/library/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.urls import reverse_lazy 5 | from django.views import generic 6 | from django import forms 7 | from django.contrib import messages 8 | 9 | try: 10 | from django_select2.forms import Select2Widget 11 | _select2 = True 12 | except ImportError: 13 | _select2 = False 14 | 15 | from popupcrud.views import PopupCrudViewSet 16 | from popupcrud.widgets import RelatedFieldPopupFormWidget 17 | 18 | from .models import Author, Book, BookRating 19 | 20 | # Create your views here. 21 | 22 | class AuthorForm(forms.ModelForm): 23 | sex = forms.ChoiceField(label="Sex", choices=(('M', 'Male'), ('F', 'Female'))) 24 | class Meta: 25 | model = Author 26 | fields = ('name', 'penname', 'age') 27 | 28 | def clean_sex(self): 29 | data = self.cleaned_data['sex'] 30 | if data == 'M': 31 | raise forms.ValidationError('Sex has to be Female!') 32 | return data 33 | 34 | 35 | class AuthorCrudViewset(PopupCrudViewSet): 36 | model = Author 37 | form_class = AuthorForm 38 | #fields = ('name', 'penname', 'age') 39 | list_display = ('name', 'penname', 'age', 'half_age', 'double_age') 40 | list_url = reverse_lazy("library:authors") 41 | new_url = reverse_lazy("library:new-author") 42 | legacy_crud = False 43 | 44 | """ 45 | form_class = AuthorForm 46 | list_permission_required = ('library.add_author',) 47 | create_permission_required = ('library.add_author',) 48 | update_permission_required = ('library.change_author',) 49 | delete_permission_required = ('library.delete_author',) 50 | """ 51 | 52 | def half_age(self, author): # pylint: disable=R0201 53 | return author.age/2 if author.age else '-' 54 | half_age.label = "Half life" 55 | half_age.order_field = 'age' 56 | 57 | def get_edit_url(self, obj): 58 | return reverse_lazy("library:edit-author", kwargs={'pk': obj.pk}) 59 | 60 | def get_delete_url(self, obj): 61 | if not obj.age or obj.age < 18: 62 | return None 63 | return reverse_lazy("library:delete-author", kwargs={'pk': obj.pk}) 64 | 65 | 66 | class BookForm(forms.ModelForm): 67 | class Meta: 68 | model = Book 69 | fields = ('title', 'author') 70 | 71 | def __init__(self, *args, **kwargs): 72 | super(BookForm, self).__init__(*args, **kwargs) 73 | author = self.fields['author'] 74 | author.widget = RelatedFieldPopupFormWidget( 75 | widget=Select2Widget(choices=author.choices) if _select2 else \ 76 | forms.Select(choices=author.choices), 77 | new_url=reverse_lazy("library:new-author")) 78 | 79 | 80 | class BookCrudViewset(PopupCrudViewSet): 81 | model = Book 82 | form_class = BookForm 83 | list_display = ('title', 'author') 84 | list_url = reverse_lazy("library:books:list") 85 | new_url = reverse_lazy("library:books:create") 86 | #paginate_by = None # disable pagination 87 | related_object_popups = { 88 | 'author': reverse_lazy("library:new-author") 89 | } 90 | legacy_crud = { 91 | 'create': True, 92 | } 93 | item_actions = [ 94 | ('Approve', 'glyphicon glyphicon-ok', 'approve') 95 | ] 96 | empty_list_icon = 'glyphicon glyphicon-book' 97 | empty_list_message = 'You have not defined any books.
Create a book to get started.' 98 | 99 | @staticmethod 100 | def get_edit_url(obj): 101 | return reverse_lazy("library:books:update", kwargs={'pk': obj.pk}) 102 | 103 | @staticmethod 104 | def get_delete_url(obj): 105 | return reverse_lazy("library:books:delete", kwargs={'pk': obj.pk}) 106 | 107 | @staticmethod 108 | def get_detail_url(obj): 109 | return reverse_lazy("library:books:detail", kwargs={'pk': obj.pk}) 110 | 111 | def approve(self, request, item_or_list): # pylint: disable=R0201 112 | return True, "Request has been approved" 113 | 114 | 115 | class AuthorRatingForm(forms.Form): 116 | author = forms.ModelChoiceField(queryset=Author.objects.all()) 117 | rating = forms.ChoiceField(label="Rating", choices=( 118 | ('1', '1 Star'), 119 | ('2', '2 Stars'), 120 | ('3', '3 Stars'), 121 | ('4', '4 Stars') 122 | )) 123 | 124 | def __init__(self, *args, **kwargs): 125 | super(AuthorRatingForm, self).__init__(*args, **kwargs) 126 | author = self.fields['author'] 127 | author.widget = RelatedFieldPopupFormWidget( 128 | widget=forms.Select(choices=author.choices), 129 | new_url=reverse_lazy("library:new-author")) 130 | 131 | 132 | class AuthorRatingView(generic.FormView): 133 | form_class = AuthorRatingForm 134 | template_name = "library/rating.html" 135 | success_url = reverse_lazy("library:author-rating") 136 | 137 | def form_valid(self, form): 138 | messages.info(self.request, "Thank you for your rating") 139 | return super(AuthorRatingView, self).form_valid(form) 140 | 141 | 142 | class BookRatingForm(forms.ModelForm): 143 | 144 | class Meta: 145 | model = BookRating 146 | fields = ('book', 'rating') 147 | 148 | 149 | class BookRatingView(generic.FormView): 150 | form_class = BookRatingForm 151 | template_name = "library/form.html" 152 | success_url = reverse_lazy("library:book-rating") 153 | 154 | def form_valid(self, form): 155 | messages.info(self.request, "Thank you for your rating") 156 | return super(BookRatingView, self).form_valid(form) 157 | 158 | 159 | class MultipleRelatedObjectForm(forms.Form): 160 | author = forms.ModelChoiceField(queryset=Author.objects.all()) 161 | book = forms.ModelChoiceField(queryset=Book.objects.all()) 162 | 163 | def __init__(self, *args, **kwargs): 164 | super(MultipleRelatedObjectForm, self).__init__(*args, **kwargs) 165 | author = self.fields['author'] 166 | author.widget = RelatedFieldPopupFormWidget( 167 | widget=Select2Widget(choices=author.choices) if _select2 else \ 168 | forms.Select(choices=author.choices), 169 | new_url=reverse_lazy("library:new-author")) 170 | book = self.fields['book'] 171 | book.widget = RelatedFieldPopupFormWidget( 172 | widget=forms.Select(choices=book.choices), 173 | new_url=reverse_lazy("library:books:create")) 174 | 175 | 176 | class MultipleRelatedObjectDemoView(generic.FormView): 177 | form_class = MultipleRelatedObjectForm 178 | template_name = "library/form.html" 179 | success_url = reverse_lazy("library:multi-related-object-demo") 180 | 181 | 182 | class CustomBookForm(forms.ModelForm): 183 | price = forms.DecimalField() 184 | # role = forms.ChoiceField(choices=( 185 | # ('A', 'Administrator'), 186 | # ('B', 'Manager'), 187 | # ('C', 'Resident'), 188 | # ('C', 'Guest'), 189 | # )) 190 | 191 | class Meta: 192 | model = Book 193 | fields = ('title', 'isbn') 194 | 195 | def clean_price(self): 196 | price = self.cleaned_data['price'] 197 | if price < 0: 198 | raise forms.ValidationError('Price has to be > 0!') 199 | return price 200 | 201 | class FormsetAuthorCrudViewset(AuthorCrudViewset): 202 | 203 | list_url = reverse_lazy("library:formset-authors:list") 204 | 205 | new_url = reverse_lazy("library:formset-authors:create") 206 | modal_sizes = { 207 | 'create_update': 'large', 208 | 'delete': 'small', 209 | 'detail': 'normal' 210 | } 211 | 212 | def get_edit_url(self, obj): 213 | return reverse_lazy("library:formset-authors:update", kwargs={'pk': obj.pk}) 214 | 215 | def get_delete_url(self, obj): 216 | if not obj.age or obj.age < 18: 217 | return None 218 | return reverse_lazy("library:formset-authors:delete", kwargs={'pk': obj.pk}) 219 | 220 | def get_formset_class(self): 221 | """ 222 | Returns the inline formset class for adding Books to this author. 223 | """ 224 | return forms.models.inlineformset_factory( 225 | Author, 226 | Book, 227 | form=CustomBookForm, 228 | fields=('title', 'isbn'), 229 | can_delete=True, 230 | extra=2) 231 | -------------------------------------------------------------------------------- /demo/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", "demo.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /demo/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n bootstrap3 %} 2 | 3 | 4 | 5 | popupcrud test app 6 | {% bootstrap_css %} 7 | 8 | {% bootstrap_javascript %} 9 | {% block extrahead %}{% endblock extrahead %} 10 | 11 | 12 |
13 |
14 |

popupcrud demo app

15 | {% block messages %} 16 | {% if messages %} 17 | {% for message in messages %} 18 | 22 | {% endfor %} 23 | {% endif %} 24 | {% endblock %} 25 | {% block content %}{% endblock content %} 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /demo/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 17 | {% endblock content %} 18 | -------------------------------------------------------------------------------- /demo/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %}{% endblock %} 5 | 6 | {% block content %} 7 | 8 |

{% trans "Thanks for spending some quality time with the Web site today." %}

9 | 10 |

{% trans 'Log in again' %}

11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /demo/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }} 5 | {{ form.media }} 6 | {% endblock %} 7 | 8 | {% block bodyclass %}{{ block.super }} login{% endblock %} 9 | 10 | {% block usertools %}{% endblock %} 11 | 12 | {% block nav-global %}{% endblock %} 13 | 14 | {% block content_title %}{% endblock %} 15 | 16 | {% block breadcrumbs %}{% endblock %} 17 | 18 | {% block content %} 19 | {% if form.errors and not form.non_field_errors %} 20 |

21 | {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 22 |

23 | {% endif %} 24 | 25 | {% if form.non_field_errors %} 26 | {% for error in form.non_field_errors %} 27 |

28 | {{ error }} 29 |

30 | {% endfor %} 31 | {% endif %} 32 | 33 |
34 | 35 | {% if user.is_authenticated %} 36 |

37 | {% blocktrans trimmed %} 38 | You are authenticated as {{ username }}, but are not authorized to 39 | access this page. Would you like to login to a different account? 40 | {% endblocktrans %} 41 |

42 | {% endif %} 43 | 44 |
{% csrf_token %} 45 |
46 | {{ form.username.errors }} 47 | {{ form.username.label_tag }} {{ form.username }} 48 |
49 |
50 | {{ form.password.errors }} 51 | {{ form.password.label_tag }} {{ form.password }} 52 | 53 |
54 | {% url 'admin_password_reset' as password_reset_url %} 55 | {% if password_reset_url %} 56 | 59 | {% endif %} 60 |
61 | 62 |
63 |
64 | 65 |
66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /demo/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | {% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} 4 | {% block breadcrumbs %} 5 | 9 | {% endblock %} 10 | 11 | {% block title %}{{ title }}{% endblock %} 12 | {% block content_title %}

{{ title }}

{% endblock %} 13 | {% block content %} 14 |

{% trans 'Your password was changed.' %}

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /demo/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | {% block extrastyle %}{{ block.super }}{% endblock %} 4 | {% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | 12 | {% block title %}{{ title }}{% endblock %} 13 | {% block content_title %}

{{ title }}

{% endblock %} 14 | 15 | {% block content %}
16 | 17 |
{% csrf_token %} 18 |
19 | {% if form.errors %} 20 |

21 | {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 22 |

23 | {% endif %} 24 | 25 | 26 |

{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}

27 | 28 |
29 | 30 |
31 | {{ form.old_password.errors }} 32 | {{ form.old_password.label_tag }} {{ form.old_password }} 33 |
34 | 35 |
36 | {{ form.new_password1.errors }} 37 | {{ form.new_password1.label_tag }} {{ form.new_password1 }} 38 | {% if form.new_password1.help_text %} 39 |
{{ form.new_password1.help_text|safe }}
40 | {% endif %} 41 |
42 | 43 |
44 | {{ form.new_password2.errors }} 45 | {{ form.new_password2.label_tag }} {{ form.new_password2 }} 46 | {% if form.new_password2.help_text %} 47 |
{{ form.new_password2.help_text|safe }}
48 | {% endif %} 49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 | 57 |
58 |
59 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /demo/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 9 | {% endblock %} 10 | 11 | {% block title %}{{ title }}{% endblock %} 12 | {% block content_title %}

{{ title }}

{% endblock %} 13 | 14 | {% block content %} 15 | 16 |

{% trans "Your password has been set. You may go ahead and log in now." %}

17 | 18 |

{% trans 'Log in' %}

19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /demo/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }}{% endblock %} 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | 12 | {% block title %}{{ title }}{% endblock %} 13 | {% block content_title %}

{{ title }}

{% endblock %} 14 | {% block content %} 15 | 16 | {% if validlink %} 17 | 18 |

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

19 | 20 |
{% csrf_token %} 21 |
22 |
23 | {{ form.new_password1.errors }} 24 | 25 | {{ form.new_password1 }} 26 |
27 |
28 | {{ form.new_password2.errors }} 29 | 30 | {{ form.new_password2 }} 31 |
32 | 33 |
34 |
35 | 36 | {% else %} 37 | 38 |

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

39 | 40 | {% endif %} 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /demo/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 9 | {% endblock %} 10 | 11 | {% block title %}{{ title }}{% endblock %} 12 | {% block content_title %}

{{ title }}

{% endblock %} 13 | {% block content %} 14 | 15 |

{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}

16 | 17 |

{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}

18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /demo/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} 3 | 4 | {% trans "Please go to the following page and choose a new password:" %} 5 | {% block reset_link %} 6 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 7 | {% endblock %} 8 | {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} 9 | 10 | {% trans "Thanks for using our site!" %} 11 | 12 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 13 | 14 | {% endautoescape %} 15 | -------------------------------------------------------------------------------- /demo/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }}{% endblock %} 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | 12 | {% block title %}{{ title }}{% endblock %} 13 | {% block content_title %}

{{ title }}

{% endblock %} 14 | {% block content %} 15 | 16 |

{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

17 | 18 |
{% csrf_token %} 19 |
20 |
21 | {{ form.email.errors }} 22 | 23 | {{ form.email }} 24 |
25 | 26 |
27 |
28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = django-popupcrud 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: skip-file 3 | # 4 | # django-popupcrud documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Sep 13 04:23:35 2017. 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 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | import django 23 | 24 | sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) 25 | sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'demo')) 26 | #sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__)))) 27 | 28 | os.environ['DJANGO_SETTINGS_MODULE'] = 'demo.settings' 29 | # Initialize django project so as to allow Sphinx to import the autodoc files 30 | # without errors. 31 | django.setup() 32 | 33 | import popupcrud # for popupcrud.__version__ which we will use as doc version 34 | 35 | 36 | # -- General configuration ------------------------------------------------ 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = ['sphinx.ext.autodoc',] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = u'django-popupcrud' 61 | copyright = u'2017, Hari Mahadevan' 62 | author = u'Hari Mahadevan' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = popupcrud.__version__ 70 | # The full version, including alpha/beta/rc tags. 71 | release = popupcrud.__version__ 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = None 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | # This patterns also effect to html_static_path and html_extra_path 83 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # If true, `todo` and `todoList` produce output, else they produce nothing. 89 | todo_include_todos = False 90 | 91 | 92 | # -- Options for HTML output ---------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | # 97 | html_theme = 'alabaster' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | # 103 | # html_theme_options = {} 104 | 105 | # Add any paths that contain custom static files (such as style sheets) here, 106 | # relative to this directory. They are copied after the builtin static files, 107 | # so a file named "default.css" will overwrite the builtin "default.css". 108 | html_static_path = ['_static'] 109 | 110 | # Custom sidebar templates, must be a dictionary that maps document names 111 | # to template names. 112 | # 113 | # This is required for the alabaster theme 114 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 115 | html_sidebars = { 116 | '**': [ 117 | 'about.html', 118 | 'navigation.html', 119 | 'relations.html', # needs 'show_related': True theme option to display 120 | 'searchbox.html', 121 | 'donate.html', 122 | ] 123 | } 124 | 125 | 126 | # -- Options for HTMLHelp output ------------------------------------------ 127 | 128 | # Output file base name for HTML help builder. 129 | htmlhelp_basename = 'django-popupcruddoc' 130 | 131 | 132 | # -- Options for LaTeX output --------------------------------------------- 133 | 134 | latex_elements = { 135 | # The paper size ('letterpaper' or 'a4paper'). 136 | # 137 | # 'papersize': 'letterpaper', 138 | 139 | # The font size ('10pt', '11pt' or '12pt'). 140 | # 141 | # 'pointsize': '10pt', 142 | 143 | # Additional stuff for the LaTeX preamble. 144 | # 145 | # 'preamble': '', 146 | 147 | # Latex figure (float) alignment 148 | # 149 | # 'figure_align': 'htbp', 150 | } 151 | 152 | # Grouping the document tree into LaTeX files. List of tuples 153 | # (source start file, target name, title, 154 | # author, documentclass [howto, manual, or own class]). 155 | latex_documents = [ 156 | (master_doc, 'django-popupcrud.tex', u'django-popupcrud Documentation', 157 | u'Hari Mahadevan', 'manual'), 158 | ] 159 | 160 | 161 | # -- Options for manual page output --------------------------------------- 162 | 163 | # One entry per manual page. List of tuples 164 | # (source start file, name, description, authors, manual section). 165 | man_pages = [ 166 | (master_doc, 'django-popupcrud', u'django-popupcrud Documentation', 167 | [author], 1) 168 | ] 169 | 170 | 171 | # -- Options for Texinfo output ------------------------------------------- 172 | 173 | # Grouping the document tree into Texinfo files. List of tuples 174 | # (source start file, target name, title, author, 175 | # dir menu entry, description, category) 176 | texinfo_documents = [ 177 | (master_doc, 'django-popupcrud', u'django-popupcrud Documentation', 178 | author, 'django-popupcrud', 'One line description of project.', 179 | 'Miscellaneous'), 180 | ] 181 | 182 | 183 | autodoc_member_order = 'bysource' 184 | 185 | 186 | -------------------------------------------------------------------------------- /docs/demo.rst: -------------------------------------------------------------------------------- 1 | Demo Project 2 | ------------ 3 | 4 | The demo project in folder ``demo`` shows four usage scenarios of 5 | ``PopupCrudViewSet``. To run the demo, issue the following commands from 6 | ``demo`` folder:: 7 | 8 | ./manage migrate --settings demo.settings 9 | ./manage runserver --settings demo.settings 10 | 11 | Homepage has links to the various views in the project that demonstrates 12 | different use cases. Each link has a brief description on the type of use case 13 | it demonstrates. 14 | 15 | One of the forms in the demo ``MultipleRelatedObjectForm``, shows how the 16 | advanced ``Select2`` can be used instead of the django's native `'Select`` 17 | widget. For this to work, you need to install ``django-select2`` in the virtual 18 | environment where ``demo`` is run. 19 | 20 | -------------------------------------------------------------------------------- /docs/howto.rst: -------------------------------------------------------------------------------- 1 | How-tos 2 | ------- 3 | 4 | Create CRUD views for a model 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Given a model in app named ``library`` (source code taken from the ``demo`` project 8 | ) in project's repo:: 9 | 10 | # library/models.py 11 | class Author(models.Model): 12 | name = models.CharField("Name", max_length=128) 13 | penname = models.CharField("Pen Name", max_length=128) 14 | age = models.SmallIntegerField("Age", null=True, blank=True) 15 | 16 | class Meta: 17 | ordering = ('name',) 18 | verbose_name = "Author" 19 | verbose_name_plural = "Authors" 20 | 21 | def __str__(self): 22 | return self.name 23 | 24 | 25 | Declare a PopupCrudViewSet derived class in app's views.py:: 26 | 27 | # library/views.py 28 | from popupcrud.views import PopupCrudViewSet 29 | 30 | class AuthorViewSet(PopupCrudViewSet): 31 | model = Author 32 | fields = ('name', 'penname', 'age') 33 | list_display = ('name', 'penname', 'age') 34 | list_url = reverse_lazy("library:authors") 35 | new_url = reverse_lazy("library:new-author") 36 | 37 | def get_edit_url(self, obj): 38 | return reverse_lazy("library:edit-author", kwargs={'pk': obj.pk}) 39 | 40 | def get_delete_url(self, obj): 41 | return reverse_lazy("library:delete-author", kwargs={'pk': obj.pk}) 42 | 43 | Wire up the individual CRUD views generated by the viewset to the app URL 44 | namespace in urls.py:: 45 | 46 | # library/urls.py 47 | urlpatterns= [ 48 | url(r'^authors/$', views.AuthorCrudViewset.list(), name='authors'), 49 | url(r'^authors/new/$', views.AuthorCrudViewset.create(), name='new-author'), 50 | url(r'^authors(?P\d+)/edit/$', views.AuthorCrudViewset.update(), name='edit-author'), 51 | url(r'^authors(?P\d+)/delete/$', views.AuthorCrudViewset.delete(), name='delete-author'), 52 | ] 53 | 54 | In the projects root urls.py:: 55 | 56 | # demo/urls.py 57 | urlpatterns + [ 58 | url(r'^library/', include('library.urls', namespace='library')), 59 | ] 60 | 61 | 62 | Control access using permissions 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | In your CRUD ViewSet, declare the permissions required for each CRUD view as:: 66 | 67 | class AuthorViewSet(PopupCrudViewSet): 68 | model = Author 69 | ... 70 | list_permission_required = ('library.list_authors',) 71 | create_permission_required = ('library.add_author',) 72 | update_permission_required = ('library.change_author',) 73 | delete_permission_required = ('library.delete_author',) 74 | 75 | However, if you want to determine the permission dynamically, override the 76 | ``get_permission_required()`` method and implement your custom permission logic:: 77 | 78 | class AuthorViewSet(PopupCrudViewSet): 79 | model = Author 80 | ... 81 | 82 | def get_permission_required(self, op): 83 | if op == 'create': 84 | # custom permission for creating new objects 85 | 86 | elif op == 'delete': 87 | # custom permission for updating existing objects 88 | else: 89 | return super(AuthorViewSet, self).get_permission_required(op) 90 | 91 | 92 | Create a model object from its FK select box in another form 93 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 94 | This allows user to create new instances of a model while they are working 95 | on a form which has a FK reference to the model for which PopupCrudViewSet 96 | views exist. This allows objects to be added seamlessly without the user 97 | switching context to another page to add the object and then coming back to 98 | work on the form. 99 | 100 | To illustrate with an example:: 101 | 102 | from popupcrud.widgets import RelatedFieldPopupFormWidget 103 | 104 | class AuthorRatingForm(forms.Form): 105 | author = forms.ModelChoiceField(queryset=Author.objects.all()) 106 | rating = forms.ChoiceField(label="Rating", choices=( 107 | ('1', '1 Star'), 108 | ('2', '2 Stars'), 109 | ('3', '3 Stars'), 110 | ('4', '4 Stars') 111 | )) 112 | 113 | def __init__(self, *args, **kwargs): 114 | super(AuthorRatingForm, self).__init__(*args, **kwargs) 115 | author = self.fields['author'] 116 | # Replace the default Select widget with PopupCrudViewSet's 117 | # RelatedFieldPopupFormWidget. Note the url argument to the widget. 118 | author.widget = RelatedFieldPopupFormWidget( 119 | widget=forms.Select(choices=author.choices), 120 | new_url=reverse_lazy("library:new-author")) 121 | 122 | 123 | class AuthorRatingView(generic.FormView): 124 | form_class = AuthorRatingForm 125 | 126 | # rest of the View handling code as per Django norms 127 | 128 | In the above form, the default widget for ``author``, django.forms.widgets.Select 129 | has been replaced by ``RelatedFieldPopupFormWidget``. Note the arguments to the 130 | widget constructor -- it takes the underlying Select widget and a url to create 131 | a new instance of the model. 132 | 133 | Use Select2 instead of native Select widget 134 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 135 | Select2 is an advanced version the browser native Select box allowing users 136 | navigate through fairly large selection list using keystrokes. Select2 is 137 | excellently supported in Django through the thirdparty app `django-select2 138 | `_. 139 | Replacing the native ``django.forms.Select`` control with equivalent 140 | ``django_select2.forms.Select2Widget`` widget is extremely easy:: 141 | 142 | from django_select2.forms import Select2Widget 143 | from popupcrud.widgets import RelatedFieldPopupFormWidget 144 | 145 | class AuthorRatingForm(forms.Form): 146 | author = forms.ModelChoiceField(queryset=Author.objects.all()) 147 | rating = forms.ChoiceField(label="Rating", choices=( 148 | ('1', '1 Star'), 149 | ('2', '2 Stars'), 150 | ('3', '3 Stars'), 151 | ('4', '4 Stars') 152 | )) 153 | 154 | def __init__(self, *args, **kwargs): 155 | super(AuthorRatingForm, self).__init__(*args, **kwargs) 156 | author = self.fields['author'] 157 | # Replace the default Select widget with PopupCrudViewSet's 158 | # RelatedFieldPopupFormWidget. Note the url argument to the widget. 159 | author.widget = RelatedFieldPopupFormWidget( 160 | widget=forms.Select2Widget(choices=author.choices), 161 | new_url=reverse_lazy("library:new-author")) 162 | 163 | Note how ``Select2Widget`` is essentially a drop in replacement for the native 164 | ``django.forms.Select`` widget. Consult ``django-select2`` `docs 165 | `_ 166 | for instructions on integrating it with your project. 167 | 168 | .. _providing-your-own-templates: 169 | 170 | Providing your own templates 171 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 172 | Out of the box, ``popupcrud`` comes with its own templates for rendering all 173 | the CRUD views. For most use cases this ought to suffice. For the detail 174 | view, the default template just renders the object name in the popup. Typically, 175 | you might want to include additional information about an object in its detail 176 | view. To do this, implement ``_detail.html`` in your app's template folder 177 | and this template will be used to display details about an object. 178 | 179 | One point to highlight about templates is that since popupcrud can work in 180 | both legacy(like Django admin) and the more modern Web 2.0 181 | modal dialog based modes, it needs two templates to render the content for the 182 | two modes. This is necessary as contents of a modal popup window should only 183 | contain details of the object without site-wide common elements such as headers 184 | and menu that is usually provided through a base template whereas the dedicated 185 | legacy crud page requires all the site-wide common artifacts. This problem 186 | exists for all CRUD views - create, update, delete and detail. Therefore, for 187 | consistency across different CRUD views, popupcurd uses a standard file naming 188 | convention to determine the template name to use for the given CRUD view mode. 189 | 190 | This convention gives first priority to Django generic CRUD views' default 191 | template file name. If it's present it will be used for the CRUD view. However, 192 | if the view is to be rendered in a modal popup window, which should not have 193 | site-wide common artifacts, popupcrud appends ``_inner`` to the base template 194 | filename (the part before ``.html``). So if you want to display 195 | details of a object of class ``Book`` in a modal popup, you have to implement 196 | the template file ``book_detail_inner.html``. However, if you disable popups 197 | for the ``detail`` view, you have to implement ``book_detail.html``. The 198 | difference between the two being that ``*_inner.html`` only renders the object's 199 | details whereas ``book_detail.html`` renders the object's details along with 200 | site-wide page common artifacts such as header, footers and/or sidebars. 201 | 202 | One strategy is to provide both templates and organize them using the 203 | ``{% include %}`` tag. With this pattern, ``book_detail.html`` would 204 | look like this:: 205 | 206 | {% extends "base.html" %} 207 | {% block content %} 208 | {% include "book_detail_inner.html" %} 209 | {% endblock content %} 210 | 211 | The same pattern is applicable to other CRUD views as well where template files 212 | such as ``book_form.html``, ``confirm_book_delete.html`` are looked for first 213 | before using popupcrud's own internal templates. 214 | 215 | Use the formset feature 216 | ~~~~~~~~~~~~~~~~~~~~~~~ 217 | To add a formset to edit objects of a child model, override the 218 | ``PopupCrudViewSet.get_formset_class()`` method in your derived class returning 219 | the ``BaseModelFormSet`` class which will be used to render the formset along 220 | with the model form. Formsets are always rendered at the bottom of the model form. 221 | 222 | To illustrate with an example, assume that we have a ``Book`` table with 223 | the following definition:: 224 | 225 | class Book(models.Model): 226 | title = models.CharField('Title', max_length=128) 227 | isbn = models.CharField('ISBN', max_length=12) 228 | author = models.ForeignKey(Author) 229 | 230 | class Meta: 231 | ordering = ('title',) 232 | verbose_name = "Book" 233 | verbose_name_plural = "Books" 234 | 235 | def __str__(self): 236 | return self.title 237 | 238 | To allow the user to edit one or more ``Book`` objects while 239 | creating or editing a ``Author`` object, you just need to extend the 240 | ``AuthorCrudViewset`` in the previous example to:: 241 | 242 | from django import forms 243 | from popupcrud.views import PopupCrudViewSet 244 | 245 | class AuthorViewSet(PopupCrudViewSet): 246 | model = Author 247 | ... 248 | 249 | def get_formset_class(self): 250 | return forms.models.inlineformset_factory( 251 | Author, 252 | Book, 253 | fields=('title', 'isbn'), 254 | can_delete=True, 255 | extra=1) 256 | 257 | Now when the modal for create or edit views will show a formset 258 | at the bottom with two fields -- ``Book.title`` and ``Book.isbn``. 259 | A button at the bottom of the formset allows additional formset rows 260 | to be added. Each formset row will also have a button at the 261 | right to delete the row. 262 | 263 | The sample above uses the django formset factory function to 264 | dynamically build a formset class based on models parent-child 265 | relationship. You may also return a custom formset class that is 266 | derived from ``BaseModelFormSet`` with appropriate specializations 267 | to suit your requirements. 268 | 269 | ``BaseModelFormSet`` base class requirement is due to 270 | ``PopupCrudViewSet`` invoking the ``save()`` method of the class 271 | to save formset data if all of them pass the field validation rules. 272 | 273 | A note about formset feature. Since formset forms are rendered in a 274 | tabular format, and since the modal dialogs are not resizable, there 275 | is a limit to the number of formset form fields that can be specified 276 | before it becomes unusable for the user. To cater for this, 277 | ``PopupCrudViewSet`` now allows the modal sizes to be adjusted through 278 | the ``modal_sizes`` class attribute. This allows you to specify the 279 | appropriate modal size based on your form and formset field count & 280 | sizes. See :ref:`modal sizes `. 281 | 282 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-popupcrud documentation master file, created by 2 | sphinx-quickstart on Wed Sep 13 04:23:35 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-popupcrud's documentation! 7 | ============================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | quickstart 14 | reference 15 | howto 16 | demo 17 | settings 18 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=django-popupcrud 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ---------- 3 | 4 | 1. Install ``django-popupcrud`` using pip: 5 | 6 | ``pip install django-popucrud`` 7 | 8 | Or install it directly from the source repository: 9 | 10 | ``pip intall git+https://github.com/harikvpy/django-popupcrud.git`` 11 | 12 | Yet another way would be to clone this repository and install from the cloned 13 | root folder via ``pip install -e .``. 14 | 15 | 2. Install the dependencies - ``django-bootstrap3`` and 16 | ``django-pure-pagination``. Add the dependencies and ``popupcrud`` to 17 | ``INSTALLED_APPS`` in your project's ``settings.py``:: 18 | 19 | INSTALLED_APPS = [ 20 | ... 21 | 'bootstrap3', 22 | 'pure_pagination', 23 | 'popupcrud', 24 | ... 25 | ] 26 | 27 | 3. Let ``PopupCrudViewSet`` know of your base template file name. This defaults 28 | to ``base.html``, but if your project uses a different base template 29 | filename, inform ``PopupCrudViewSet`` about it in ``settings.py``:: 30 | 31 | POPUPCRUD = { 32 | 'base_template': 'mybase.html', 33 | } 34 | 35 | Include Bootstrap CSS & JS resources in this base template. 36 | If you were to use ``django-bootstrap3`` tags for these, your base 37 | template should look something like this:: 38 | 39 | 40 | {% bootstrap_css %} 41 | 42 | {% bootstrap_javascript %} 43 | {% block extrahead %}{% endblock extrahead %} 44 | 45 | 46 | Also, define a block named ``extrahead`` within the ```` element. 47 | ``PopupCrudViewSet`` views use a few custom CSS styles to show column 48 | sorting options and sort priority. These styles are defined in 49 | ``static/popupcrud/css/popupcrud.css`` which is inserted into 50 | the ``extrahead`` block. If you don't declare this block, 51 | you will have to explicitly load the stylesheet into your base template. 52 | 53 | 4. In your app's ``views.py``, create a ``ViewSet`` for each model for which you 54 | want to support CRUD operations. 55 | 56 | Models.py:: 57 | 58 | from django.db import models 59 | 60 | class Author(models.Model): 61 | name = models.CharField("Name", max_length=128) 62 | penname = models.CharField("Pen Name", max_length=128) 63 | age = models.SmallIntegerField("Age", null=True, blank=True) 64 | 65 | class Meta: 66 | ordering = ('name',) 67 | verbose_name = "Author" 68 | verbose_name_plural = "Authors" 69 | 70 | def __str__(self): 71 | return self.name 72 | 73 | Views.py:: 74 | 75 | from django.core.urlresolvers import reverse_lazy, reverse 76 | from popupcrud.views import PopupCrudViewSet 77 | 78 | class AuthorViewSet(PopupCrudViewSet): 79 | model = Author 80 | fields = ('name', 'penname', 'age') 81 | list_display = ('name', 'penname', 'age') 82 | list_url = reverse_lazy("library:authors:list") 83 | new_url = reverse_lazy("library:authors:create") 84 | 85 | def get_edit_url(self, obj): 86 | return reverse("library:authors:update", kwargs={'pk': obj.pk}) 87 | 88 | def get_delete_url(self, obj): 89 | return reverse("library:authos:delete", kwargs={'pk': obj.pk}) 90 | 91 | 5. Wire up the CRUD views generated by the viewset to the URLconf:: 92 | 93 | urlpatterns= [ 94 | url(r'^authors/', views.AuthorCrudViewset.urls()), 95 | ] 96 | 97 | This will register the following urls: 98 | 99 | * ``authors/`` - list view 100 | * ``authors/create/`` - create view 101 | * ``authors//`` - detail view 102 | * ``authors//update/`` - update view 103 | * ``authors//delete/`` - delete view 104 | 105 | The urls are registered under its own namespace, which defaults to the 106 | model's ``verbose_name_plural`` meta value. 107 | 108 | 6. Thats it! Your modern HTML popup based CRUD for your table is up and running. 109 | 110 | PopupCrudViewSet has many options to customize the fields displayed in list 111 | view, form used for create/update operations, permission control and more. 112 | Refer to the Reference and How-to sections of the documentation for more 113 | details. 114 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | --------- 3 | 4 | Classes 5 | ~~~~~~~ 6 | 7 | PopupCrudViewSet 8 | ++++++++++++++++ 9 | 10 | .. autoclass:: popupcrud.views.PopupCrudViewSet 11 | :members: 12 | 13 | RelatedFieldPopupFormWidget 14 | +++++++++++++++++++++++++++ 15 | .. autoclass:: popupcrud.widgets.RelatedFieldPopupFormWidget 16 | :members: 17 | __init__ 18 | 19 | Template Tags 20 | ~~~~~~~~~~~~~ 21 | 22 | bsmodal 23 | +++++++ 24 | 25 | .. automodule:: popupcrud.templatetags.bsmodal 26 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | -------- 3 | 4 | .. autodata:: popupcrud.views.POPUPCRUD_DEFAULTS 5 | :annotation: 6 | -------------------------------------------------------------------------------- /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", "testsettings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /popupcrud.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "python.pythonPath": "${workspaceFolder}\\pyenv\\Scripts\\python.exe" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /popupcrud/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.12.0" 2 | -------------------------------------------------------------------------------- /popupcrud/static/popupcrud/css/popupcrud.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Code copied for Django admin base.css. The sorting-icons.svg is also ripped 3 | * off from Django admin. Since we're using the same code for displaying 4 | * sorting options (code copied fro Django admin's implementation with minor 5 | * changes) we can safely use these styles with some tweaks for it to co-exist 6 | * nicely with Bootstrap styles. 7 | */ 8 | thead th.sorted .text { 9 | padding-right: 42px; 10 | } 11 | thead th.sorted a.sortremove { 12 | visibility: hidden; 13 | } 14 | table thead th.sorted:hover a.sortremove { 15 | visibility: visible; 16 | } 17 | table thead th.sorted .sortoptions { 18 | float: right; 19 | text-align: right; 20 | } 21 | table thead th.sorted .sortpriority { 22 | font-size: .6em; 23 | text-align: center; 24 | vertical-align: 3px; 25 | margin-left: 1px; 26 | margin-right: 1px; 27 | } 28 | table thead th.sorted .sortoptions a { 29 | position: relative; 30 | width: 14px; 31 | height: 14px; 32 | display: inline-block; 33 | background: url(../img/sorting-icons.svg) 0 0 no-repeat; 34 | background-size: 14px auto; 35 | } 36 | table thead th.sorted .sortoptions a.sortremove { 37 | background-position: 0 0; 38 | } 39 | table thead th.sorted .sortoptions a.sortremove:after { 40 | content: '\\'; 41 | position: absolute; 42 | top: -6px; 43 | left: 3px; 44 | font-weight: 200; 45 | font-size: 18px; 46 | color: #999; 47 | } 48 | table thead th.sorted .sortoptions a.sortremove:focus:after, 49 | table thead th.sorted .sortoptions a.sortremove:hover:after { 50 | color: #447e9b; 51 | } 52 | table thead th.sorted .sortoptions a.sortremove:focus, 53 | table thead th.sorted .sortoptions a.sortremove:hover { 54 | background-position: 0 -14px; 55 | } 56 | table thead th.sorted .sortoptions a.ascending { 57 | background-position: 0 -28px; 58 | } 59 | table thead th.sorted .sortoptions a.ascending:focus, 60 | table thead th.sorted .sortoptions a.ascending:hover { 61 | background-position: 0 -42px; 62 | } 63 | table thead th.sorted .sortoptions a.descending { 64 | top: 1px; 65 | background-position: 0 -56px; 66 | } 67 | table thead th.sorted .sortoptions a.descending:focus, 68 | table thead th.sorted .sortoptions a.descending:hover { 69 | background-position: 0 -70px; 70 | } 71 | thead th.sorted .text { 72 | padding-right: 42px; 73 | } 74 | .empty-list-icon { 75 | font-size: 5em; 76 | } 77 | .push-30-t { 78 | margin-top: 30px; 79 | } 80 | .push-30 { 81 | margin-bottom: 30px; 82 | } 83 | .push-50-t { 84 | margin-top: 50px; 85 | } 86 | .push-50 { 87 | margin-bottom: 50px; 88 | } 89 | .modal-formset .table>tbody>tr>td { 90 | border-top: 0px gray solid; 91 | } 92 | .modal-formset .table>thead>tr>th.required:after { 93 | content:"*"; 94 | color: red; 95 | } 96 | .modal-formset-field { 97 | margin-right: 0px; 98 | width: auto; 99 | } 100 | .modal-formset .table>tbody>tr>td:last-child .modal-formset-field { 101 | display: none; 102 | } 103 | .modal-formset .table>tbody>tr>td:last-child { 104 | vertical-align: middle; 105 | } 106 | .modal-body { 107 | max-height: 75%; 108 | overflow-y: auto; 109 | } 110 | .modal-body::-webkit-scrollbar { 111 | width: .6em; 112 | } 113 | .modal-body::-webkit-scrollbar-track { 114 | -webkit-box-shadow: inset 0 0 3px rgba(0,0,0,0.2); 115 | box-shadow: inset 0 0 3px rgba(0,0,0,0.2); 116 | border-radius: 8px; 117 | } 118 | .modal-body::-webkit-scrollbar-thumb { 119 | background-color: darkgrey; 120 | outline: 1px solid darkgray; 121 | border-radius: 8px; 122 | } 123 | .modal-body .help-block { 124 | margin-top: 0px; 125 | margin-bottom: 0px; 126 | font-size: 0.9em; 127 | } -------------------------------------------------------------------------------- /popupcrud/static/popupcrud/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /popupcrud/static/popupcrud/js/jquery.formset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Formset 1.3-pre 3 | * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) 4 | * @requires jQuery 1.2.6 or later 5 | * 6 | * Copyright (c) 2009, Stanislaus Madueke 7 | * All rights reserved. 8 | * 9 | * Licensed under the New BSD License 10 | * See: http://www.opensource.org/licenses/bsd-license.php 11 | */ 12 | ;(function($) { 13 | $.fn.formset = function(opts) 14 | { 15 | var options = $.extend({}, $.fn.formset.defaults, opts), 16 | flatExtraClasses = options.extraClasses.join(' '), 17 | totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'), 18 | maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'), 19 | minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'), 20 | childElementSelector = 'input,select,textarea,label,div', 21 | $$ = $(this), 22 | 23 | applyExtraClasses = function(row, ndx) { 24 | if (options.extraClasses) { 25 | row.removeClass(flatExtraClasses); 26 | row.addClass(options.extraClasses[ndx % options.extraClasses.length]); 27 | } 28 | }, 29 | 30 | updateElementIndex = function(elem, prefix, ndx) { 31 | var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'), 32 | replacement = prefix + '-' + ndx + '-'; 33 | if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement)); 34 | if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement)); 35 | if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement)); 36 | }, 37 | 38 | hasChildElements = function(row) { 39 | return row.find(childElementSelector).length > 0; 40 | }, 41 | 42 | showAddButton = function() { 43 | return maxForms.length == 0 || // For Django versions pre 1.2 44 | (maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0)); 45 | }, 46 | 47 | /** 48 | * Indicates whether delete link(s) can be displayed - when total forms > min forms 49 | */ 50 | showDeleteLinks = function() { 51 | return minForms.length == 0 || // For Django versions pre 1.7 52 | (minForms.val() == '' || (totalForms.val() - minForms.val() > 0)); 53 | }, 54 | 55 | insertDeleteLink = function(row) { 56 | var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), 57 | addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); 58 | if (row.is('TR')) { 59 | // If the forms are laid out in table rows, insert 60 | // the remove button into the last table cell: 61 | // django-crispy renders inline formsets using the 'th' tag within each row 62 | // whereas django-bootstrap3 inline formsets are rendered using the 'td' tag 63 | // Since we use both in our code, we need to make the insertion work on 64 | // both cases. Following code handles this. 65 | var lastChildSelector = "td:last"; 66 | if (row.children(":first").is("th")) { 67 | lastChildSelector = "th:last"; 68 | } 69 | row.children(lastChildSelector).append('' + options.deleteText + ''); 70 | } else if (row.is('UL') || row.is('OL')) { 71 | // If they're laid out as an ordered/unordered list, 72 | // insert an
  • after the last list item: 73 | row.append('
  • ' + options.deleteText +'
  • '); 74 | } else { 75 | // Otherwise, just insert the remove button as the 76 | // last child element of the form's container: 77 | row.append(''); 78 | } 79 | // Check if we're under the minimum number of forms - not to display delete link at rendering 80 | if (!showDeleteLinks()){ 81 | row.find('a.' + delCssSelector).hide(); 82 | } 83 | 84 | row.find('a.' + delCssSelector).click(function() { 85 | var row = $(this).parents('.' + options.formCssClass), 86 | del = row.find('input:hidden[id $= "-DELETE"]'), 87 | buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'), 88 | forms; 89 | if (del.length) { 90 | // We're dealing with an inline formset. 91 | // Rather than remove this form from the DOM, we'll mark it as deleted 92 | // and hide it, then let Django handle the deleting: 93 | del.val('on'); 94 | row.hide(); 95 | forms = $('.' + options.formCssClass).not(':hidden'); 96 | } else { 97 | row.remove(); 98 | // Update the TOTAL_FORMS count: 99 | forms = $('.' + options.formCssClass).not('.formset-custom-template'); 100 | totalForms.val(forms.length); 101 | } 102 | for (var i=0, formCount=forms.length; i