├── .github └── workflows │ └── gh.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── example ├── books │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── scaffolding.py │ ├── templates │ │ └── books │ │ │ └── book_form.html │ ├── tests.py │ └── views.py ├── example │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py ├── generic_scaffold ├── __init__.py ├── models.py ├── templates │ └── generic_scaffold │ │ ├── base.html │ │ ├── confirm_delete.html │ │ ├── detail.html │ │ ├── form.html │ │ ├── list.html │ │ ├── testmodelimplicit_confirm_delete.html │ │ ├── testmodelimplicit_detail.html │ │ ├── testmodelimplicit_form.html │ │ └── testmodelimplicit_list.html ├── templatetags │ ├── __init__.py │ └── generic_scaffold_tags.py ├── tests.py └── views.py ├── publish.bat ├── quicktest.py ├── setup.py └── tox.ini /.github/workflows/gh.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: [3.8, 3.11] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install tox tox-gh-actions 24 | - name: Test with tox 25 | run: tox -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.sqlite3 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | .DS_Store 57 | 58 | tags 59 | tags.lock 60 | tags.temp 61 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | install: 4 | - pip install tox 5 | script: 6 | - tox 7 | env: 8 | - TOXENV=py38-django32 9 | - TOXENV=py38-django40 10 | - TOXENV=py38-django41 11 | - TOXENV=py38-django42 12 | - TOXENV=py311-django32 13 | - TOXENV=py311-django40 14 | - TOXENV=py311-django41 15 | - TOXENV=py311-django42 16 | - TOXENV=py311-django50 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-24 Serafeim Papastefanos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-include generic_scaffold/templates * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | django-generic-scaffold 3 | ======================= 4 | 5 | .. image:: https://github.com/spapas/django-generic-scaffold/actions/workflows/gh.yml/badge.svg 6 | :target: https://github.com/spapas/django-generic-scaffold/actions/workflows/gh.yml 7 | 8 | .. image:: https://badge.fury.io/py/django-generic-scaffold.svg 9 | :target: https://badge.fury.io/py/django-generic-scaffold 10 | 11 | 12 | 13 | With django-generic-scaffold you can quickly create CRUD generic class based views for your models so you will have a basic CRUD interface to your models by writing only a couple of lines of extra code! The purpose of this CRUD interface is, as opposed to django-admin, to be used by users and not staff members. 14 | 15 | django-generic-scaffold is different from other scaffolding tools because it generates all views/url routes *on-the-fly* (by creating subclasses of normal django class-based views) and *not* by outputing python code. This way you can re-configure 16 | your views anytime you wish. 17 | 18 | As you can understand the main purpose of this library is to be able to add CRUD for as many models in your project with as little mental effort as possible. Nothing beats the django-admin for that of course but usually you don't want to give access to /admin to all the users that will do data entry. I've found this project to be invaluable to my work (I mostly create apps to by used internally by the members of a public sector org); I guess it should also be very useful when you need to create a quick MVP for your project. 19 | 20 | 21 | Example 22 | ======= 23 | 24 | I've added an example project of using django-generic-scaffold: https://github.com/spapas/generic-scaffold-demo 25 | 26 | Installation 27 | ============ 28 | 29 | Install it with ``pip install django-generic-scaffold``, or if you want to use the latest version on github, try ``pip install git+https://github.com/spapas/django-generic-scaffold``. 30 | 31 | If you want to use the template tags and the fallback templates of django-generic-scaffold, please put ``generic_scaffold`` in your ``INSTALLED_APPS`` setting. If you 32 | don't need the template tags or fallback templates then no modifying of your settings is needed, just go ahead and use it! 33 | 34 | Simple usage 35 | ============ 36 | 37 | Let's say you have defined a model named ``Book`` in your ``models.py``. In your ``views.py`` or, even better in a module named ``scaffolding.py`` define a class that overrides ``CrudManager``: 38 | 39 | .. code-block:: python 40 | 41 | from generic_scaffold import CrudManager 42 | import models 43 | 44 | class BookCrudManager(CrudManager): 45 | model = models.Book 46 | prefix = 'books' 47 | 48 | 49 | Now, include the following lines to the ``urls.py`` of your application: 50 | 51 | .. code-block:: python 52 | 53 | from scaffolding import BookCrudManager # or from views import BookCrudManager depending on where you've put it 54 | book_crud = BookCrudManager() 55 | 56 | # [...] define your urlpatters here 57 | 58 | urlpatterns += book_crud.get_url_patterns() 59 | 60 | 61 | You may now visit ``http://127.0.0.1:8000/books`` (or whatever was your ``prefix``) to get a list of your ``Book`` instances. 62 | The following methods have also been created: 63 | 64 | * Create: ``http://127.0.0.1:8000/bookscreate`` 65 | * Detail: ``http://127.0.0.1:8000/booksdetail/`` 66 | * Edit: ``http://127.0.0.1:8000/booksupdate/`` 67 | * Delete: ``http://127.0.0.1:8000/booksdelete/`` 68 | 69 | If you don't do anything else, the default fallback templates will be used (they are ugly and should only be used for testing). 70 | You should add a template named ``app_name/testmodel_list.html`` (which is the default template for the ``ListView``) to override 71 | the fallback templates - please read the next section for more info on that. 72 | 73 | The ``prefix`` option you set to the ``BooksCrudManager`` method will just prepend this prefix to all created urls 74 | and can also be used to get your url names for reversing. 75 | 76 | Template selection 77 | ================== 78 | 79 | There's a bunch of fallback templates that will be used if no other template can be used instead. 80 | These template are for testing purposes only and should be overriden (unless you want to 81 | quickly see that everything works). Now, there are two ways you can redefine your templates: 82 | 83 | * Implicitly: Just add appropriate templates depending on your app/model name (similarly to normal class-based-views), for example for ``app_name`` and ``TestModel`` you can add the following templates: 84 | 85 | For create/update add ``app_name/testmodel_form.html``, 86 | for list add ``app_name/testmodel_list.html``, 87 | for detail add ``app_name/testmodel_detail.html``, 88 | for delete add ``app_name/testmodel_confirm_delete.html``. 89 | 90 | * Explicitly: You can use the ``action_template_name`` configuration option to explicitly set which templates will be used for each action. The ``action`` could be ``list, detail, update, create`` or ``delete``. So to configure the detail template name to be ``foo.html`` you'll use the option ``detail_template_name = 'foo.html'``. 91 | 92 | So, the priority of templates is: 93 | 94 | * Explicit templates (if configured) 95 | * Implicit templates (if found) 96 | * Fallback templates (as a last resort) 97 | 98 | Configuration 99 | ============= 100 | 101 | Most of the time, you'll need to configure three things before using ``django-generic-scaffold``: The form class used for create and update views, the access permissions for each generic class based view and the templates that each view will use. These can be configured just by settings attributes to your ``CrudManager`` class. 102 | 103 | * To configure the form class that will be used, use the option ``form_class``. 104 | * To set the permissions you have to set the ``permissions`` attribute to a dictionary of callables. The keys of that dictionary should be ``list, detail, update, create`` or ``delete`` while the values should be callables like ``login_required`` or ``permission_required('permission')`` etc. 105 | * To configure the template names explicitly, use ``action_template_name``. 106 | 107 | For any other configuration of the generated class based views you'll need to define mixins that will be passed to the generated CBV classes as a list using the option ``action_mixins`` (again action is either ``list, detail``, etc). 108 | 109 | Using mixins you can do whatever you want to your resulting CBV classes -- also, by forcing you to use mixins django-generic-scaffold will help you follow bet code practices (DRY). 110 | 111 | However, sometimes mixins are not enough and you may need to completely override the parent Views to use something else. For this, you may set the ``action_view_class`` property to your own parent class view (i.e ``list_view_class = OverridenListView``). 112 | 113 | API and template tags 114 | ===================== 115 | 116 | If you want to use the provided template tags to your templates, you'll need to add ``{% load generic_scaffold_tags %}`` near 117 | the top of your template. Then you may use ``set_urls_for_scaffold`` which will output the URLs of the 118 | selected scaffold depending on your configuration. This tag can receive 119 | three parameters: The django app name, the model name and the prefix name. You can either use 120 | the combination of app name / model name or just the prefix. 121 | 122 | It will return a dictionary with all 123 | the scaffolded urls for this model. For example, to get the url names for the model ``test2`` (careful you must use the internal model name so for ``Test2`` use ``test2`` ) 124 | belonging to the app ``test1`` you'll use ``{% set_urls_for_scaffold "test1" "test2" as url_names %}`` and then you could use the attributes ``list, 125 | create, detail, update, delete`` of that object to reverse and get the corresponding urls, for example 126 | use ``{% url url_names.list }`` to get the url for list. 127 | 128 | There's also a similar API function named ``get_url_names that`` you can use to get the urls for your scaffolds. 129 | 130 | For example, you can do something like: 131 | 132 | .. code-block:: python 133 | 134 | from generic_scaffold import get_url_names 135 | from django.core.urlresolvers import reverse 136 | 137 | names = get_url_names(prefix='test') 138 | list_url = reverse(names['list']) 139 | 140 | Please notice above that if you need to call the above template tag or function with the prefix you need to pass the parameter name i.e call it like ``{% set_urls_for_scaffold prefix="my_prefix" as url_names %}``. 141 | 142 | Finally, if for some reason you'd prefer to access the url name directly without using the above you can generate the url name of a scaffolded view yourself using the following algorithm: ``{prefix}_{app_name}_{model_name}_{method}`` where the method is one of list/create/update/detail/delete. This could then be used directly with ``{% url %}`` or ``reverse``. 143 | 144 | Sample configuration 145 | ==================== 146 | 147 | A sample config that uses a different form (``TestForm``), defines different behavior using mixins for create and update and needs a logged in user for update / delete / create (but anonymous users can list and detail) is the following: 148 | 149 | .. code-block:: python 150 | 151 | from django.contrib.auth.decorators import login_required 152 | 153 | class TestCrudManager(CrudManager): 154 | prefix = 'test' 155 | model = models.TestModel 156 | form_class = forms.TestForm 157 | create_mixins = (CreateMixin, ) 158 | update_mixins = (UpdateMixin, ) 159 | permissions = { 160 | 'update': login_required, 161 | 'delete': login_required, 162 | 'create': login_required, 163 | } 164 | 165 | Django/python version support 166 | ============================= 167 | 168 | As can be seen from tox.ini, the tests are run for Python for Python 3.8 and Python 3.11 with Django 3.2-5, so these are the 169 | supported versions. Intermediate versions should also work without problems. 170 | 171 | .. list-table:: Python Django Version Support 172 | :widths: 25 25 173 | :header-rows: 1 174 | 175 | * - Python Version 176 | - Django Version 177 | * - 3.8+ 178 | - 3.2-5.0 179 | 180 | Some trickery for django-generic-scaffold 181 | ========================================= 182 | 183 | Here are some more tricks and advice to make even better usage of this package: 184 | 185 | - For a model called ``Company`` I would use a prefix `"companies/"` (notice the slash at the end). This may seem a little strange at first but it creates nice looking urls like: ``/companies/`` (for list), ``/companies/detail/3`` (for detail) etc. 186 | 187 | - Add a ``get_absolute_url`` method to your models to avoid having to declare where to redirect after a successful post when creating/editing instances. For example for the same Company model I'd do it like this: 188 | 189 | .. code-block:: python 190 | 191 | from generic_scaffold import get_url_names 192 | 193 | class Company(models.Model): 194 | 195 | def get_absolute_url(self): 196 | return reverse(get_url_names(prefix='companies/')['detail'], args=[self.id]) 197 | 198 | - Continuing the above ``Company`` example you could add the following template tag to the company related templates: 199 | 200 | .. code-block:: python 201 | 202 | {% load generic_scaffold_tags %} 203 | [...] 204 | {% set_urls_for_scaffold prefix="companies/" as co_url_names %} 205 | 206 | And then you'd be able to access the urls like: ``{% url co_url_names.list %}`` or ``{% url co_url_names.detail %}``. 207 | 208 | - As mentioned above, If for some reason you'd prefer to access the url name directly you can generate yourself using the following algorithm: ``{prefix}_{app_name}_{model_name}_{method}``. Thus for our ``Company`` example, if the app name is called ``core`` the name of the list view would be ``companies/_core_company_detail`` (notice that the prefix is ``companies/``). 209 | 210 | - Sometimes django-generic-scaffold creates more views than you'd like! For example, for various reasons I usually avoid having delete views. Also for small models you may don't need a detail view. To "disable" a view you can use the following simple mixin: 211 | 212 | .. code-block:: python 213 | 214 | from django.core.exceptions import PermissionDenied 215 | 216 | class NotAllowedMixin(object, ): 217 | def get_queryset(self): 218 | raise PermissionDenied 219 | 220 | Then when you define your ``CrudManager`` use that as the mixin for your method, for example if you want to disable delete you'll add: 221 | ``delete_mixins = (NotAllowedMixin, )``. I guess it would be better if the ``CrudManager`` had a way to actually define which methods you need but this solution is much easier (for me) :) 222 | 223 | - If you want to change the fields that appear in the Create/Update views you'll need to define a ``form_class``. Without it all fields will be visible. 224 | 225 | - You'll probably need to fix your query to avoid n+1 problems. This can easily be done with a mixin like this: 226 | 227 | .. code-block:: python 228 | 229 | class FixQuerysetMixin(object, ): 230 | def get_queryset(self): 231 | return super(FixQuerysetMixin, self).get_queryset().select_related( 232 | 'field1', 'field2' 233 | ) 234 | 235 | You can then add that mixin to either your ``CrudManager`` corresponding ``list_mixins`` or ``detail_mixins`` list. 236 | 237 | - My list views *always* use a table (from https://github.com/jieter/django-tables2) and a filter (from https://github.com/carltongibson/django-filter). If you want to move your DRYness to the next level, you can add the following mixin to your CrudManager's ``list_mixins`` to auto-add both a table and a filter to your list view: 238 | 239 | .. code-block:: python 240 | 241 | import filters, tables 242 | 243 | class AddFilterTableMixin(object, ): 244 | def get_context_data(self, **kwargs): 245 | context = super(AddFilterTableMixin, self).get_context_data(**kwargs) 246 | qs = self.get_queryset() 247 | filter = getattr(filters, self.model.__name__+'Filter')(self.request.GET, qs) 248 | table = getattr(tables, self.model.__name__+'Table')(filter.qs) 249 | RequestConfig(self.request, paginate={"per_page": 15}).configure(table) 250 | context['table'] = table 251 | context['filter'] = filter 252 | return context 253 | 254 | This will try to find a ``filters.XFilter`` and ``tables.XTable`` class in the ``filters`` and ``tables`` modules (you need to import them ofcourse). So if your model name is ``Company`` it will use the ``CompanyFilter`` and ``CompanyTable`` classes! 255 | 256 | Now this could be made even more DRY by using some ``type`` magic to auto-generate the table and filer class on the fly; however I've concluded that you'll almost always need to configure them to define which fields to display at the table and which fields to use at te filter so I don't think it's really worth it. 257 | 258 | Changelog 259 | ========= 260 | 261 | v.0.6.0 262 | ------- 263 | 264 | - Support Django 5, remove old versions 265 | 266 | v.0.5.7 267 | ------- 268 | 269 | - Add Django 4.1 and 4.2 to tox.ini 270 | 271 | v.0.5.6 272 | ------- 273 | 274 | - Add Django 4.0 to tox.ini 275 | 276 | v.0.5.5 277 | ------- 278 | 279 | - Add Django 3.0 to tox.ini 280 | 281 | v.0.5.4 282 | ------- 283 | 284 | - Add Django 2.2 to tox.ini 285 | - Drop support for Django < 1.8 286 | 287 | v.0.5.3 288 | ------- 289 | 290 | - Add Django 2.1 to tox.ini 291 | 292 | v.0.5.2 293 | ------- 294 | 295 | - Upload readme to pypi 296 | 297 | v.0.5.0 298 | ------- 299 | 300 | - Add support for Django 2 301 | 302 | v.0.4.1 303 | ------- 304 | 305 | - Add support for Django 1.11 306 | 307 | 308 | v.0.4.0 309 | ------- 310 | 311 | - Add support for Django 1.10 312 | - Allow overriding the parent classes of all views 313 | 314 | v.0.3.3 315 | ------- 316 | 317 | - Fix bug with django 1.9 not containing the (url) patterns function 318 | 319 | v.0.3.2 320 | ------- 321 | 322 | - Include templates in pip package (old version did not include them due to wrong setup.py configuration) 323 | 324 | v.0.3.1 325 | ------- 326 | 327 | - Fix bug with '__all__' fields when adding form_class 328 | 329 | v.0.3.0 330 | ------- 331 | 332 | - Drop support for Django 1.4 and 1.5 333 | - Add support for python 3 (python 3.5) for Django 1.8 and 1.9 334 | 335 | v.0.2.0 336 | ------- 337 | 338 | - Braking changes for API and template tags 339 | - Add example project 340 | - Add support and configure tox for Django 1.9 341 | - A bunch of fallback templates have been added (``generic_scaffold/{list, detail, form, confirm_delete}.html``) 342 | - Use API (get_url_names) for tests and add it to docs 343 | - Add (url) prefix as an attribute to CrudManager and fix templatetag to use it. 344 | - Prefix has to be unique to make API and template tags easier to use 345 | - Model also has to be unique 346 | 347 | v.0.1.2 348 | ------- 349 | 350 | - Add tests and integrate with tox 351 | - Add some basic templates (non-empty, mainly for tests) 352 | 353 | v.0.1.1 354 | ------- 355 | 356 | - Add template tags to get crud urls 357 | 358 | v.0.1 359 | ----- 360 | 361 | - Initial 362 | -------------------------------------------------------------------------------- /example/books/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/example/books/__init__.py -------------------------------------------------------------------------------- /example/books/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /example/books/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class BooksConfig(AppConfig): 7 | name = 'books' 8 | -------------------------------------------------------------------------------- /example/books/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-14 21:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Book', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=128)), 21 | ('author', models.CharField(max_length=128)), 22 | ('category', models.CharField(max_length=32)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /example/books/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/example/books/migrations/__init__.py -------------------------------------------------------------------------------- /example/books/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | try: 4 | from django.core.urlresolvers import reverse 5 | except ModuleNotFoundError: 6 | from django.urls import reverse 7 | 8 | from django.db import models 9 | import generic_scaffold 10 | 11 | class Book(models.Model): 12 | title = models.CharField(max_length=128) 13 | author = models.CharField(max_length=128) 14 | category = models.CharField(max_length=32) 15 | 16 | def get_absolute_url(self): 17 | return reverse(self.detail_url_name, args=[self.id]) 18 | 19 | def __str__(self): 20 | return '{0} {1} {2}'.format(self.title, self.author, self.category) 21 | -------------------------------------------------------------------------------- /example/books/scaffolding.py: -------------------------------------------------------------------------------- 1 | from generic_scaffold import CrudManager 2 | from books.models import Book 3 | 4 | class BookCrudManager(CrudManager): 5 | model = Book 6 | prefix = 'books' 7 | 8 | -------------------------------------------------------------------------------- /example/books/templates/books/book_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic_scaffold/base.html' %} 2 | {% load generic_scaffold_tags %} 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {{ form }} 7 | 8 |
9 | {% set_urls_for_scaffold app="books" model="book" as urls %} 10 | List 11 | {% endblock %} -------------------------------------------------------------------------------- /example/books/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/books/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | # SECURITY WARNING: keep the secret key used in production secret! 7 | SECRET_KEY = 's3zr&8kj5$qha_(#3yh#c&kx(o1oh5l^((4jyfqk_m&f4t32$h' 8 | 9 | # SECURITY WARNING: don't run with debug turned on in production! 10 | DEBUG = True 11 | 12 | ALLOWED_HOSTS = [] 13 | 14 | # Application definition 15 | INSTALLED_APPS = [ 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.messages', 21 | 'django.contrib.staticfiles', 22 | 23 | 'generic_scaffold', 24 | 'books', 25 | ] 26 | 27 | MIDDLEWARE_CLASSES = [ 28 | 'django.middleware.security.SecurityMiddleware', 29 | 'django.contrib.sessions.middleware.SessionMiddleware', 30 | 'django.middleware.common.CommonMiddleware', 31 | 'django.middleware.csrf.CsrfViewMiddleware', 32 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 33 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 34 | 'django.contrib.messages.middleware.MessageMiddleware', 35 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 36 | ] 37 | 38 | ROOT_URLCONF = 'example.urls' 39 | 40 | TEMPLATES = [ 41 | { 42 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 43 | 'DIRS': [], 44 | 'APP_DIRS': True, 45 | 'OPTIONS': { 46 | 'context_processors': [ 47 | 'django.template.context_processors.debug', 48 | 'django.template.context_processors.request', 49 | 'django.contrib.auth.context_processors.auth', 50 | 'django.contrib.messages.context_processors.messages', 51 | ], 52 | }, 53 | }, 54 | ] 55 | 56 | WSGI_APPLICATION = 'example.wsgi.application' 57 | 58 | DATABASES = { 59 | 'default': { 60 | 'ENGINE': 'django.db.backends.sqlite3', 61 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 62 | } 63 | } 64 | 65 | AUTH_PASSWORD_VALIDATORS = [ 66 | { 67 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 68 | }, { 69 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 70 | }, { 71 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 72 | }, { 73 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 74 | }, 75 | ] 76 | 77 | LANGUAGE_CODE = 'en-us' 78 | TIME_ZONE = 'UTC' 79 | USE_I18N = True 80 | USE_L10N = True 81 | USE_TZ = True 82 | 83 | STATIC_URL = '/static/' 84 | 85 | # Add the parent directory to the sys.path to help development 86 | import sys 87 | scaffold_path = os.path.join(BASE_DIR, '..') 88 | sys.path.insert(1, scaffold_path) 89 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | """example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/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 17 | from django.contrib import admin 18 | from books.scaffolding import BookCrudManager 19 | book_crud = BookCrudManager() 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', admin.site.urls), 23 | ] 24 | 25 | 26 | urlpatterns += book_crud.get_url_patterns() 27 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example 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.9/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", "example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/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", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /generic_scaffold/__init__.py: -------------------------------------------------------------------------------- 1 | from generic_scaffold.views import CrudManager 2 | from generic_scaffold.views import get_url_names -------------------------------------------------------------------------------- /generic_scaffold/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/base.html: -------------------------------------------------------------------------------- 1 | {% block content %}{% endblock content %} -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic_scaffold/base.html' %} 2 | {% block content %} 3 | {{ object }} 4 |
5 | {% csrf_token %} 6 | 7 |
8 | List 9 | {% endblock %} -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic_scaffold/base.html' %} 2 | {% block content %} 3 | {{ object }} 4 | List 5 | {% endblock %} -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic_scaffold/base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form }} 6 | 7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic_scaffold/base.html' %} 2 | {% load generic_scaffold_tags %} 3 | {% block content %} 4 | 5 | Create 6 |
    7 | {% for object in object_list %} 8 |
  • 9 | {{ object }} 10 | detail 11 | update 12 | delete 13 |
  • 14 | {% endfor %} 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/testmodelimplicit_confirm_delete.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/generic_scaffold/templates/generic_scaffold/testmodelimplicit_confirm_delete.html -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/testmodelimplicit_detail.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/generic_scaffold/templates/generic_scaffold/testmodelimplicit_detail.html -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/testmodelimplicit_form.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/generic_scaffold/templates/generic_scaffold/testmodelimplicit_form.html -------------------------------------------------------------------------------- /generic_scaffold/templates/generic_scaffold/testmodelimplicit_list.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/generic_scaffold/templates/generic_scaffold/testmodelimplicit_list.html -------------------------------------------------------------------------------- /generic_scaffold/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/django-generic-scaffold/01ff2a4f9eb6739d1148e810d5522fac38a0d023/generic_scaffold/templatetags/__init__.py -------------------------------------------------------------------------------- /generic_scaffold/templatetags/generic_scaffold_tags.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import template 3 | from django.conf import settings 4 | 5 | from generic_scaffold import get_url_names 6 | 7 | register = template.Library() 8 | 9 | 10 | if django.VERSION >= (1, 9, 0): 11 | decorator = register.simple_tag 12 | else: 13 | decorator = register.assignment_tag 14 | 15 | 16 | def set_urls_for_scaffold(app=None, model=None, prefix=None): 17 | url_names = get_url_names(app, model, prefix) 18 | return url_names 19 | 20 | set_urls_for_scaffold = decorator(set_urls_for_scaffold) 21 | 22 | -------------------------------------------------------------------------------- /generic_scaffold/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, RequestFactory, Client 2 | import django 3 | if django.VERSION >= (2, 0, 0): 4 | from django.urls import reverse 5 | else: 6 | from django.core.urlresolvers import reverse 7 | from django.db import models 8 | from django.views.generic import ListView, CreateView , DetailView, UpdateView, DeleteView 9 | from generic_scaffold import CrudManager, get_url_names 10 | from generic_scaffold.templatetags.generic_scaffold_tags import set_urls_for_scaffold 11 | 12 | class TestModel(models.Model): 13 | test = models.CharField(max_length=16) 14 | 15 | class TestModel2(models.Model): 16 | test = models.CharField(max_length=16) 17 | 18 | class TestEmptyModel(models.Model): 19 | test = models.CharField(max_length=16) 20 | 21 | class TestModelImplicit(models.Model): 22 | test = models.CharField(max_length=16) 23 | 24 | class TestModelExplicit(models.Model): 25 | test = models.CharField(max_length=16) 26 | 27 | class TestCrudManager(CrudManager): 28 | model = TestModel 29 | prefix = 'test' 30 | 31 | class TestEmptyPrefixCrudManager(CrudManager): 32 | model = TestEmptyModel 33 | 34 | class TestImplicitCrudManager(CrudManager): 35 | model = TestModelImplicit 36 | prefix = 'test_implicit' 37 | 38 | class TestExplicitCrudManager(CrudManager): 39 | model = TestModelExplicit 40 | prefix = 'test_explicit' 41 | list_template_name = 'generic_scaffold/list.html' 42 | form_template_name = 'generic_scaffold/form.html' 43 | detail_template_name = 'generic_scaffold/detail.html' 44 | delete_template_name = 'generic_scaffold/confirm_delete.html' 45 | 46 | 47 | class TestOverrideViewsCrudManager(CrudManager): 48 | model = TestModel2 49 | prefix = 'test_override_views' 50 | 51 | list_view_class = type('OverridenListView', (ListView, ), {} ) 52 | create_view_class = type('OverridenCreateView', (CreateView, ), {} ) 53 | detail_view_class = type('OverridenDetailView', (DetailView, ), {} ) 54 | update_view_class = type('OverridenUpdateView', (UpdateView, ), {} ) 55 | delete_view_class = type('OverridenDeleteView', (DeleteView, ), {} ) 56 | 57 | list_template_name = 'generic_scaffold/list.html' 58 | form_template_name = 'generic_scaffold/form.html' 59 | detail_template_name = 'generic_scaffold/detail.html' 60 | delete_template_name = 'generic_scaffold/confirm_delete.html' 61 | 62 | 63 | test_crud = TestCrudManager() 64 | urlpatterns = test_crud.get_url_patterns() 65 | 66 | test_empty_prefix_crud = TestEmptyPrefixCrudManager() 67 | urlpatterns += test_empty_prefix_crud.get_url_patterns() 68 | 69 | test_implicit_crud = TestImplicitCrudManager() 70 | urlpatterns += test_implicit_crud.get_url_patterns() 71 | 72 | test_explicit_crud = TestExplicitCrudManager() 73 | urlpatterns += test_explicit_crud.get_url_patterns() 74 | 75 | test_override_crud = TestOverrideViewsCrudManager() 76 | urlpatterns += test_override_crud.get_url_patterns() 77 | 78 | 79 | class DuplicatesTest(TestCase): 80 | def test_duplicate_prefix(self): 81 | with self.assertRaises(django.core.exceptions.ImproperlyConfigured): 82 | klazz = type("Thrower", (CrudManager, ), {'prefix': 'test',} ) 83 | 84 | def test_duplicate_model(self): 85 | with self.assertRaises(django.core.exceptions.ImproperlyConfigured): 86 | klazz = type("Thrower", (CrudManager, ), { 87 | 'prefix': 'foo', 88 | 'model': TestModel, 89 | } ) 90 | 91 | 92 | class EmptyPrefixTest(TestCase): 93 | def setUp(self): 94 | self.crud = test_empty_prefix_crud 95 | self.list_view = self.crud.get_list_class_view() 96 | self.create_view = self.crud.get_create_class_view() 97 | self.update_view = self.crud.get_update_class_view() 98 | self.delete_view = self.crud.get_delete_class_view() 99 | self.detail_view = self.crud.get_detail_class_view() 100 | 101 | TestEmptyModel.objects.create(test='test') 102 | 103 | def test_urls_have_correct_name(self): 104 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 105 | self.assertEquals( getattr(self.crud, attr+'_url_name'), "generic_scaffold_testemptymodel_{0}".format(attr)) 106 | 107 | def test_views_have_correct_parent_class(self): 108 | self.assertEquals(self.list_view.__bases__[-1].__name__, "ListView") 109 | self.assertEquals(self.create_view.__bases__[-1].__name__, "CreateView") 110 | self.assertEquals(self.update_view.__bases__[-1].__name__, "UpdateView") 111 | self.assertEquals(self.delete_view.__bases__[-1].__name__, "DeleteView") 112 | self.assertEquals(self.detail_view.__bases__[-1].__name__, "DetailView") 113 | 114 | def test_view_have_correct_model(self): 115 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 116 | self.assertEquals( getattr(self, attr+'_view').model.__name__, "TestEmptyModel") 117 | 118 | def test_with_client(self): 119 | c = Client() 120 | 121 | list_resp = c.get( reverse(get_url_names(None)['list'])) 122 | self.assertEquals(list_resp.status_code, 200) 123 | self.assertTrue(b'TestEmptyModel object' in list_resp.content) 124 | 125 | create_resp = c.get( reverse(get_url_names(None)['create'])) 126 | self.assertEquals(create_resp.status_code, 200) 127 | self.assertTrue(b'id_test' in create_resp.content) 128 | 129 | update_resp = c.get( reverse(get_url_names(None)['update'], args=[1])) 130 | self.assertEquals(update_resp.status_code, 200) 131 | self.assertTrue(b'id_test' in update_resp.content) 132 | 133 | detail_resp = c.get( reverse(get_url_names(None)['detail'], args=[1])) 134 | self.assertEquals(detail_resp.status_code, 200) 135 | self.assertTrue(b'TestEmptyModel object' in detail_resp.content) 136 | 137 | delete_resp = c.get( reverse(get_url_names(None)['delete'], args=[1])) 138 | self.assertEquals(delete_resp.status_code, 200) 139 | self.assertTrue(b'TestEmptyModel object' in delete_resp.content) 140 | 141 | 142 | 143 | class SimpleParameterTest(TestCase): 144 | def setUp(self): 145 | self.crud = test_crud 146 | self.list_view = self.crud.get_list_class_view() 147 | self.create_view = self.crud.get_create_class_view() 148 | self.update_view = self.crud.get_update_class_view() 149 | self.delete_view = self.crud.get_delete_class_view() 150 | self.detail_view = self.crud.get_detail_class_view() 151 | 152 | TestModel.objects.create(test='test') 153 | 154 | def test_urls_have_correct_name(self): 155 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 156 | self.assertEquals( getattr(self.crud, attr+'_url_name'), "{0}_generic_scaffold_testmodel_{1}".format(TestCrudManager.prefix, attr)) 157 | 158 | def test_views_have_correct_parent_class(self): 159 | self.assertEquals(self.list_view.__bases__[-1].__name__, "ListView") 160 | self.assertEquals(self.create_view.__bases__[-1].__name__, "CreateView") 161 | self.assertEquals(self.update_view.__bases__[-1].__name__, "UpdateView") 162 | self.assertEquals(self.delete_view.__bases__[-1].__name__, "DeleteView") 163 | self.assertEquals(self.detail_view.__bases__[-1].__name__, "DetailView") 164 | 165 | def test_view_have_correct_model(self): 166 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 167 | self.assertEquals( getattr(self, attr+'_view').model.__name__, "TestModel") 168 | 169 | def test_with_client(self): 170 | c = Client() 171 | 172 | list_resp = c.get( reverse(get_url_names(prefix='test')['list'])) 173 | self.assertEquals(list_resp.status_code, 200) 174 | self.assertTrue(b'TestModel object' in list_resp.content) 175 | 176 | create_resp = c.get( reverse(get_url_names(prefix='test')['create'])) 177 | self.assertEquals(create_resp.status_code, 200) 178 | self.assertTrue(b'id_test' in create_resp.content) 179 | 180 | update_resp = c.get( reverse(get_url_names(prefix='test')['update'], args=[1])) 181 | self.assertEquals(update_resp.status_code, 200) 182 | self.assertTrue(b'id_test' in update_resp.content) 183 | 184 | detail_resp = c.get( reverse(get_url_names(prefix='test')['detail'], args=[1])) 185 | self.assertEquals(detail_resp.status_code, 200) 186 | self.assertTrue(b'TestModel object' in detail_resp.content) 187 | 188 | delete_resp = c.get( reverse(get_url_names(prefix='test')['delete'], args=[1])) 189 | self.assertEquals(delete_resp.status_code, 200) 190 | self.assertTrue(b'TestModel object' in delete_resp.content) 191 | 192 | 193 | class TemplateOrderingTest(TestCase): 194 | def setUp(self): 195 | self.client = Client() 196 | TestModel.objects.create(test='test') 197 | TestModelImplicit.objects.create(test='test') 198 | TestModelExplicit.objects.create(test='test') 199 | 200 | def test_fallback_templates(self): 201 | list_resp = self.client.get( reverse(get_url_names(prefix='test')['list'])) 202 | self.assertTemplateUsed(list_resp, 'generic_scaffold/list.html' ) 203 | 204 | create_resp = self.client.get( reverse(get_url_names(prefix='test')['create'])) 205 | self.assertTemplateUsed(create_resp, 'generic_scaffold/form.html' ) 206 | 207 | update_resp = self.client.get( reverse(get_url_names(prefix='test')['update'], args=[1])) 208 | self.assertTemplateUsed(update_resp, 'generic_scaffold/form.html' ) 209 | 210 | detail_resp = self.client.get( reverse(get_url_names(prefix='test')['detail'], args=[1])) 211 | self.assertTemplateUsed(detail_resp, 'generic_scaffold/detail.html' ) 212 | 213 | delete_resp = self.client.get( reverse(get_url_names(prefix='test')['delete'], args=[1])) 214 | self.assertTemplateUsed(delete_resp, 'generic_scaffold/confirm_delete.html' ) 215 | 216 | def test_implicit_templates(self): 217 | list_resp = self.client.get( reverse(get_url_names(prefix='test_implicit')['list'])) 218 | self.assertTemplateUsed(list_resp, 'generic_scaffold/testmodelimplicit_list.html' ) 219 | 220 | create_resp = self.client.get( reverse(get_url_names(prefix='test_implicit')['create'])) 221 | self.assertTemplateUsed(create_resp, 'generic_scaffold/testmodelimplicit_form.html' ) 222 | 223 | update_resp = self.client.get( reverse(get_url_names(prefix='test_implicit')['update'], args=[1])) 224 | self.assertTemplateUsed(update_resp, 'generic_scaffold/testmodelimplicit_form.html' ) 225 | 226 | detail_resp = self.client.get( reverse(get_url_names(prefix='test_implicit')['detail'], args=[1])) 227 | self.assertTemplateUsed(detail_resp, 'generic_scaffold/testmodelimplicit_detail.html' ) 228 | 229 | delete_resp = self.client.get( reverse(get_url_names(prefix='test_implicit')['delete'], args=[1])) 230 | self.assertTemplateUsed(delete_resp, 'generic_scaffold/testmodelimplicit_confirm_delete.html' ) 231 | 232 | def test_explicit_templates(self): 233 | list_resp = self.client.get( reverse(get_url_names(prefix='test_explicit')['list'])) 234 | self.assertTemplateUsed(list_resp, 'generic_scaffold/list.html' ) 235 | 236 | create_resp = self.client.get( reverse(get_url_names(prefix='test_explicit')['create'])) 237 | self.assertTemplateUsed(create_resp, 'generic_scaffold/form.html' ) 238 | 239 | update_resp = self.client.get( reverse(get_url_names(prefix='test_explicit')['update'], args=[1])) 240 | self.assertTemplateUsed(update_resp, 'generic_scaffold/form.html' ) 241 | 242 | detail_resp = self.client.get( reverse(get_url_names(prefix='test_explicit')['detail'], args=[1])) 243 | self.assertTemplateUsed(detail_resp, 'generic_scaffold/detail.html' ) 244 | 245 | delete_resp = self.client.get( reverse(get_url_names(prefix='test_explicit')['delete'], args=[1])) 246 | self.assertTemplateUsed(delete_resp, 'generic_scaffold/confirm_delete.html' ) 247 | 248 | 249 | class TestUrlNames(TestCase): 250 | def setUp(self): 251 | pass 252 | 253 | def test_get_url_names_with_prefix(self): 254 | names = get_url_names(prefix='test') 255 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 256 | self.assertEquals( names[attr], "{0}_generic_scaffold_testmodel_{1}".format(TestCrudManager.prefix, attr)) 257 | 258 | def test_get_url_names_with_model(self): 259 | names = get_url_names(app='generic_scaffold', model='testmodel') 260 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 261 | self.assertEquals( names[attr], "{0}_generic_scaffold_testmodel_{1}".format(TestCrudManager.prefix, attr)) 262 | 263 | class TestTempalteTags(TestCase): 264 | def test_template_tags_with_prefix(self): 265 | names = set_urls_for_scaffold(prefix='test') 266 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 267 | self.assertEquals( names[attr], "{0}_generic_scaffold_testmodel_{1}".format(TestCrudManager.prefix, attr)) 268 | 269 | def test_get_url_names_with_model(self): 270 | names = set_urls_for_scaffold(app='generic_scaffold', model='testmodel') 271 | for attr in ['list', 'create', 'update', 'delete', 'detail']: 272 | self.assertEquals( names[attr], "{0}_generic_scaffold_testmodel_{1}".format(TestCrudManager.prefix, attr)) 273 | 274 | 275 | class TestOverrideViews(TestCase): 276 | def setUp(self): 277 | self.crud = test_override_crud 278 | self.list_view = self.crud.get_list_class_view() 279 | self.create_view = self.crud.get_create_class_view() 280 | self.update_view = self.crud.get_update_class_view() 281 | self.delete_view = self.crud.get_delete_class_view() 282 | self.detail_view = self.crud.get_detail_class_view() 283 | 284 | def test_views_have_correct_parent_classes(self): 285 | self.assertEquals(self.list_view.__bases__[-1].__name__, "OverridenListView") 286 | self.assertEquals(self.create_view.__bases__[-1].__name__, "OverridenCreateView") 287 | self.assertEquals(self.update_view.__bases__[-1].__name__, "OverridenUpdateView") 288 | self.assertEquals(self.delete_view.__bases__[-1].__name__, "OverridenDeleteView") 289 | self.assertEquals(self.detail_view.__bases__[-1].__name__, "OverridenDetailView") 290 | -------------------------------------------------------------------------------- /generic_scaffold/views.py: -------------------------------------------------------------------------------- 1 | import django 2 | if django.VERSION >= (4, 0, 0): 3 | from django.urls import re_path as url 4 | else: 5 | from django.conf.urls import url 6 | 7 | if django.VERSION >= (2, 0, 0): 8 | from django.urls import reverse 9 | else: 10 | from django.core.urlresolvers import reverse 11 | 12 | from django.views.generic import ListView, CreateView , DetailView, UpdateView, DeleteView, TemplateView 13 | from six import with_metaclass 14 | try: 15 | from django.apps import apps 16 | get_model = apps.get_model 17 | except: 18 | from django.db.models.loading import get_model 19 | 20 | 21 | def get_model_name(model): 22 | try: 23 | return model._meta.model_name 24 | except: 25 | return model._meta.module_name 26 | 27 | def get_app_label(model): 28 | return model._meta.app_label 29 | 30 | 31 | class CrudTracker(type): 32 | def __init__(cls, name, bases, attrs): 33 | 34 | try: 35 | if CrudManager not in bases: 36 | return 37 | except NameError: 38 | return 39 | 40 | try: 41 | cls.prefix 42 | except AttributeError: 43 | cls.prefix = None 44 | 45 | for r in CrudManager._registry: 46 | if r.prefix == cls.prefix: 47 | raise django.core.exceptions.ImproperlyConfigured 48 | 49 | if get_model_name(r.model)==get_model_name(cls.model) and \ 50 | get_app_label(r.model) == get_app_label(cls.model): 51 | raise django.core.exceptions.ImproperlyConfigured 52 | CrudManager._registry.append(cls) 53 | 54 | 55 | def identity(f): 56 | return f 57 | 58 | 59 | class FallbackTemplateMixin(object, ): 60 | def get_template_names(self): 61 | names = super(FallbackTemplateMixin, self).get_template_names() 62 | if self.kind == 'delete': 63 | fallback_name = 'confirm_delete' 64 | elif self.kind in ['create', 'update']: 65 | fallback_name = 'form' 66 | else: 67 | fallback_name = self.kind 68 | names.append('generic_scaffold/{0}.html'.format(fallback_name)) 69 | return names 70 | 71 | 72 | 73 | class CrudManager(with_metaclass(CrudTracker, object, )): 74 | _registry = [] 75 | list_mixins = [] 76 | delete_mixins = [] 77 | detail_mixins = [] 78 | create_mixins = [] 79 | update_mixins = [] 80 | 81 | list_view_class = ListView 82 | create_view_class = CreateView 83 | detail_view_class = DetailView 84 | update_view_class = UpdateView 85 | delete_view_class = DeleteView 86 | 87 | def __new__(cls): 88 | cls.app_label = get_app_label(cls.model) 89 | cls.model_name = get_model_name(cls.model) 90 | 91 | if hasattr(cls, 'prefix') and cls.prefix: 92 | cls.list_url_name = '{0}_{1}_{2}'.format(cls.prefix, cls.get_name(), 'list') 93 | cls.create_url_name = '{0}_{1}_{2}'.format(cls.prefix, cls.get_name(), 'create') 94 | cls.detail_url_name = '{0}_{1}_{2}'.format(cls.prefix, cls.get_name(), 'detail') 95 | cls.update_url_name = '{0}_{1}_{2}'.format(cls.prefix, cls.get_name(), 'update') 96 | cls.delete_url_name = '{0}_{1}_{2}'.format(cls.prefix, cls.get_name(), 'delete') 97 | else: 98 | cls.list_url_name = '{0}_{1}'.format(cls.get_name(), 'list') 99 | cls.create_url_name = '{0}_{1}'.format(cls.get_name(), 'create') 100 | cls.detail_url_name = '{0}_{1}'.format(cls.get_name(), 'detail') 101 | cls.update_url_name = '{0}_{1}'.format(cls.get_name(), 'update') 102 | cls.delete_url_name = '{0}_{1}'.format(cls.get_name(), 'delete') 103 | 104 | cls.model.list_url_name = cls.list_url_name 105 | cls.model.detail_url_name = cls.detail_url_name 106 | cls.model.create_url_name = cls.create_url_name 107 | cls.model.update_url_name = cls.update_url_name 108 | cls.model.delete_url_name = cls.delete_url_name 109 | 110 | return super(CrudManager, cls).__new__(cls) 111 | 112 | def __init__(self, *args, **kwargs): 113 | self.perms = { 114 | 'list': identity, 115 | 'create': identity, 116 | 'update': identity, 117 | 'delete': identity, 118 | 'detail': identity, 119 | } 120 | if hasattr(self, 'permissions') and self.permissions: 121 | self.perms.update(self.permissions) 122 | 123 | def get_get_context_data(self, klazz, **kwargs): 124 | def wrapped_get_context_data(inst, **kwargs): 125 | context = super(klazz, inst).get_context_data(**kwargs) 126 | context['crud'] = self 127 | #context.update(inst.get_context_data() ) 128 | return context 129 | return wrapped_get_context_data 130 | 131 | @classmethod 132 | def get_name(cls): 133 | try: 134 | model_name = cls.model._meta.model_name 135 | except: 136 | model_name = cls.model._meta.module_name 137 | return '{0}_{1}'.format(cls.model._meta.app_label, model_name) 138 | 139 | def get_list_class_view(self): 140 | name = '{0}_{1}'.format(self.get_name(), 'ListView') 141 | options_dict = { 142 | 'kind': 'list', 143 | 'model': self.model, 144 | } 145 | 146 | if hasattr(self, 'list_template_name') and self.list_template_name: 147 | options_dict['template_name'] = self.list_template_name 148 | 149 | parent_classes_list = [FallbackTemplateMixin] 150 | parent_classes_list.extend(self.list_mixins) 151 | parent_classes_list.append(self.list_view_class) 152 | 153 | klazz = type(name, tuple(parent_classes_list), options_dict ) 154 | klazz.get_context_data = self.get_get_context_data(klazz) 155 | 156 | return klazz 157 | 158 | def get_create_class_view(self): 159 | name = '{0}_{1}'.format(self.get_name(), 'CreateView') 160 | options_dict = { 161 | 'kind': 'create', 162 | 'model': self.model, 163 | 'fields': '__all__', 164 | } 165 | if hasattr(self, 'form_template_name') and self.form_template_name: 166 | options_dict['template_name'] = self.form_template_name 167 | 168 | if hasattr(self, 'form_class') and self.form_class: 169 | options_dict['form_class'] = self.form_class 170 | options_dict['fields'] = None 171 | 172 | parent_classes_list = [FallbackTemplateMixin] 173 | parent_classes_list.extend(self.create_mixins) 174 | parent_classes_list.append(self.create_view_class) 175 | 176 | klazz = type(name, tuple(parent_classes_list), options_dict ) 177 | klazz.get_context_data = self.get_get_context_data(klazz) 178 | return klazz 179 | 180 | def get_detail_class_view(self): 181 | name = '{0}_{1}'.format(self.get_name(), 'DetailView') 182 | options_dict = { 183 | 'kind': 'detail', 184 | 'model': self.model, 185 | } 186 | if hasattr(self, 'detail_template_name') and self.detail_template_name: 187 | options_dict['template_name'] = self.detail_template_name 188 | 189 | parent_classes_list = [FallbackTemplateMixin] 190 | parent_classes_list.extend(self.detail_mixins) 191 | parent_classes_list.append(self.detail_view_class) 192 | 193 | klazz = type(name, tuple(parent_classes_list), options_dict ) 194 | klazz.get_context_data = self.get_get_context_data(klazz) 195 | return klazz 196 | 197 | def get_update_class_view(self): 198 | name = '{0}_{1}'.format(self.get_name(), 'UpdateView') 199 | options_dict = { 200 | 'kind': 'update', 201 | 'model': self.model, 202 | 'fields': '__all__', 203 | } 204 | if hasattr(self, 'form_template_name') and self.form_template_name: 205 | options_dict['template_name'] = self.form_template_name 206 | 207 | if hasattr(self, 'form_class') and self.form_class: 208 | options_dict['form_class'] = self.form_class 209 | options_dict['fields'] = None 210 | 211 | parent_classes_list = [FallbackTemplateMixin] 212 | parent_classes_list.extend(self.update_mixins) 213 | parent_classes_list.append(self.update_view_class) 214 | 215 | klazz = type(name, tuple(parent_classes_list), options_dict ) 216 | klazz.get_context_data = self.get_get_context_data(klazz) 217 | return klazz 218 | 219 | def get_delete_class_view(self): 220 | name = '{0}_{1}'.format(self.get_name(), 'DeleteView') 221 | options_dict = { 222 | 'model': self.model, 223 | 'kind': 'delete', 224 | 'fields': '__all__', 225 | 'get_success_url': lambda x: reverse(self.list_url_name), 226 | } 227 | if hasattr(self, 'delete_template_name') and self.delete_template_name: 228 | options_dict['template_name'] = self.delete_template_name 229 | 230 | parent_classes_list = [FallbackTemplateMixin] 231 | parent_classes_list.extend(self.delete_mixins) 232 | parent_classes_list.append(self.delete_view_class) 233 | 234 | klazz = type(name, tuple(parent_classes_list), options_dict ) 235 | klazz.get_context_data = self.get_get_context_data(klazz) 236 | return klazz 237 | 238 | def get_url_patterns(self, ): 239 | prefix = hasattr(self, 'prefix') and self.prefix or '' 240 | 241 | url_patterns = [ 242 | url(r'^'+prefix+'$', self.perms['list'](self.get_list_class_view().as_view()), name=self.list_url_name, ), 243 | url(r'^'+prefix+'create/$', self.perms['create'](self.get_create_class_view().as_view()), name=self.create_url_name ), 244 | url(r'^'+prefix+'detail/(?P\d+)$', self.perms['detail'](self.get_detail_class_view().as_view()), name=self.detail_url_name ), 245 | url(r'^'+prefix+'update/(?P\d+)$', self.perms['update'](self.get_update_class_view().as_view()), name=self.update_url_name ), 246 | url(r'^'+prefix+'delete/(?P\d+)$', self.perms['delete'](self.get_delete_class_view().as_view()), name=self.delete_url_name ), 247 | ] 248 | 249 | if django.VERSION >= (1, 8, 0): 250 | return url_patterns 251 | else: 252 | from django.conf.urls import patterns 253 | return patterns('', *url_patterns) 254 | 255 | @classmethod 256 | def get_url_names(cls, prefix=None, model_class=None): 257 | for r in cls._registry: 258 | if r.prefix==prefix or model_class and r.model==model_class: 259 | return { 260 | 'list': r.list_url_name, 261 | 'create': r.create_url_name, 262 | 'update': r.update_url_name, 263 | 'delete': r.delete_url_name, 264 | 'detail': r.detail_url_name, 265 | } 266 | 267 | def get_url_names(app=None, model=None, prefix=None): 268 | model_class = None 269 | if app and model: 270 | model_class = get_model(app, model) 271 | return CrudManager.get_url_names(prefix=prefix, model_class=model_class) 272 | -------------------------------------------------------------------------------- /publish.bat: -------------------------------------------------------------------------------- 1 | python setup.py sdist 2 | twine upload dist\* 3 | -------------------------------------------------------------------------------- /quicktest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import sys 6 | import argparse 7 | from django.conf import settings 8 | import django 9 | 10 | 11 | class QuickDjangoTest(object): 12 | """ 13 | A quick way to run the Django test suite without a fully-configured project. 14 | 15 | Example usage: 16 | 17 | >>> QuickDjangoTest(apps=['app1', 'app2'], db='sqlite') 18 | 19 | Based on a script published by Lukasz Dziedzia at: 20 | http://stackoverflow.com/questions/3841725/how-to-launch-tests-for-django-reusable-app 21 | """ 22 | DIRNAME = os.path.dirname(__file__) 23 | INSTALLED_APPS = [ 24 | 'django.contrib.staticfiles', 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'django.contrib.sessions', 28 | 'django.contrib.messages', 29 | 'django.contrib.admin', 30 | ] 31 | 32 | 33 | def __init__(self, *args, **kwargs): 34 | self.apps = kwargs.get('apps', []) 35 | self.database= kwargs.get('db', 'sqlite') 36 | self.run_tests() 37 | 38 | def run_tests(self): 39 | 40 | 41 | django_settings = { 42 | 'DATABASES':{ 43 | 'default': { 44 | 'ENGINE': 'django.db.backends.sqlite3', 45 | } 46 | }, 47 | 'INSTALLED_APPS':self.INSTALLED_APPS + self.apps, 48 | 'STATIC_URL':'/static/', 49 | 'ROOT_URLCONF':'generic_scaffold.tests', 50 | 'SILENCED_SYSTEM_CHECKS':['1_7.W001'], 51 | 'SECRET_KEY':'123', 52 | } 53 | 54 | 55 | django_settings['TEMPLATES'] = [{ 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'APP_DIRS': True, 58 | 'OPTIONS': { 59 | 'context_processors': [ 60 | 'django.template.context_processors.debug', 61 | 'django.template.context_processors.request', 62 | 'django.contrib.auth.context_processors.auth', 63 | 'django.contrib.messages.context_processors.messages', 64 | ], 65 | }, 66 | }] 67 | 68 | django_settings['MIDDLEWARE'] = [ 69 | 'django.contrib.sessions.middleware.SessionMiddleware', 70 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 71 | 'django.contrib.messages.middleware.MessageMiddleware' 72 | ] 73 | 74 | 75 | django_settings['DEFAULT_AUTO_FIELD'] = 'django.db.models.BigAutoField' 76 | 77 | settings.configure(**django_settings) 78 | 79 | 80 | django.setup() 81 | 82 | 83 | from django.test.runner import DiscoverRunner as Runner 84 | 85 | failures = Runner().run_tests(self.apps, verbosity=1) 86 | if failures: # pragma: no cover 87 | sys.exit(failures) 88 | 89 | if __name__ == '__main__': 90 | QuickDjangoTest(apps=['generic_scaffold'], db='sqlite3') 91 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | def readme(): 5 | with open('README.rst') as f: 6 | return f.read() 7 | 8 | setup( 9 | name='django-generic-scaffold', 10 | version='0.6.0', 11 | description='Generic scaffolding for Django', 12 | long_description=readme(), 13 | author='Serafeim Papastefanos', 14 | author_email='spapas@gmail.com', 15 | license='MIT', 16 | url='https://github.com/spapas/django-generic-scaffold/', 17 | zip_safe=False, 18 | include_package_data=True, 19 | packages=find_packages(exclude=['tests.*', 'tests',]), 20 | 21 | install_requires=['Django >=3.2', 'six'], 22 | 23 | classifiers=[ 24 | 'Environment :: Web Environment', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.8', 28 | 'Programming Language :: Python :: 3.11', 29 | 'Framework :: Django', 30 | 'Framework :: Django :: 3.0', 31 | 'Framework :: Django :: 3.1', 32 | 'Framework :: Django :: 3.2', 33 | 'Framework :: Django :: 4.0', 34 | 'Framework :: Django :: 4.1', 35 | 'Framework :: Django :: 4.2', 36 | 'Framework :: Django :: 5.0', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: MIT License', 39 | 'Operating System :: OS Independent', 40 | 'Topic :: Internet :: WWW/HTTP', 41 | 'Topic :: Software Development :: Libraries', 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py38}-django{32,40,41,42}, {py311}-django{32,40,41,42,50} 3 | 4 | [testenv] 5 | basepython = 6 | py38: python3.8 7 | py311: python3.11 8 | deps = 9 | pytest 10 | django32: Django>=3.2,<3.3 11 | django40: Django>=4.0,<4.1 12 | django41: Django>=4.1,<4.2 13 | django42: Django>=4.2,<4.3 14 | django50: Django>=5.0,<5.1 15 | 16 | commands = python quicktest.py 17 | 18 | [gh-actions] 19 | python = 20 | 3.8: py38 21 | 3.11: py311 --------------------------------------------------------------------------------