├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.py └── wagtailmodeladmin ├── __init__.py ├── forms.py ├── helpers.py ├── locale └── nl │ └── LC_MESSAGES │ └── django.po ├── menus.py ├── middleware.py ├── options.py ├── recipes ├── __init__.py └── readonly │ ├── __init__.py │ ├── helpers.py │ └── options.py ├── static └── wagtailmodeladmin │ └── css │ ├── choose_parent_page.css │ └── index.css ├── templates └── wagtailmodeladmin │ ├── choose_parent.html │ ├── confirm_delete.html │ ├── create.html │ ├── edit.html │ ├── includes │ ├── breadcrumb.html │ ├── button.html │ ├── filter.html │ ├── result_list.html │ ├── result_row.html │ ├── result_row_value.html │ └── search_form.html │ ├── index.html │ └── inspect.html ├── templatetags ├── __init__.py └── wagtailmodeladmin_tags.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .DS_Store 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andy Babic 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 LICENSE 2 | include README.rst 3 | recursive-include wagtailmodeladmin/static *.css 4 | recursive-include wagtailmodeladmin/templates *.html 5 | recursive-include wagtailmodeladmin/recipes/readonly/static *.css 6 | recursive-include wagtailmodeladmin/recipes/readonly/templates *.html 7 | recursive-include wagtailmodeladmin/locale *.po 8 | recursive-include wagtailmodeladmin *.py 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | What is wagtailmodeladmin? 2 | ========================== 3 | 4 | It's an extension for Torchbox's `Wagtail 5 | CMS `__ that allows you create 6 | customisable listing pages for any model in your Wagtail project, and 7 | have them appear in the navigation when you log into the admin area. 8 | Simply extend the ``ModelAdmin`` class, override a few attributes to 9 | suit your needs, link it into Wagtail using a few hooks (you can copy 10 | and paste from the examples below), and you're good to go. 11 | 12 | NOTE: ``wagtailmodeladmin`` is now part of Wagtail (from v1.5) 13 | ----------------------------------------------------------------- 14 | 15 | As of version ``1.5``, Wagtail now comes packaged with ``wagtailmodeladmin`` as a contrib app, ``wagtail.contrib.modeladmin``, so you no longer need to install it separately. However, the versions are not identical. Some underlying components/classes have been re-factored, so **there will likely be upgrade considerations for projects with more customised `ModelAdmin` implementations** (where views and helpers have been extended and overridden). 16 | 17 | On the plus side, the new version is better integrated, and provides a more consistent experience, as a developer and user. It also includes bug fixes, performance improvements, and tests, making this standalone version redundant. For these reasons, **this version will no longer be maintained**. 18 | 19 | View the documentation here: http://docs.wagtail.io/en/latest/reference/contrib/modeladmin.html 20 | 21 | Upgrading from ``wagtailmodeladmin`` to ``wagtail.contrib.modeladmin`` 22 | ---------------------------------------------------------------------- 23 | 24 | If you only used the core ``ModelAdmin`` and ``ModelAdminGroup`` classes in your projects, and made use of standard attributes/methods for customisation, migrating to the new version should be as simple as: 25 | 26 | 1. Follow the installation instructions outlined in the Wagtail docs: http://docs.wagtail.io/en/latest/reference/contrib/modeladmin.html. 27 | 28 | 2. In ``wagtail_hooks.py``, for each of your custom apps, import the core classes from their new locations in ``wagtail.contrib.modeladmin.options`` instead of ``wagtailmodeladmin.options``. 29 | 30 | 3. Import the ``wagtail.contrib.modeladmin.options.modeladmin_register`` method instead of ``wagtailmodeladmin.options.wagtailmodeladmin_register``, and use that to register your classes instead. 31 | 32 | 4. Remove the old app from your project, by removing ``wagtailmodeladmin`` from ``INSTALLED_APPS``, and ``wagtailmodeladmin.middleware.ModelAdminMiddleware`` from ``MIDDLEWARE_CLASSES`` in your project's settings. 33 | 34 | 35 | A full list of features: 36 | ------------------------ 37 | 38 | - A customisable list view, allowing you to control what values are 39 | displayed for each item, available filter options, default ordering, 40 | and more. 41 | - Access your list views from the CMS easily with automatically 42 | generated menu items, with automatic 'active item' highlighting. 43 | Control the label text and icons used with easy-to-change attributes 44 | on your class. 45 | - An additional ``ModelAdminGroup`` class, that allows you to group 46 | your related models, and list them together in their own submenu, for 47 | a more logical user experience. 48 | - Simple, robust **add** and **edit** views for your non-Page models 49 | that use the panel configurations defined on your model using 50 | Wagtail's edit panels. 51 | - For Page models, the system cleverly directs to Wagtail's existing 52 | add and edit views, and returns you back to the correct list page, 53 | for a seamless experience. 54 | - Full respect for permissions assigned to your Wagtail users and 55 | groups. Users will only be able to do what you want them to! 56 | - All you need to easily hook your ``ModelAdmin`` classes into Wagtail, 57 | taking care of URL registration, menu changes, and registering any 58 | missing model permissions, so that you can assign them to Groups. 59 | - **Built to be customisable** - While wagtailmodeladmin provides a 60 | solid experience out of the box, you can easily use your own 61 | templates, and the ``ModelAdmin`` class has a large number of methods 62 | that you can override or extend, allowing you to customise the 63 | behaviour to a greater degree. 64 | 65 | Supported list options: 66 | ----------------------- 67 | 68 | With the exception of bulk actions and date hierarchy, the 69 | ``ModelAdmin`` class offers similar list functionality to Django's 70 | ModelAdmin class, providing: 71 | 72 | - control over what values are displayed (via the ``list_display`` 73 | attribute) 74 | - control over default ordering (via the ``ordering`` attribute) 75 | - customisable model-specific text search (via the ``search_fields`` 76 | attribute) 77 | - customisable filters (via the ``list_filter`` attribue) 78 | 79 | ``list_display`` supports the same fields and methods as Django's 80 | ModelAdmin class (including ``short_description`` and 81 | ``admin_order_field`` on custom methods), giving you lots of flexibility 82 | when it comes to output. `Read more about list\_display in the Django 83 | docs `__. 84 | 85 | ``list_filter`` supports the same field types as Django's ModelAdmin 86 | class, giving your users an easy way to find what they're looking for. 87 | `Read more about list\_filter in the Django 88 | docs `__. 89 | 90 | Adding functionality, not taking it away 91 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 92 | 93 | wagtailmodeladmin doesn't interfere with what Wagtail does. If your 94 | model extends Wagtail's ``Page`` model, or is registered as a 95 | ``Snippet``, they'll still be appear in Wagtail's Snippet and Page views 96 | within the admin centre. wagtailmodeladmin simply adds an additional, 97 | alternative set of views, which you're in control of. 98 | 99 | How to install 100 | -------------- 101 | 102 | 1. Install the package using pip: ``pip install wagtailmodeladmin`` 103 | 2. Add ``wagtailmodeladmin`` to ``INSTALLED_APPS`` in your project 104 | settings 105 | 3. Add the ``wagtailmodeladmin.middleware.ModelAdminMiddleware`` class 106 | to ``MIDDLEWARE_CLASSES`` in your project settings (it should be fine 107 | at the end) 108 | 4. Add a ``wagtail_hooks.py`` file to your app's folder and extend the 109 | ``ModelAdmin``, and ``ModelAdminGroup`` classes to produce the 110 | desired effect 111 | 112 | A simple example 113 | ---------------- 114 | 115 | You have a model in your app, and you want a listing page specifically 116 | for that model, with a menu item added to the menu in Wagtail's CMS so 117 | that you can get to it. 118 | 119 | **wagtail\_hooks.py** in your app directory would look something like 120 | this: 121 | 122 | .. code:: python 123 | 124 | from wagtailmodeladmin.options import ModelAdmin, wagtailmodeladmin_register 125 | from .models import MyPageModel 126 | 127 | 128 | class MyPageModelAdmin(ModelAdmin): 129 | model = MyPageModel 130 | menu_label = 'Page Model' # ditch this to use verbose_name_plural from model 131 | menu_icon = 'date' # change as required 132 | menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd) 133 | add_to_settings_menu = False # or True to add your model to the Settings sub-menu 134 | list_display = ('title', 'example_field2', 'example_field3', 'live') 135 | list_filter = ('live', 'example_field2', 'example_field3') 136 | search_fields = ('title',) 137 | 138 | # Now you just need to register your customised ModelAdmin class with Wagtail 139 | wagtailmodeladmin_register(MyPageModelAdmin) 140 | 141 | The Wagtail CMS menu would look something like this: 142 | 143 | .. figure:: http://i.imgur.com/Ztb2aYf.png 144 | :alt: Simple example menu preview 145 | 146 | Simple example menu preview 147 | 148 | A more complicated example 149 | -------------------------- 150 | 151 | You have an app with several models that you want to show grouped 152 | together in Wagtail's admin menu. Some of the models might extend Page, 153 | and others might be simpler models, perhaps registered as Snippets, 154 | perhaps not. No problem! ModelAdminGroup allows you to group them all 155 | together nicely. 156 | 157 | **wagtail\_hooks.py** in your app directory would look something like 158 | this: 159 | 160 | .. code:: python 161 | 162 | from wagtailmodeladmin.options import ( 163 | ModelAdmin, ModelAdminGroup, wagtailmodeladmin_register) 164 | from .models import ( 165 | MyPageModel, MyOtherPageModel, MySnippetModel, SomeOtherModel) 166 | 167 | 168 | class MyPageModelAdmin(ModelAdmin): 169 | model = MyPageModel 170 | menu_label = 'Page Model' # ditch this to use verbose_name_plural from model 171 | menu_icon = 'doc-full-inverse' # change as required 172 | list_display = ('title', 'example_field2', 'example_field3', 'live') 173 | list_filter = ('live', 'example_field2', 'example_field3') 174 | search_fields = ('title',) 175 | 176 | 177 | class MyOtherPageModelAdmin(ModelAdmin): 178 | model = MyOtherPageModel 179 | menu_label = 'Other Page Model' # ditch this to use verbose_name_plural from model 180 | menu_icon = 'doc-full-inverse' # change as required 181 | list_display = ('title', 'example_field2', 'example_field3', 'live') 182 | list_filter = ('live', 'example_field2', 'example_field3') 183 | search_fields = ('title',) 184 | 185 | 186 | class MySnippetModelAdmin(ModelAdmin): 187 | model = MySnippetModel 188 | menu_label = 'Snippet Model' # ditch this to use verbose_name_plural from model 189 | menu_icon = 'snippet' # change as required 190 | list_display = ('title', 'example_field2', 'example_field3') 191 | list_filter = ('example_field2', 'example_field3') 192 | search_fields = ('title',) 193 | 194 | 195 | class SomeOtherModelAdmin(ModelAdmin): 196 | model = SomeOtherModel 197 | menu_label = 'Some other model' # ditch this to use verbose_name_plural from model 198 | menu_icon = 'snippet' # change as required 199 | list_display = ('title', 'example_field2', 'example_field3') 200 | list_filter = ('example_field2', 'example_field3') 201 | search_fields = ('title',) 202 | 203 | 204 | class MyModelAdminGroup(ModelAdminGroup): 205 | menu_label = 'My App' 206 | menu_icon = 'folder-open-inverse' # change as required 207 | menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd) 208 | items = (MyPageModelAdmin, MyOtherPageModelAdmin, MySnippetModelAdmin, SomeOtherModelAdmin) 209 | 210 | # When using a ModelAdminGroup class to group several ModelAdmin classes together, 211 | # you only need to register the ModelAdminGroup class with Wagtail: 212 | wagtailmodeladmin_register(MyModelAdminGroup) 213 | 214 | The Wagtail CMS menu would look something like this: 215 | 216 | .. figure:: http://i.imgur.com/skxP6ek.png 217 | :alt: Complex example menu preview 218 | 219 | Complex example menu preview 220 | 221 | Notes 222 | ----- 223 | 224 | - For a list of available icons that can be used, you can enable 225 | Wagtail's Styleguide 226 | (http://docs.wagtail.io/en/latest/contributing/styleguide.html), and 227 | view the page it creates in the CMS for you. The list of icons can be 228 | found toward the bottom of the page. 229 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | from wagtailmodeladmin import __version__ 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 6 | README = readme.read() 7 | 8 | # allow setup.py to be run from any path 9 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 10 | 11 | setup( 12 | name="wagtailmodeladmin", 13 | version=__version__, 14 | author="Andy Babic", 15 | author_email="ababic@rkh.co.uk", 16 | description="Customisable 'django-admin' style listing pages for Wagtail", 17 | long_description=README, 18 | packages=find_packages(), 19 | license="MIT", 20 | keywords="wagtail cms model utility", 21 | download_url="https://github.com/rkhleics/wagtailmodeladmin/tarball/0.1", 22 | url="https://github.com/rkhleics/wagtailmodeladmin", 23 | include_package_data=True, 24 | zip_safe=False, 25 | classifiers=[ 26 | "Environment :: Web Environment", 27 | "Framework :: Django", 28 | "Intended Audience :: Developers", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python", 31 | 'Topic :: Internet :: WWW/HTTP', 32 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 33 | ], 34 | install_requires=[ 35 | "wagtail>=0.8.7", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /wagtailmodeladmin/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.5.8' 2 | -------------------------------------------------------------------------------- /wagtailmodeladmin/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import ugettext as _ 3 | from wagtail.wagtailcore.models import Page 4 | from django.utils.safestring import mark_safe 5 | 6 | 7 | class CustomModelChoiceField(forms.ModelChoiceField): 8 | def label_from_instance(self, obj): 9 | bits = [] 10 | for ancestor in obj.get_ancestors(inclusive=True).exclude(depth=1): 11 | bits.append(ancestor.title) 12 | return mark_safe(''.join(bits)) 13 | 14 | 15 | class ParentChooserForm(forms.Form): 16 | parent_page = CustomModelChoiceField( 17 | label=_('Put it under'), 18 | required=True, 19 | empty_label=None, 20 | queryset=Page.objects.none(), 21 | widget=forms.RadioSelect(), 22 | ) 23 | 24 | def __init__(self, valid_parents_qs, *args, **kwargs): 25 | self.valid_parents_qs = valid_parents_qs 26 | super(ParentChooserForm, self).__init__(*args, **kwargs) 27 | self.fields['parent_page'].queryset = self.valid_parents_qs 28 | -------------------------------------------------------------------------------- /wagtailmodeladmin/helpers.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | from django.contrib.auth import get_permission_codename 3 | from django.contrib.auth.models import Permission 4 | from django.utils.translation import ugettext as _ 5 | from django.utils.encoding import force_text 6 | from django.contrib.admin.utils import quote 7 | from django.core.urlresolvers import reverse 8 | from wagtail.wagtailcore.models import Page 9 | 10 | 11 | class PermissionHelper(object): 12 | """ 13 | Provides permission-related helper functions to help determine what a 14 | user can do with a 'typical' model (where permissions are granted 15 | model-wide). 16 | """ 17 | 18 | def __init__(self, model): 19 | self.model = model 20 | self.opts = model._meta 21 | 22 | def get_all_model_permissions(self): 23 | return Permission.objects.filter( 24 | content_type__app_label=self.opts.app_label, 25 | content_type__model=self.opts.model_name, 26 | ) 27 | 28 | def has_specific_permission(self, user, codename): 29 | return user.has_perm("%s.%s" % (self.opts.app_label, codename)) 30 | 31 | def has_any_permissions(self, user): 32 | """ 33 | Return a boolean to indicate whether the supplied user has any 34 | permissions at all on the associated model 35 | """ 36 | for perm in self.get_all_model_permissions(): 37 | if self.has_specific_permission(user, perm.codename): 38 | return True 39 | return False 40 | 41 | def has_add_permission(self, user): 42 | """ 43 | For typical models, whether or not a user can add an object depends 44 | on their permissions on that model 45 | """ 46 | return self.has_specific_permission( 47 | user, get_permission_codename('add', self.opts)) 48 | 49 | def has_edit_permission(self, user): 50 | """ 51 | For typical models, whether or not a user can edit an object depends 52 | on their permissions on that model 53 | """ 54 | return self.has_specific_permission( 55 | user, get_permission_codename('change', self.opts)) 56 | 57 | def has_delete_permission(self, user): 58 | """ 59 | For typical models, whether or not a user can delete an object depends 60 | on their permissions on that model 61 | """ 62 | return self.has_specific_permission( 63 | user, get_permission_codename('delete', self.opts)) 64 | 65 | def has_list_permission(self, user): 66 | return self.has_any_permissions(user) 67 | 68 | def can_edit_object(self, user, obj): 69 | """ 70 | Used from within templates to decide what functionality to allow 71 | for a specific object. For typical models, we just return the 72 | model-wide permission. 73 | """ 74 | return self.has_edit_permission(user) 75 | 76 | def can_delete_object(self, user, obj): 77 | """ 78 | Used from within templates to decide what functionality to allow 79 | for a specific object. For typical models, we just return the 80 | model-wide permission. 81 | """ 82 | return self.has_delete_permission(user) 83 | 84 | def can_unpublish_object(self, user, obj): 85 | """ 86 | 'Unpublishing' isn't really a valid option for models not extending 87 | Page, so we always return False 88 | """ 89 | return False 90 | 91 | def can_copy_object(self, user, obj): 92 | """ 93 | 'Copying' isn't really a valid option for models not extending 94 | Page, so we always return False 95 | """ 96 | return False 97 | 98 | 99 | class PagePermissionHelper(PermissionHelper): 100 | """ 101 | Provides permission-related helper functions to help determine what 102 | a user can do with a model extending Wagtail's Page model. It differs 103 | from `PermissionHelper`, because model-wide permissions aren't really 104 | relevant. We generally need to determine permissions on an 105 | object-specific basis. 106 | """ 107 | 108 | def get_valid_parent_pages(self, user): 109 | """ 110 | Identifies possible parent pages for the current user by first looking 111 | at allowed_parent_page_models() on self.model to limit options to the 112 | correct type of page, then checking permissions on those individual 113 | pages to make sure we have permission to add a subpage to it. 114 | """ 115 | # Start with empty qs 116 | parents_qs = Page.objects.none() 117 | 118 | # Add pages of the correct type 119 | for pt in self.model.allowed_parent_page_models(): 120 | pt_items = Page.objects.type(pt) 121 | parents_qs = parents_qs | pt_items 122 | 123 | # Exclude pages that we can't add subpages to 124 | for page in parents_qs.all(): 125 | if not page.permissions_for_user(user).can_add_subpage(): 126 | parents_qs = parents_qs.exclude(pk=page.pk) 127 | 128 | return parents_qs 129 | 130 | def has_list_permssion(self, user): 131 | """ 132 | For models extending Page, permitted actions are determined by 133 | permissions on individual objects. Rather than check for change 134 | permissions on every object individually (which would be quite 135 | resource intensive), we simply always allow the list view to be 136 | viewed, and limit further functionality when relevant. 137 | """ 138 | return True 139 | 140 | def has_add_permission(self, user): 141 | """ 142 | For models extending Page, whether or not a page of this type can be 143 | added somewhere in the tree essentially determines the add permission, 144 | rather than actual model-wide permissions 145 | """ 146 | return self.get_valid_parent_pages(user).count() > 0 147 | 148 | def can_edit_object(self, user, obj): 149 | perms = obj.permissions_for_user(user) 150 | return perms.can_edit() 151 | 152 | def can_delete_object(self, user, obj): 153 | perms = obj.permissions_for_user(user) 154 | return perms.can_delete() 155 | 156 | def can_unpublish_object(self, user, obj): 157 | perms = obj.permissions_for_user(user) 158 | return obj.live and perms.can_unpublish() 159 | 160 | def can_copy_object(self, user, obj): 161 | parent_page = obj.get_parent() 162 | return parent_page.permissions_for_user(user).can_publish_subpage() 163 | 164 | 165 | def get_url_pattern(model_meta, action=None): 166 | if not action: 167 | return r'^modeladmin/%s/%s/$' % ( 168 | model_meta.app_label, model_meta.model_name) 169 | return r'^modeladmin/%s/%s/%s/$' % ( 170 | model_meta.app_label, model_meta.model_name, action) 171 | 172 | 173 | def get_object_specific_url_pattern(model_meta, action): 174 | return r'^modeladmin/%s/%s/%s/(?P[-\w]+)/$' % ( 175 | model_meta.app_label, model_meta.model_name, action) 176 | 177 | 178 | def get_url_name(model_meta, action='index'): 179 | return '%s_%s_modeladmin_%s/' % ( 180 | model_meta.app_label, model_meta.model_name, action) 181 | 182 | 183 | class ButtonHelper(object): 184 | 185 | default_button_classnames = ['button'] 186 | add_button_classnames = ['bicolor', 'icon', 'icon-plus'] 187 | inspect_button_classnames = [] 188 | edit_button_classnames = [] 189 | delete_button_classnames = ['no'] 190 | 191 | def __init__(self, model, permission_helper, user, 192 | inspect_view_enabled=False): 193 | self.user = user 194 | self.model = model 195 | self.opts = model._meta 196 | self.permission_helper = permission_helper 197 | self.inspect_view_enabled = inspect_view_enabled 198 | self.model_name = force_text(self.opts.verbose_name).lower() 199 | 200 | def finalise_classname(self, classnames_add=[], classnames_exclude=[]): 201 | combined = self.default_button_classnames + classnames_add 202 | finalised = [cn for cn in combined if cn not in classnames_exclude] 203 | return ' '.join(finalised) 204 | 205 | def get_action_url(self, action='index', pk=None): 206 | kwargs = {} 207 | if pk and action not in ('create', 'index'): 208 | kwargs.update({'object_id': pk}) 209 | return reverse(get_url_name(self.opts, action), kwargs=kwargs) 210 | 211 | def show_add_button(self): 212 | return self.permission_helper.has_add_permission(self.user) 213 | 214 | def add_button(self, classnames_add=[], classnames_exclude=[]): 215 | classnames = self.add_button_classnames + classnames_add 216 | cn = self.finalise_classname(classnames, classnames_exclude) 217 | return { 218 | 'url': self.get_action_url('create'), 219 | 'label': _('Add %s') % self.model_name, 220 | 'classname': cn, 221 | 'title': _('Add a new %s') % self.model_name, 222 | } 223 | 224 | def inspect_button(self, pk, classnames_add=[], classnames_exclude=[]): 225 | classnames = self.inspect_button_classnames + classnames_add 226 | cn = self.finalise_classname(classnames, classnames_exclude) 227 | return { 228 | 'url': self.get_action_url('inspect', pk), 229 | 'label': _('Inspect'), 230 | 'classname': cn, 231 | 'title': _('View details for this %s') % self.model_name, 232 | } 233 | 234 | def edit_button(self, pk, classnames_add=[], classnames_exclude=[]): 235 | classnames = self.edit_button_classnames + classnames_add 236 | cn = self.finalise_classname(classnames, classnames_exclude) 237 | return { 238 | 'url': self.get_action_url('edit', pk), 239 | 'label': _('Edit'), 240 | 'classname': cn, 241 | 'title': _('Edit this %s') % self.model_name, 242 | } 243 | 244 | def delete_button(self, pk, classnames_add=[], classnames_exclude=[]): 245 | classnames = self.delete_button_classnames + classnames_add 246 | cn = self.finalise_classname(classnames, classnames_exclude) 247 | return { 248 | 'url': self.get_action_url('confirm_delete', pk), 249 | 'label': _('Delete'), 250 | 'classname': cn, 251 | 'title': _('Delete this %s') % self.model_name, 252 | } 253 | 254 | def get_buttons_for_obj(self, obj, exclude=[], classnames_add=[], 255 | classnames_exclude=[]): 256 | ph = self.permission_helper 257 | pk = quote(getattr(obj, self.opts.pk.attname)) 258 | btns = [] 259 | if('inspect' not in exclude and self.inspect_view_enabled): 260 | btns.append( 261 | self.inspect_button(pk, classnames_add, classnames_exclude) 262 | ) 263 | if('edit' not in exclude and ph.can_edit_object(self.user, obj)): 264 | btns.append( 265 | self.edit_button(pk, classnames_add, classnames_exclude) 266 | ) 267 | if('delete' not in exclude and ph.can_delete_object(self.user, obj)): 268 | btns.append( 269 | self.delete_button(pk, classnames_add, classnames_exclude) 270 | ) 271 | return btns 272 | 273 | 274 | class PageButtonHelper(ButtonHelper): 275 | 276 | unpublish_button_classnames = [] 277 | copy_button_classnames = [] 278 | 279 | def unpublish_button(self, pk, classnames_add=[], classnames_exclude=[]): 280 | classnames = self.unpublish_button_classnames + classnames_add 281 | cn = self.finalise_classname(classnames, classnames_exclude) 282 | return { 283 | 'url': self.get_action_url('unpublish', pk), 284 | 'label': _('Unpublish'), 285 | 'classname': cn, 286 | 'title': _('Unpublish this %s') % self.model_name, 287 | } 288 | 289 | def copy_button(self, pk, classnames_add=[], classnames_exclude=[]): 290 | classnames = self.copy_button_classnames + classnames_add 291 | cn = self.finalise_classname(classnames, classnames_exclude) 292 | return { 293 | 'url': self.get_action_url('copy', pk), 294 | 'label': _('Copy'), 295 | 'classname': cn, 296 | 'title': _('Copy this %s') % self.model_name, 297 | } 298 | 299 | def get_buttons_for_obj(self, obj, exclude=[], classnames_add=[], 300 | classnames_exclude=[]): 301 | user = self.user 302 | ph = self.permission_helper 303 | pk = quote(getattr(obj, self.opts.pk.attname)) 304 | btns = [] 305 | if('inspect' not in exclude and self.inspect_view_enabled): 306 | btns.append( 307 | self.inspect_button(pk, classnames_add, classnames_exclude) 308 | ) 309 | if('edit' not in exclude and ph.can_edit_object(user, obj)): 310 | btns.append( 311 | self.edit_button(pk, classnames_add, classnames_exclude) 312 | ) 313 | if('copy' not in exclude and ph.can_copy_object(user, obj)): 314 | btns.append( 315 | self.copy_button(pk, classnames_add, classnames_exclude) 316 | ) 317 | if('unpublish' not in exclude and ph.can_unpublish_object(user, obj)): 318 | btns.append( 319 | self.unpublish_button(pk, classnames_add, classnames_exclude) 320 | ) 321 | if('delete' not in exclude and ph.can_delete_object(user, obj)): 322 | btns.append( 323 | self.delete_button(pk, classnames_add, classnames_exclude) 324 | ) 325 | return btns 326 | -------------------------------------------------------------------------------- /wagtailmodeladmin/locale/nl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Thijs Kramer , 2015 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Wagtailmodeladmin\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2015-12-15 17:49+0100\n" 12 | "PO-Revision-Date: 2015-12-15 17:30+0100\n" 13 | "Last-Translator: Thijs Kramer \n" 14 | "Language: nl_NL\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: forms.py:16 21 | msgid "Put it under" 22 | msgstr "Plaats het onder" 23 | 24 | #: templates/wagtailmodeladmin/choose_parent.html:12 25 | msgid "Where should it go?" 26 | msgstr "Waar moet het geplaatst worden?" 27 | 28 | #: templates/wagtailmodeladmin/choose_parent.html:13 29 | #, python-format 30 | msgid "" 31 | "%(plural)s can be added to more than one place within your site. Where would " 32 | "you like this new one to go?" 33 | msgstr "" 34 | "%(plural)s kunnen op meerdere plaatsen in uw website toegevoegd worden. Waar " 35 | "wilt u dat een nieuwe geplaatst wordt?" 36 | 37 | #: templates/wagtailmodeladmin/choose_parent.html:21 38 | msgid "Continue" 39 | msgstr "Doorgaan" 40 | 41 | #: templates/wagtailmodeladmin/confirm_delete.html:13 42 | msgid "Yes, delete" 43 | msgstr "Ja, verwijderen" 44 | 45 | #: templates/wagtailmodeladmin/create.html:20 46 | #: templates/wagtailmodeladmin/edit.html:8 47 | msgid "Save" 48 | msgstr "Opslaan" 49 | 50 | #: templates/wagtailmodeladmin/edit.html:12 utils.py:59 views.py:762 51 | msgid "Delete" 52 | msgstr "Verwijderen" 53 | 54 | #: templates/wagtailmodeladmin/includes/result_list.html:25 55 | #, python-format 56 | msgid "Sorry, there are no %(name)s matching your search parameters." 57 | msgstr "Sorry, er zijn geen %(name)s die overeenkomen met uw zoekterm." 58 | 59 | #: templates/wagtailmodeladmin/includes/search_form.html:7 60 | msgid "Search for" 61 | msgstr "Zoek op" 62 | 63 | #: templates/wagtailmodeladmin/includes/search_form.html:10 64 | #, python-format 65 | msgid "Search %(name)s" 66 | msgstr "Zoek %(name)s" 67 | 68 | #: templates/wagtailmodeladmin/index.html:29 69 | #, python-format 70 | msgid "Add %(name)s" 71 | msgstr "Voeg %(name)s toe" 72 | 73 | #: templates/wagtailmodeladmin/index.html:42 74 | msgid "Filter" 75 | msgstr "Filteren" 76 | 77 | #: templates/wagtailmodeladmin/index.html:56 78 | #, python-format 79 | msgid "" 80 | "No %(name)s have been created yet. One of the following must be added to " 81 | "your site before any %(name)s can be added." 82 | msgstr "" 83 | "Er is nog geen %(name)s aangemaakt. Een van de volgende items moet eerst " 84 | "toegevoegd worden voordat u een %(name)s kunt aanmaken." 85 | 86 | #: templates/wagtailmodeladmin/index.html:61 87 | #, python-format 88 | msgid "No %(name)s have been created yet." 89 | msgstr "Er zijn nog een %(name)s aangemaakt." 90 | 91 | #: templates/wagtailmodeladmin/index.html:63 92 | #, fuzzy, python-format 93 | #| msgid "" 94 | #| "\n" 95 | #| " Why not add one? \n" 97 | #| " " 98 | msgid "" 99 | "\n" 100 | " Why not add one?\n" 101 | " " 102 | msgstr "" 103 | "\n" 104 | " Waarom voegt u er niet " 105 | "een toe? \n" 106 | " " 107 | 108 | #: templates/wagtailmodeladmin/index.html:77 109 | #, python-format 110 | msgid "Page %(current_page)s of %(num_pages)s." 111 | msgstr "Pagina %(current_page)s van %(num_pages)s." 112 | 113 | #: templatetags/wagtailmodeladmin_tags.py:96 114 | msgid "Previous" 115 | msgstr "Vorige" 116 | 117 | #: templatetags/wagtailmodeladmin_tags.py:107 118 | msgid "Next" 119 | msgstr "Volgende" 120 | 121 | #: utils.py:31 122 | msgid "Sorry, you do not have permission to access this area." 123 | msgstr "Helaas, u heeft geen toegang tot dit onderdeel." 124 | 125 | #: utils.py:51 126 | #, python-format 127 | msgid "Edit this %s" 128 | msgstr "Wijzig %s" 129 | 130 | #: utils.py:52 views.py:171 131 | msgid "Edit" 132 | msgstr "Wijzig" 133 | 134 | #: utils.py:58 135 | #, python-format 136 | msgid "Delete this %s" 137 | msgstr "Verwijder %s" 138 | 139 | #: utils.py:65 140 | #, python-format 141 | msgid "Unpublish this %s" 142 | msgstr "Publicatie ongedaan maken van %s" 143 | 144 | #: utils.py:66 145 | msgid "Unpublish" 146 | msgstr "Publicatie ongedaan maken" 147 | 148 | #: utils.py:72 149 | #, python-format 150 | msgid "Copy this %s" 151 | msgstr "Kopieer %s" 152 | 153 | #: utils.py:73 154 | msgid "Copy" 155 | msgstr "Kopieer" 156 | 157 | #: views.py:166 158 | #, python-brace-format 159 | msgid "{model_name} '{instance}' created." 160 | msgstr "{model_name} '{instance}' is aangemaakt." 161 | 162 | #: views.py:176 163 | #, python-format 164 | msgid "The %s could not be created due to errors." 165 | msgstr "%s kon vanwege foutmeldingen niet aangemaakt worden." 166 | 167 | #: views.py:661 168 | msgid "New" 169 | msgstr "Nieuw" 170 | 171 | #: views.py:687 172 | #, python-format 173 | msgid "Create new %s" 174 | msgstr "Maak nieuw %s aan" 175 | 176 | #: views.py:703 177 | #, python-format 178 | msgid "Add %s" 179 | msgstr "Voeg %s toe" 180 | 181 | #: views.py:728 182 | msgid "Editing" 183 | msgstr "Bezig met wijzigen" 184 | 185 | #: views.py:744 186 | #, python-format 187 | msgid "Editing %s" 188 | msgstr "Bezig met wijzigen van %s" 189 | 190 | #: views.py:750 191 | #, python-brace-format 192 | msgid "{model_name} '{instance}' updated." 193 | msgstr "{model_name} '{instance}' is aangepast." 194 | 195 | #: views.py:755 196 | #, python-format 197 | msgid "The %s could not be saved due to errors." 198 | msgstr "%s kon vanwege foutmeldingen niet opgeslagen worden." 199 | 200 | #: views.py:779 201 | #, python-format 202 | msgid "Confirm deletion of %s" 203 | msgstr "Bevestig het verwijderen van %s" 204 | 205 | #: views.py:786 206 | #, python-format 207 | msgid "" 208 | "Are you sure you want to delete this %s? If other things in your site are " 209 | "related to it, they may also be affected." 210 | msgstr "" 211 | "Weet u zeker dat u dit item van het type %s wilt verwijderen? Dit heeft " 212 | "mogelijk gevolgen voor andere zaken in uw site, mochten die naar dit item " 213 | "verwijzen." 214 | 215 | #: views.py:796 216 | #, python-brace-format 217 | msgid "{model_name} '{instance}' deleted." 218 | msgstr "{model_name} '{instance}' is verwijderd." 219 | -------------------------------------------------------------------------------- /wagtailmodeladmin/menus.py: -------------------------------------------------------------------------------- 1 | from wagtail.wagtailadmin.menu import Menu, MenuItem, SubmenuMenuItem 2 | 3 | 4 | class ModelAdminMenuItem(MenuItem): 5 | """ 6 | A sub-class of wagtail's MenuItem, used by PageModelAdmin to add a link 7 | to it's listing page 8 | """ 9 | def __init__(self, model_admin, order): 10 | self.model_admin = model_admin 11 | self.model = model_admin.model 12 | self.opts = model_admin.model._meta 13 | classnames = 'icon icon-%s' % model_admin.get_menu_icon() 14 | super(ModelAdminMenuItem, self).__init__( 15 | label=model_admin.get_menu_label(), url=model_admin.get_index_url(), 16 | classnames=classnames, order=order) 17 | 18 | def is_shown(self, request): 19 | return self.model_admin.show_menu_item(request) 20 | 21 | 22 | class GroupMenuItem(SubmenuMenuItem): 23 | """ 24 | A sub-class of wagtail's SubmenuMenuItem, used by ModelAdminGroup to add a 25 | link to the admin menu with it's own submenu, linking to various listing 26 | pages 27 | """ 28 | def __init__(self, modeladmingroup, order, menu): 29 | classnames = 'icon icon-%s' % modeladmingroup.get_menu_icon() 30 | super(GroupMenuItem, self).__init__( 31 | label=modeladmingroup.get_menu_label(), menu=menu, 32 | classnames=classnames, order=order, ) 33 | 34 | def is_shown(self, request): 35 | """ 36 | If there aren't any visible items in the submenu, don't bother to show 37 | this menu item 38 | """ 39 | for menuitem in self.menu._registered_menu_items: 40 | if menuitem.is_shown(request): 41 | return True 42 | return False 43 | 44 | 45 | class SubMenu(Menu): 46 | """ 47 | A sub-class of wagtail's Menu, used by AppModelAdmin. We just want to 48 | override __init__, so that we can specify the items to include on 49 | initialisation 50 | """ 51 | def __init__(self, menuitem_list): 52 | self._registered_menu_items = menuitem_list 53 | self.construct_hook_name = None 54 | -------------------------------------------------------------------------------- /wagtailmodeladmin/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.six.moves.urllib.parse import urlparse 2 | from django.http import HttpResponseRedirect 3 | from django.core.urlresolvers import resolve, Resolver404 4 | 5 | 6 | class ModelAdminMiddleware(object): 7 | """ 8 | Whenever loading wagtail's wagtailadmin_explore views, we check the session 9 | for a `return_to_list_url` value (set by some views), to see if the user 10 | should be redirected to a custom list view instead, and if so, redirect 11 | them to it. 12 | """ 13 | 14 | def process_request(self, request): 15 | """ 16 | Ignore unnecessary actions for static file requests, posts, or ajax 17 | requests. We're only interested in redirecting following a 'natural' 18 | request redirection to the `wagtailadmin_explore_root` or 19 | `wagtailadmin_explore` views. 20 | """ 21 | referer_url = request.META.get('HTTP_REFERER') 22 | return_to_index_url = request.session.get('return_to_index_url') 23 | 24 | try: 25 | if all(( 26 | return_to_index_url, 27 | referer_url, 28 | request.method == 'GET', 29 | not request.is_ajax(), 30 | resolve(request.path).url_name in ('wagtailadmin_explore_root', 31 | 'wagtailadmin_explore'), 32 | )): 33 | perform_redirection = False 34 | referer_match = resolve(urlparse(referer_url).path) 35 | if all(( 36 | referer_match.namespace == 'wagtailadmin_pages', 37 | referer_match.url_name in ( 38 | 'add', 39 | 'edit', 40 | 'delete', 41 | 'unpublish', 42 | 'copy' 43 | ), 44 | )): 45 | perform_redirection = True 46 | elif all(( 47 | not referer_match.namespace, 48 | referer_match.url_name in ( 49 | 'wagtailadmin_pages_create', 50 | 'wagtailadmin_pages_edit', 51 | 'wagtailadmin_pages_delete', 52 | 'wagtailadmin_pages_unpublish' 53 | ), 54 | )): 55 | perform_redirection = True 56 | if perform_redirection: 57 | del request.session['return_to_index_url'] 58 | return HttpResponseRedirect(return_to_index_url) 59 | 60 | except Resolver404: 61 | pass 62 | 63 | return None 64 | -------------------------------------------------------------------------------- /wagtailmodeladmin/options.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from django.contrib.auth.models import Permission 4 | from django.conf.urls import url 5 | from django.core.urlresolvers import reverse 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.db.models import Model 8 | from django.forms.widgets import flatatt 9 | from django.utils.translation import ugettext_lazy as _ 10 | from django.utils.safestring import mark_safe 11 | 12 | from wagtail.wagtailcore.models import Page 13 | from wagtail.wagtailimages.models import Filter 14 | from wagtail.wagtailcore import hooks 15 | 16 | from .menus import ModelAdminMenuItem, GroupMenuItem, SubMenu 17 | from .helpers import ( 18 | PermissionHelper, PagePermissionHelper, ButtonHelper, PageButtonHelper, 19 | get_url_pattern, get_object_specific_url_pattern, get_url_name) 20 | from .views import ( 21 | IndexView, InspectView, CreateView, ChooseParentView, EditView, 22 | ConfirmDeleteView, CopyRedirectView, UnpublishRedirectView) 23 | 24 | 25 | class WagtailRegisterable(object): 26 | """ 27 | Base class, providing a more convenient way for ModelAdmin or 28 | ModelAdminGroup instances to be registered with Wagtail's admin area. 29 | """ 30 | add_to_settings_menu = False 31 | 32 | def register_with_wagtail(self): 33 | 34 | @hooks.register('register_permissions') 35 | def register_permissions(): 36 | return self.get_permissions_for_registration() 37 | 38 | @hooks.register('register_admin_urls') 39 | def register_admin_urls(): 40 | return self.get_admin_urls_for_registration() 41 | 42 | menu_hook = ( 43 | 'register_settings_menu_item' if self.add_to_settings_menu else 44 | 'register_admin_menu_item' 45 | ) 46 | 47 | @hooks.register(menu_hook) 48 | def register_admin_menu_item(): 49 | return self.get_menu_item() 50 | 51 | 52 | class ThumbmnailMixin(object): 53 | """ 54 | Mixin class to help display thumbnail images in ModelAdmin listing results. 55 | `thumb_image_field_name` must be overridden to name a ForeignKey field on 56 | your model, linking to `wagtailimages.Image`. 57 | """ 58 | thumb_image_field_name = 'image' 59 | thumb_image_filter_spec = 'fill-100x100' 60 | thumb_image_width = 50 61 | thumb_classname = 'admin-thumb' 62 | thumb_col_header_text = _('image') 63 | thumb_default = None 64 | 65 | def admin_thumb(self, obj): 66 | try: 67 | image = getattr(obj, self.thumb_image_field_name, None) 68 | except AttributeError: 69 | raise ImproperlyConfigured( 70 | u"The `thumb_image_field_name` attribute on your `%s` class " 71 | "must name a field on your model." % self.__class__.__name__ 72 | ) 73 | 74 | img_attrs = { 75 | 'src': self.thumb_default, 76 | 'width': self.thumb_image_width, 77 | 'class': self.thumb_classname, 78 | } 79 | if image: 80 | fltr, _ = Filter.objects.get_or_create( 81 | spec=self.thumb_image_filter_spec) 82 | img_attrs.update({'src': image.get_rendition(fltr).url}) 83 | return mark_safe(''.format(flatatt(img_attrs))) 84 | elif self.thumb_default: 85 | return mark_safe(''.format(flatatt(img_attrs))) 86 | return '' 87 | admin_thumb.short_description = thumb_col_header_text 88 | 89 | 90 | class ModelAdmin(WagtailRegisterable): 91 | """ 92 | The core wagtailmodeladmin class. It provides an alternative means to 93 | list and manage instances of a given 'model' within Wagtail's admin area. 94 | It is essentially comprised of attributes and methods that allow a degree 95 | of control over how the data is represented, and other methods to make the 96 | additional functionality available via various Wagtail hooks. 97 | """ 98 | 99 | model = None 100 | menu_label = None 101 | menu_icon = None 102 | menu_order = None 103 | list_display = ('__str__',) 104 | list_display_add_buttons = None 105 | inspect_view_fields = None 106 | inspect_view_fields_exclude = [] 107 | inspect_view_enabled = False 108 | empty_value_display = '-' 109 | list_filter = () 110 | list_select_related = False 111 | list_per_page = 100 112 | search_fields = None 113 | ordering = None 114 | parent = None 115 | index_view_class = IndexView 116 | create_view_class = CreateView 117 | inspect_view_class = InspectView 118 | edit_view_class = EditView 119 | confirm_delete_view_class = ConfirmDeleteView 120 | choose_parent_view_class = ChooseParentView 121 | copy_view_class = CopyRedirectView 122 | unpublish_view_class = UnpublishRedirectView 123 | index_template_name = '' 124 | create_template_name = '' 125 | edit_template_name = '' 126 | inspect_template_name = '' 127 | confirm_delete_template_name = '' 128 | choose_parent_template_name = '' 129 | permission_helper_class = None 130 | button_helper_class = None 131 | index_view_extra_css = [] 132 | index_view_extra_js = [] 133 | inspect_view_extra_css = [] 134 | inspect_view_extra_js = [] 135 | form_view_extra_css = [] 136 | form_view_extra_js = [] 137 | 138 | def __init__(self, parent=None): 139 | """ 140 | Don't allow initialisation unless self.model is set to a valid model 141 | """ 142 | if not self.model or not issubclass(self.model, Model): 143 | raise ImproperlyConfigured( 144 | u"The model attribute on your '%s' class must be set, and " 145 | "must be a valid Django model." % self.__class__.__name__) 146 | self.opts = self.model._meta 147 | self.is_pagemodel = issubclass(self.model, Page) 148 | self.parent = parent 149 | permission_helper_class = self.get_permission_helper_class() 150 | self.permission_helper = permission_helper_class(self.model) 151 | 152 | def get_permission_helper_class(self): 153 | if self.permission_helper_class: 154 | return self.permission_helper_class 155 | if self.is_pagemodel: 156 | return PagePermissionHelper 157 | return PermissionHelper 158 | 159 | def get_button_helper_class(self): 160 | if self.button_helper_class: 161 | return self.button_helper_class 162 | if self.is_pagemodel: 163 | return PageButtonHelper 164 | return ButtonHelper 165 | 166 | def get_menu_label(self): 167 | """ 168 | Returns the label text to be used for the menu item 169 | """ 170 | return self.menu_label or self.opts.verbose_name_plural.title() 171 | 172 | def get_menu_icon(self): 173 | """ 174 | Returns the icon to be used for the menu item. The value is prepended 175 | with 'icon-' to create the full icon class name. For design 176 | consistency, the same icon is also applied to the main heading for 177 | views called by this class 178 | """ 179 | if self.menu_icon: 180 | return self.menu_icon 181 | if self.is_pagemodel: 182 | return 'doc-full-inverse' 183 | return 'snippet' 184 | 185 | def get_menu_order(self): 186 | """ 187 | Returns the 'order' to be applied to the menu item. 000 being first 188 | place. Where ModelAdminGroup is used, the menu_order value should be 189 | applied to that, and any ModelAdmin classes added to 'items' 190 | attribute will be ordered automatically, based on their order in that 191 | sequence. 192 | """ 193 | return self.menu_order or 999 194 | 195 | def show_menu_item(self, request): 196 | """ 197 | Returns a boolean indicating whether the menu item should be visible 198 | for the user in the supplied request, based on their permissions. 199 | """ 200 | return self.permission_helper.has_list_permission(request.user) 201 | 202 | def get_list_display(self, request): 203 | """ 204 | Return a sequence containing the fields/method output to be displayed 205 | in the list view. 206 | """ 207 | return self.list_display 208 | 209 | def get_list_display_add_buttons(self, request): 210 | """ 211 | Return the name of the field/method from list_display where action 212 | buttons should be added. 213 | """ 214 | return self.list_display_add_buttons or self.list_display[0] 215 | 216 | def get_empty_value_display(self): 217 | """ 218 | Return the empty_value_display set on ModelAdmin. 219 | """ 220 | return mark_safe(self.empty_value_display) 221 | 222 | def get_list_filter(self, request): 223 | """ 224 | Returns a sequence containing the fields to be displayed as filters in 225 | the right sidebar in the list view. 226 | """ 227 | return self.list_filter 228 | 229 | def get_ordering(self, request): 230 | """ 231 | Returns a sequence defining the default ordering for results in the 232 | list view. 233 | """ 234 | return self.ordering or () 235 | 236 | def get_queryset(self, request): 237 | """ 238 | Returns a QuerySet of all model instances that can be edited by the 239 | admin site. 240 | """ 241 | qs = self.model._default_manager.get_queryset() 242 | ordering = self.get_ordering(request) 243 | if ordering: 244 | qs = qs.order_by(*ordering) 245 | return qs 246 | 247 | def get_search_fields(self, request): 248 | """ 249 | Returns a sequence defining which fields on a model should be searched 250 | when a search is initiated from the list view. 251 | """ 252 | return self.search_fields or () 253 | 254 | def get_index_url(self): 255 | return reverse(get_url_name(self.opts)) 256 | 257 | def get_choose_parent_url(self): 258 | return reverse(get_url_name(self.opts, 'choose_parent')) 259 | 260 | def get_create_url(self): 261 | return reverse(get_url_name(self.opts, 'create')) 262 | 263 | def get_inspect_view_fields(self): 264 | if not self.inspect_view_fields: 265 | found_fields = [] 266 | for f in self.model._meta.get_fields(): 267 | if f.name not in self.inspect_view_fields_exclude: 268 | if f.concrete and ( 269 | not f.is_relation or 270 | (not f.auto_created and f.related_model) 271 | ): 272 | found_fields.append(f.name) 273 | return found_fields 274 | return self.inspect_view_fields 275 | 276 | def get_extra_class_names_for_field_col(self, obj, field_name): 277 | """ 278 | Return a list of additional CSS class names to be added to the table 279 | cell's `class` attribute when rendering the output of `field_name` for 280 | `obj` in `index_view`. 281 | 282 | Must always return a list or tuple. 283 | """ 284 | return [] 285 | 286 | def get_extra_attrs_for_field_col(self, obj, field_name): 287 | """ 288 | Return a dictionary of additional HTML attributes to be added to a 289 | table cell when rendering the output of `field_name` for `obj` in 290 | `index_view`. 291 | 292 | Must always return a dictionary. 293 | """ 294 | return {} 295 | 296 | def get_index_view_extra_css(self): 297 | css = ['wagtailmodeladmin/css/index.css'] 298 | css.extend(self.index_view_extra_css) 299 | return css 300 | 301 | def get_index_view_extra_js(self): 302 | return self.index_view_extra_js 303 | 304 | def get_form_view_extra_css(self): 305 | return self.form_view_extra_css 306 | 307 | def get_form_view_extra_js(self): 308 | return self.form_view_extra_js 309 | 310 | def get_inspect_view_extra_css(self): 311 | return self.inspect_view_extra_css 312 | 313 | def get_inspect_view_extra_js(self): 314 | return self.inspect_view_extra_js 315 | 316 | def index_view(self, request): 317 | """ 318 | Instantiates a class-based view to provide listing functionality for 319 | the assigned model. The view class used can be overridden by changing 320 | the 'index_view_class' attribute. 321 | """ 322 | kwargs = {'model_admin': self} 323 | view_class = self.index_view_class 324 | return view_class.as_view(**kwargs)(request) 325 | 326 | def create_view(self, request): 327 | """ 328 | Instantiates a class-based view to provide 'creation' functionality for 329 | the assigned model, or redirect to Wagtail's create view if the 330 | assigned model extends 'Page'. The view class used can be overridden by 331 | changing the 'create_view_class' attribute. 332 | """ 333 | kwargs = {'model_admin': self} 334 | view_class = self.create_view_class 335 | return view_class.as_view(**kwargs)(request) 336 | 337 | def inspect_view(self, request, object_id): 338 | kwargs = {'model_admin': self, 'object_id': object_id} 339 | view_class = self.inspect_view_class 340 | return view_class.as_view(**kwargs)(request) 341 | 342 | def choose_parent_view(self, request): 343 | """ 344 | Instantiates a class-based view to provide a view that allows a parent 345 | page to be chosen for a new object, where the assigned model extends 346 | Wagtail's Page model, and there is more than one potential parent for 347 | new instances. The view class used can be overridden by changing the 348 | 'choose_parent_view_class' attribute. 349 | """ 350 | kwargs = {'model_admin': self} 351 | view_class = self.choose_parent_view_class 352 | return view_class.as_view(**kwargs)(request) 353 | 354 | def edit_view(self, request, object_id): 355 | """ 356 | Instantiates a class-based view to provide 'edit' functionality for the 357 | assigned model, or redirect to Wagtail's edit view if the assigned 358 | model extends 'Page'. The view class used can be overridden by changing 359 | the 'edit_view_class' attribute. 360 | """ 361 | kwargs = {'model_admin': self, 'object_id': object_id} 362 | view_class = self.edit_view_class 363 | return view_class.as_view(**kwargs)(request) 364 | 365 | def confirm_delete_view(self, request, object_id): 366 | """ 367 | Instantiates a class-based view to provide 'delete confirmation' 368 | functionality for the assigned model, or redirect to Wagtail's delete 369 | confirmation view if the assigned model extends 'Page'. The view class 370 | used can be overridden by changing the 'confirm_delete_view_class' 371 | attribute. 372 | """ 373 | kwargs = {'model_admin': self, 'object_id': object_id} 374 | view_class = self.confirm_delete_view_class 375 | return view_class.as_view(**kwargs)(request) 376 | 377 | def unpublish_view(self, request, object_id): 378 | """ 379 | Instantiates a class-based view that redirects to Wagtail's 'unpublish' 380 | view for models that extend 'Page' (if the user has sufficient 381 | permissions). We do this via our own view so that we can reliably 382 | control redirection of the user back to the index_view once the action 383 | is completed. The view class used can be overridden by changing the 384 | 'unpublish_view_class' attribute. 385 | """ 386 | kwargs = {'model_admin': self, 'object_id': object_id} 387 | view_class = self.unpublish_view_class 388 | return view_class.as_view(**kwargs)(request) 389 | 390 | def copy_view(self, request, object_id): 391 | """ 392 | Instantiates a class-based view that redirects to Wagtail's 'copy' 393 | view for models that extend 'Page' (if the user has sufficient 394 | permissions). We do this via our own view so that we can reliably 395 | control redirection of the user back to the index_view once the action 396 | is completed. The view class used can be overridden by changing the 397 | 'copy_view_class' attribute. 398 | """ 399 | kwargs = {'model_admin': self, 'object_id': object_id} 400 | view_class = self.copy_view_class 401 | return view_class.as_view(**kwargs)(request) 402 | 403 | def get_templates(self, action='index'): 404 | """ 405 | Utility function that provides a list of templates to try for a given 406 | view, when the template isn't overridden by one of the template 407 | attributes on the class. 408 | """ 409 | app = self.opts.app_label 410 | model_name = self.opts.model_name 411 | return [ 412 | 'wagtailmodeladmin/%s/%s/%s.html' % (app, model_name, action), 413 | 'wagtailmodeladmin/%s/%s.html' % (app, action), 414 | 'wagtailmodeladmin/%s.html' % (action,), 415 | ] 416 | 417 | def get_index_template(self): 418 | """ 419 | Returns a template to be used when rendering 'index_view'. If a 420 | template is specified by the 'index_template_name' attribute, that will 421 | be used. Otherwise, a list of preferred template names are returned, 422 | allowing custom templates to be used by simply putting them in a 423 | sensible location in an app's template directory. 424 | """ 425 | return self.index_template_name or self.get_templates('index') 426 | 427 | def get_inspect_template(self): 428 | """ 429 | Returns a template to be used when rendering 'inspect_view'. If a 430 | template is specified by the 'inspect_template_name' attribute, that 431 | will be used. Otherwise, a list of preferred template names are 432 | returned. 433 | """ 434 | return self.inspect_template_name or self.get_templates('inspect') 435 | 436 | def get_choose_parent_template(self): 437 | """ 438 | Returns a template to be used when rendering 'choose_parent_view'. If a 439 | template is specified by the 'choose_parent_template_name' attribute, 440 | that will be used. Otherwise, a list of preferred template names are 441 | returned. 442 | """ 443 | return self.choose_parent_template_name or self.get_templates( 444 | 'choose_parent') 445 | 446 | def get_create_template(self): 447 | """ 448 | Returns a template to be used when rendering 'create_view'. If a 449 | template is specified by the 'create_template_name' attribute, 450 | that will be used. Otherwise, a list of preferred template names are 451 | returned. 452 | """ 453 | return self.create_template_name or self.get_templates('create') 454 | 455 | def get_edit_template(self): 456 | """ 457 | Returns a template to be used when rendering 'edit_view'. If a template 458 | is specified by the 'edit_template_name' attribute, that will be used. 459 | Otherwise, a list of preferred template names are returned. 460 | """ 461 | return self.edit_template_name or self.get_templates('edit') 462 | 463 | def get_confirm_delete_template(self): 464 | """ 465 | Returns a template to be used when rendering 'confirm_delete_view'. If 466 | a template is specified by the 'confirm_delete_template_name' 467 | attribute, that will be used. Otherwise, a list of preferred template 468 | names are returned. 469 | """ 470 | return self.confirm_delete_template_name or self.get_templates( 471 | 'confirm_delete') 472 | 473 | def get_menu_item(self, order=None): 474 | """ 475 | Utilised by Wagtail's 'register_menu_item' hook to create a menu item 476 | to access the listing view, or can be called by ModelAdminGroup 477 | to create a SubMenu 478 | """ 479 | return ModelAdminMenuItem(self, order or self.get_menu_order()) 480 | 481 | def get_permissions_for_registration(self): 482 | """ 483 | Utilised by Wagtail's 'register_permissions' hook to allow permissions 484 | for a model to be assigned to groups in settings. This is only required 485 | if the model isn't a Page model, and isn't registered as a Snippet 486 | """ 487 | from wagtail.wagtailsnippets.models import SNIPPET_MODELS 488 | if not self.is_pagemodel and self.model not in SNIPPET_MODELS: 489 | return self.permission_helper.get_all_model_permissions() 490 | return Permission.objects.none() 491 | 492 | def get_admin_urls_for_registration(self): 493 | """ 494 | Utilised by Wagtail's 'register_admin_urls' hook to register urls for 495 | our the views that class offers. 496 | """ 497 | urls = ( 498 | url(get_url_pattern(self.opts), 499 | self.index_view, name=get_url_name(self.opts)), 500 | url(get_url_pattern(self.opts, 'create'), 501 | self.create_view, name=get_url_name(self.opts, 'create')), 502 | url(get_object_specific_url_pattern(self.opts, 'edit'), 503 | self.edit_view, name=get_url_name(self.opts, 'edit')), 504 | url(get_object_specific_url_pattern(self.opts, 'confirm_delete'), 505 | self.confirm_delete_view, 506 | name=get_url_name(self.opts, 'confirm_delete')), 507 | ) 508 | if self.inspect_view_enabled: 509 | urls = urls + ( 510 | url(get_object_specific_url_pattern(self.opts, 'inspect'), 511 | self.inspect_view, 512 | name=get_url_name(self.opts, 'inspect')), 513 | ) 514 | if self.is_pagemodel: 515 | urls = urls + ( 516 | url(get_url_pattern(self.opts, 'choose_parent'), 517 | self.choose_parent_view, 518 | name=get_url_name(self.opts, 'choose_parent')), 519 | url(get_object_specific_url_pattern(self.opts, 'unpublish'), 520 | self.unpublish_view, 521 | name=get_url_name(self.opts, 'unpublish')), 522 | url(get_object_specific_url_pattern(self.opts, 'copy'), 523 | self.copy_view, 524 | name=get_url_name(self.opts, 'copy')), 525 | ) 526 | return urls 527 | 528 | def construct_main_menu(self, request, menu_items): 529 | warnings.warn(( 530 | "The 'construct_main_menu' method is now deprecated. You " 531 | "should also remove the construct_main_menu hook from " 532 | "wagtail_hooks.py in your app folder."), DeprecationWarning) 533 | return menu_items 534 | 535 | 536 | class ModelAdminGroup(WagtailRegisterable): 537 | """ 538 | Acts as a container for grouping together multiple PageModelAdmin and 539 | SnippetModelAdmin instances. Creates a menu item with a SubMenu for 540 | accessing the listing pages of those instances 541 | """ 542 | items = () 543 | menu_label = None 544 | menu_order = None 545 | menu_icon = None 546 | 547 | def __init__(self): 548 | """ 549 | When initialising, instantiate the classes within 'items', and assign 550 | the instances to a 'modeladmin_instances' attribute for convenient 551 | access later 552 | """ 553 | self.modeladmin_instances = [] 554 | for ModelAdminClass in self.items: 555 | self.modeladmin_instances.append(ModelAdminClass(parent=self)) 556 | 557 | def get_menu_label(self): 558 | return self.menu_label or self.get_app_label_from_subitems() 559 | 560 | def get_app_label_from_subitems(self): 561 | for instance in self.modeladmin_instances: 562 | return instance.opts.app_label.title() 563 | return '' 564 | 565 | def get_menu_icon(self): 566 | return self.menu_icon or 'icon-folder-open-inverse' 567 | 568 | def get_menu_order(self): 569 | return self.menu_order or 999 570 | 571 | def get_submenu_items(self): 572 | menu_items = [] 573 | item_order = 1 574 | for modeladmin in self.modeladmin_instances: 575 | menu_items.append(modeladmin.get_menu_item(order=item_order)) 576 | item_order += 1 577 | return menu_items 578 | 579 | def get_menu_item(self): 580 | """ 581 | Utilised by Wagtail's 'register_menu_item' hook to create a menu 582 | for this group with a SubMenu linking to listing pages for any 583 | associated ModelAdmin instances 584 | """ 585 | if self.modeladmin_instances: 586 | submenu = SubMenu(self.get_submenu_items()) 587 | return GroupMenuItem(self, self.get_menu_order(), submenu) 588 | 589 | def get_permissions_for_registration(self): 590 | """ 591 | Utilised by Wagtail's 'register_permissions' hook to allow permissions 592 | for a all models grouped by this class to be assigned to Groups in 593 | settings. 594 | """ 595 | qs = Permission.objects.none() 596 | for instance in self.modeladmin_instances: 597 | qs = qs | instance.get_permissions_for_registration() 598 | return qs 599 | 600 | def get_admin_urls_for_registration(self): 601 | """ 602 | Utilised by Wagtail's 'register_admin_urls' hook to register urls for 603 | used by any associated ModelAdmin instances 604 | """ 605 | urls = [] 606 | for instance in self.modeladmin_instances: 607 | urls.extend(instance.get_admin_urls_for_registration()) 608 | return urls 609 | 610 | def construct_main_menu(self, request, menu_items): 611 | warnings.warn(( 612 | "The 'construct_main_menu' method is now deprecated. You should " 613 | "also remove the construct_main_menu hook from wagtail_hooks.py " 614 | "in your app folder."), DeprecationWarning) 615 | return menu_items 616 | 617 | 618 | class PageModelAdmin(ModelAdmin): 619 | def __init__(self, parent=None): 620 | warnings.warn(( 621 | "The 'PageModelAdmin' class is now deprecated. You should extend " 622 | "the 'ModelAdmin' class instead (which supports all model types)." 623 | ), DeprecationWarning) 624 | super(PageModelAdmin, self).__init__(parent) 625 | 626 | 627 | class SnippetModelAdmin(ModelAdmin): 628 | def __init__(self, parent=None): 629 | warnings.warn(( 630 | "The 'SnippetModelAdmin' class is now deprecated. You should " 631 | "extend the 'ModelAdmin' class instead (which supports all model " 632 | "types)."), DeprecationWarning) 633 | super(SnippetModelAdmin, self).__init__(parent) 634 | 635 | 636 | class AppModelAdmin(ModelAdminGroup): 637 | pagemodeladmins = () 638 | snippetmodeladmins = () 639 | 640 | def __init__(self): 641 | warnings.warn(( 642 | "The 'AppModelAdmin' class is now deprecated, along with the " 643 | "pagemodeladmins and snippetmodeladmins attributes. You should " 644 | "use 'ModelAdminGroup' class instead, and combine the contents " 645 | "of pagemodeladmins and snippetmodeladmins into a single 'items' " 646 | "attribute."), DeprecationWarning) 647 | self.items = self.pagemodeladmins + self.snippetmodeladmins 648 | super(AppModelAdmin, self).__init__() 649 | 650 | 651 | def wagtailmodeladmin_register(wagtailmodeladmin_class): 652 | """ 653 | Alternative one-line method for registering ModelAdmin or ModelAdminGroup 654 | classes with Wagtail. 655 | """ 656 | instance = wagtailmodeladmin_class() 657 | instance.register_with_wagtail() 658 | -------------------------------------------------------------------------------- /wagtailmodeladmin/recipes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkhleics/wagtailmodeladmin/7fddc853bab2ff3868b8c7a03329308c55f16358/wagtailmodeladmin/recipes/__init__.py -------------------------------------------------------------------------------- /wagtailmodeladmin/recipes/readonly/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkhleics/wagtailmodeladmin/7fddc853bab2ff3868b8c7a03329308c55f16358/wagtailmodeladmin/recipes/readonly/__init__.py -------------------------------------------------------------------------------- /wagtailmodeladmin/recipes/readonly/helpers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Permission 2 | from wagtailmodeladmin.helpers import PermissionHelper 3 | 4 | 5 | class ReadOnlyPermissionHelper(PermissionHelper): 6 | def has_add_permission(self, user): 7 | return False 8 | 9 | def has_list_permission(self, user): 10 | try: 11 | list_perm_codename = 'list_%s' % self.opts.model_name 12 | perm = Permission.objects.get( 13 | content_type__app_label=self.opts.app_label, 14 | codename=list_perm_codename, 15 | ) 16 | return user.has_perm(perm) 17 | except Permission.DoesNotExist: 18 | pass 19 | return super(ReadOnlyPermissionHelper, self).has_list_permission(user) 20 | -------------------------------------------------------------------------------- /wagtailmodeladmin/recipes/readonly/options.py: -------------------------------------------------------------------------------- 1 | from wagtailmodeladmin.options import ModelAdmin 2 | from ...helpers import PagePermissionHelper 3 | from .helpers import ReadOnlyPermissionHelper 4 | 5 | 6 | class ReadOnlyModelAdmin(ModelAdmin): 7 | inspect_view_enabled = True 8 | 9 | def get_permission_helper_class(self): 10 | if self.is_pagemodel: 11 | return PagePermissionHelper 12 | return ReadOnlyPermissionHelper 13 | -------------------------------------------------------------------------------- /wagtailmodeladmin/static/wagtailmodeladmin/css/choose_parent_page.css: -------------------------------------------------------------------------------- 1 | form ul#id_parent_page li { 2 | margin: 15px 0; 3 | } 4 | form ul#id_parent_page li label { 5 | float: none; 6 | } -------------------------------------------------------------------------------- /wagtailmodeladmin/static/wagtailmodeladmin/css/index.css: -------------------------------------------------------------------------------- 1 | .content header { margin-bottom: 0; } 2 | #result_list { 3 | padding: 0 15px; 4 | } 5 | #result_list table { margin-bottom:0; } 6 | #result_list tbody th { 7 | background-color: transparent; 8 | text-align: left; 9 | padding: 1.2em 1em; 10 | } 11 | #result_list tbody tr:hover ul.actions { 12 | visibility: visible; 13 | } 14 | #result_list tbody td, #result_list tbody th { 15 | vertical-align: top; 16 | } 17 | 18 | #changelist-filter { 19 | padding: 0 15px; 20 | } 21 | 22 | #changelist-filter h2 { 23 | background-color: #fafafa; 24 | font-size: 13px; 25 | line-height: 31px; 26 | margin-top: 0; 27 | padding-left: 8px; 28 | border-bottom: 1px solid #E6E6E6; 29 | } 30 | 31 | #changelist-filter h3 { 32 | font-size: 12px; 33 | margin-bottom: 0; 34 | } 35 | 36 | #changelist-filter ul { 37 | padding-left: 0; 38 | margin-bottom: 25px; 39 | } 40 | 41 | #changelist-filter li { 42 | list-style-type: none; 43 | margin: 0 0 4px; 44 | padding-left: 0; 45 | } 46 | 47 | #changelist-filter a { 48 | font-family: Open Sans,Arial,sans-serif; 49 | -webkit-border-radius: 3px; 50 | border-radius: 3px; 51 | width: auto; 52 | line-height: 1.2em; 53 | padding: 8px 12px; 54 | font-size: 0.9em; 55 | font-weight: normal; 56 | vertical-align: middle; 57 | display: block; 58 | background-color: white; 59 | border: 1px solid #43b1b0; 60 | color: #43b1b0; 61 | text-decoration: none; 62 | text-transform: uppercase; 63 | position: relative; 64 | overflow: hidden; 65 | outline: none; 66 | box-sizing: border-box; 67 | -webkit-font-smoothing: auto; 68 | -moz-appearance: none; 69 | -moz-box-sizing: border-box; 70 | } 71 | 72 | #changelist-filter a:hover { 73 | background-color: #358c8b; 74 | border-color: #358c8b; 75 | color: white; 76 | } 77 | 78 | #changelist-filter li.selected a { 79 | color: white !important; 80 | border-color: #43b1b0 !important; 81 | background-color: #43b1b0; 82 | } 83 | 84 | .no-search-results { 85 | margin-top: 30px; 86 | } 87 | 88 | .no-search-results h2 { 89 | padding-top: 0.3em; 90 | margin-bottom: 0.3em; 91 | } 92 | .no-search-results img { 93 | float: left; 94 | margin: 0 15px 15px 0; 95 | width: 50px; 96 | } 97 | 98 | div.pagination { 99 | margin-top: 3em; 100 | border-top: 1px dashed #d9d9d9; 101 | padding: 2em 1em 0; 102 | } 103 | div.pagination ul { 104 | margin-top: -1.25em 105 | } 106 | p.no-results { 107 | margin: 30px 1em 0; 108 | } 109 | 110 | @media screen and (min-width: 50em) { 111 | #changelist-filter { 112 | float: right; 113 | padding: 0 1.5%; 114 | } 115 | #result_list { 116 | padding: 0 1.5% 0 0; 117 | } 118 | #result_list tbody th:first-child { 119 | padding-left: 50px; 120 | } 121 | #result_list.col12 tbody td:last-child { 122 | padding-right: 50px; 123 | } 124 | div.pagination { 125 | padding-left: 50px; 126 | padding-right: 50px; 127 | } 128 | div.pagination.col9 { 129 | width: 73.5%; 130 | } 131 | p.no-results { 132 | margin: 30px 50px 0; 133 | } 134 | } 135 | 136 | @media screen and (min-width: 1200px) { 137 | #result_list.col9 { 138 | width: 79%; 139 | } 140 | #changelist-filter { 141 | width: 21%; 142 | } 143 | div.pagination.col9 { 144 | width: 77.5%; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/choose_parent.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n admin_static %} 3 | 4 | {% block titletag %}{{ view.get_meta_title }}{% endblock %} 5 | 6 | {% block extra_css %} 7 | {% include "wagtailadmin/pages/_editor_css.html" %} 8 | 9 | {% endblock %} 10 | 11 | {% block extra_js %} 12 | {% include "wagtailadmin/pages/_editor_js.html" %} 13 | {% endblock %} 14 | 15 | 16 | {% block content %} 17 |
18 | 19 | {% block header %} 20 | {% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %} 21 | {% endblock %} 22 | 23 |
24 |

{% blocktrans %}Where should it go?{% endblocktrans %}

25 |

{% blocktrans with view.model_name_plural as plural %}{{ plural }} can be added to more than one place within your site. Where would you like this new one to go?{% endblocktrans %}

26 | 27 |
28 | {% csrf_token %} 29 | 30 |
    31 | {% include "wagtailadmin/shared/field_as_li.html" with field=form.parent_page %} 32 |
  • 33 | 34 |
  • 35 |
36 |
37 | 38 |
39 |
40 | {% endblock %} 41 | 42 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n wagtailmodeladmin_tags %} 3 | 4 | {% block titletag %}{{ view.get_meta_title }}{% endblock %} 5 | 6 | {% block content %} 7 | 8 | {% block header %} 9 | {% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %} 10 | {% endblock %} 11 | 12 | {% block content_main %} 13 |
14 | {% if error_protected %} 15 |

{% blocktrans with view.model_name|lower as model_name %}{{ model_name }} could not be deleted{% endblocktrans %}

16 |

{% blocktrans with instance as instance_name and view.model_name as model_name %}'{{ instance_name }}' is currently referenced by other objects, and cannot be deleted without jeopardising data integrity. To delete it successfully, first remove references from the following objects, then try to delete it again:{% endblocktrans %}

17 |
    18 | {% for obj in linked_objects %}
  • {{ obj|get_content_type_for_obj|title }}: {{ obj }}
  • {% endfor %} 19 |
20 |

{% trans 'Go back to listing' %}

21 | {% else %} 22 |

{{ view.confirmation_message }}

23 |
24 | {% csrf_token %} 25 | 26 |
27 | {% endif %} 28 |
29 | {% endblock %} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/create.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block titletag %}{{ view.get_meta_title }}{% endblock %} 5 | 6 | {% block extra_css %} 7 | {% include "wagtailadmin/pages/_editor_css.html" %} 8 | {{ view.media.css }} 9 | {% endblock %} 10 | 11 | {% block extra_js %} 12 | {% include "wagtailadmin/pages/_editor_js.html" %} 13 | {{ view.media.js }} 14 | {% endblock %} 15 | 16 | {% block content %} 17 | 18 | {% block header %} 19 | {% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 %} 20 | {% endblock %} 21 | 22 |
23 | {% csrf_token %} 24 | 25 | {% block form %}{{ edit_handler.render_form_content }}{% endblock %} 26 | 27 | {% block footer %} 28 |
29 |
    30 |
  • 31 | {% block form_actions %} 32 | 35 | {% endblock %} 36 |
  • 37 |
38 |
39 | {% endblock %} 40 |
41 | {% endblock %} 42 | 43 | 44 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailmodeladmin/create.html" %} 2 | {% load i18n %} 3 | 4 | {% block form_action %}{{ view.get_edit_url }}{% endblock %} 5 | 6 | {% block form_actions %} 7 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 6 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/button.html: -------------------------------------------------------------------------------- 1 | {{ button.label }} 2 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/filter.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %} 3 | 9 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/result_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailmodeladmin_tags %} 2 | {% if results %} 3 | 4 | 5 | 6 | {% for header in result_headers %} 7 | 12 | {% endfor %} 13 | 14 | 15 | 16 | {% for result in results %} 17 | 18 | {% result_row_display forloop.counter0 %} 19 | 20 | {% endfor %} 21 | 22 |
8 | {% if header.sortable %}{% endif %} 9 | {{ header.text|capfirst }} 10 | {% if header.sortable %}{% endif %} 11 |
23 | {% else %} 24 |
25 |

{% blocktrans with view.model_name_plural|lower as name %}Sorry, there are no {{ name }} matching your search parameters.{% endblocktrans %}

26 |
27 | {% endif %} 28 | 29 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/result_row.html: -------------------------------------------------------------------------------- 1 | {% load wagtailmodeladmin_tags %} 2 | {% for item in result %} 3 | {% result_row_value_display forloop.counter0 %} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/result_row_value.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailmodeladmin_tags %} 2 | {{ item }}{% if add_action_buttons %} 3 | {% if action_buttons %} 4 |
    5 | {% for button in action_buttons %} 6 |
  • {% include 'wagtailmodeladmin/includes/button.html' %}
  • 7 | {% endfor %} 8 |
9 | {% endif %} 10 | {{ item_closing_tag }} 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/includes/search_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if view.search_fields %} 3 | 19 | 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n wagtailmodeladmin_tags %} 3 | 4 | {% block titletag %}{{ view.get_meta_title }}{% endblock %} 5 | 6 | {% block extra_css %} 7 | {{ view.media.css }} 8 | {% endblock %} 9 | 10 | {% block extra_js %} 11 | {{ view.media.js }} 12 | {% endblock %} 13 | 14 | {% block content %} 15 | {% block header %} 16 |
17 |
18 |
19 |
20 | {% block h1 %}

{{ view.get_page_title }}

{% endblock %} 21 |
22 | {% block search %}{% search_form %}{% endblock %} 23 |
24 | {% block header_extra %} 25 | {% if has_add_permission and view.button_helper.add_button %} 26 |
27 |
28 | {% include 'wagtailmodeladmin/includes/button.html' with button=view.button_helper.add_button %} 29 |
30 |
31 | {% endif %} 32 | {% endblock %} 33 |
34 |
35 | {% endblock %} 36 | 37 | {% block content_main %} 38 |
39 |
40 | {% block content_cols %} 41 | 42 | {% block filters %} 43 | {% if view.has_filters and all_count %} 44 |
45 |

{% trans 'Filter' %}

46 | {% for spec in view.filter_specs %}{% admin_list_filter view spec %}{% endfor %} 47 |
48 | {% endif %} 49 | {% endblock %} 50 | 51 |
52 | {% block result_list %} 53 | {% if not all_count %} 54 |
55 | {% if no_valid_parents %} 56 |

{% blocktrans with view.model_name_plural|lower as name %}No {{ name }} have been created yet. One of the following must be added to your site before any {{ name }} can be added.{% endblocktrans %}

57 |
    58 | {% for type in required_parent_types %}
  • {{ type|title }}
  • {% endfor %} 59 |
60 | {% else %} 61 |

{% blocktrans with view.model_name_plural|lower as name %}No {{ name }} have been created yet.{% endblocktrans %} 62 | {% if has_add_permission %} 63 | {% blocktrans with view.get_create_url as url %} 64 | Why not add one? 65 | {% endblocktrans %} 66 | {% endif %}

67 | {% endif %} 68 |
69 | {% else %} 70 | {% result_list %} 71 | {% endif %} 72 | {% endblock %} 73 |
74 | 75 | {% block pagination %} 76 | 85 | {% endblock %} 86 | 87 | {% endblock %} 88 |
89 |
90 | {% endblock %} 91 | {% endblock %} 92 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templates/wagtailmodeladmin/inspect.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block titletag %}{{ view.get_meta_title }}{% endblock %} 5 | 6 | {% block extra_css %} 7 | {{ view.media.css }} 8 | {% endblock %} 9 | 10 | {% block extra_js %} 11 | {{ view.media.js }} 12 | {% endblock %} 13 | 14 | {% block content %} 15 |
16 | 17 | {% block header %} 18 | 22 | {% include "wagtailadmin/shared/header.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %} 23 | {% endblock %} 24 | 25 | {% block content_main %} 26 |
27 | 28 | {% block fields_output %} 29 | {% if fields %} 30 |
31 | {% for field in fields %} 32 |
{{ field.label }}
33 |
{{ field.value }}
34 | {% endfor %} 35 |
36 | {% endif %} 37 | {% endblock %} 38 | 39 |
40 | {% endblock %} 41 |
42 | 43 | {% block footer %} 44 | {% if buttons %} 45 |
46 |
47 | {% for button in buttons %} 48 | {% include "wagtailmodeladmin/includes/button.html" %} 49 | {% endfor %} 50 |
51 |
52 | {% endif %} 53 | {% endblock %} 54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /wagtailmodeladmin/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkhleics/wagtailmodeladmin/7fddc853bab2ff3868b8c7a03329308c55f16358/wagtailmodeladmin/templatetags/__init__.py -------------------------------------------------------------------------------- /wagtailmodeladmin/templatetags/wagtailmodeladmin_tags.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import datetime 3 | 4 | import django 5 | from django.db import models 6 | from django.template import Library 7 | from django.template.loader import get_template 8 | from django.utils.safestring import mark_safe 9 | from django.utils.encoding import force_text 10 | from django.utils.html import format_html 11 | from django.utils.translation import ugettext as _ 12 | from django.core.exceptions import ObjectDoesNotExist 13 | 14 | from django.contrib.admin.templatetags.admin_list import ( 15 | ResultList, result_headers, 16 | ) 17 | from django.contrib.admin.utils import ( 18 | display_for_field, display_for_value, lookup_field, 19 | ) 20 | 21 | from ..views import PAGE_VAR, SEARCH_VAR 22 | 23 | register = Library() 24 | 25 | 26 | def items_for_result(view, result): 27 | """ 28 | Generates the actual list of data. 29 | """ 30 | model_admin = view.model_admin 31 | for field_name in view.list_display: 32 | empty_value_display = model_admin.get_empty_value_display() 33 | row_classes = ['field-%s' % field_name] 34 | try: 35 | f, attr, value = lookup_field(field_name, result, model_admin) 36 | except ObjectDoesNotExist: 37 | result_repr = empty_value_display 38 | else: 39 | empty_value_display = getattr(attr, 'empty_value_display', empty_value_display) 40 | if f is None or f.auto_created: 41 | allow_tags = getattr(attr, 'allow_tags', False) 42 | boolean = getattr(attr, 'boolean', False) 43 | if boolean or not value: 44 | allow_tags = True 45 | 46 | if django.VERSION >= (1, 9): 47 | result_repr = display_for_value(value, empty_value_display, boolean) 48 | else: 49 | result_repr = display_for_value(value, boolean) 50 | # Strip HTML tags in the resulting text, except if the 51 | # function has an "allow_tags" attribute set to True. 52 | if allow_tags: 53 | result_repr = mark_safe(result_repr) 54 | if isinstance(value, (datetime.date, datetime.time)): 55 | row_classes.append('nowrap') 56 | else: 57 | if isinstance(f, models.ManyToOneRel): 58 | field_val = getattr(result, f.name) 59 | if field_val is None: 60 | result_repr = empty_value_display 61 | else: 62 | result_repr = field_val 63 | else: 64 | if django.VERSION >= (1, 9): 65 | result_repr = display_for_field(value, f, empty_value_display) 66 | else: 67 | result_repr = display_for_field(value, f) 68 | 69 | if isinstance(f, (models.DateField, models.TimeField, models.ForeignKey)): 70 | row_classes.append('nowrap') 71 | if force_text(result_repr) == '': 72 | result_repr = mark_safe(' ') 73 | row_classes.extend(model_admin.get_extra_class_names_for_field_col(field_name, result)) 74 | row_attributes_dict = model_admin.get_extra_attrs_for_field_col(field_name, result) 75 | row_attributes_dict['class'] = ' ' . join(row_classes) 76 | row_attributes = ''.join(' %s="%s"' % (key, val) for key, val in row_attributes_dict.items()) 77 | row_attributes_safe = mark_safe(row_attributes) 78 | yield format_html('{}', row_attributes_safe, result_repr) 79 | 80 | 81 | def results(view, object_list): 82 | for item in object_list: 83 | yield ResultList(None, items_for_result(view, item)) 84 | 85 | 86 | @register.inclusion_tag("wagtailmodeladmin/includes/result_list.html", 87 | takes_context=True) 88 | def result_list(context): 89 | """ 90 | Displays the headers and data list together 91 | """ 92 | view = context['view'] 93 | object_list = context['object_list'] 94 | headers = list(result_headers(view)) 95 | num_sorted_fields = 0 96 | for h in headers: 97 | if h['sortable'] and h['sorted']: 98 | num_sorted_fields += 1 99 | context.update({ 100 | 'result_headers': headers, 101 | 'num_sorted_fields': num_sorted_fields, 102 | 'results': list(results(view, object_list))}) 103 | return context 104 | 105 | 106 | @register.simple_tag 107 | def pagination_link_previous(current_page, view): 108 | if current_page.has_previous(): 109 | previous_page_number0 = current_page.previous_page_number() - 1 110 | return format_html( 111 | '' % 112 | (view.get_query_string({PAGE_VAR: previous_page_number0}), _('Previous')) 113 | ) 114 | return '' 115 | 116 | 117 | @register.simple_tag 118 | def pagination_link_next(current_page, view): 119 | if current_page.has_next(): 120 | next_page_number0 = current_page.next_page_number() - 1 121 | return format_html( 122 | '' % 123 | (view.get_query_string({PAGE_VAR: next_page_number0}), _('Next')) 124 | ) 125 | return '' 126 | 127 | 128 | @register.inclusion_tag("wagtailmodeladmin/includes/search_form.html", 129 | takes_context=True) 130 | def search_form(context): 131 | context.update({'search_var': SEARCH_VAR}) 132 | return context 133 | 134 | 135 | @register.simple_tag 136 | def admin_list_filter(view, spec): 137 | template_name = spec.template 138 | if template_name == 'admin/filter.html': 139 | template_name = 'wagtailmodeladmin/includes/filter.html' 140 | tpl = get_template(template_name) 141 | return tpl.render({ 142 | 'title': spec.title, 143 | 'choices': list(spec.choices(view)), 144 | 'spec': spec, 145 | }) 146 | 147 | 148 | @register.inclusion_tag("wagtailmodeladmin/includes/result_row.html", 149 | takes_context=True) 150 | def result_row_display(context, index=0): 151 | obj = context['object_list'][index] 152 | view = context['view'] 153 | context.update({ 154 | 'obj': obj, 155 | 'action_buttons': view.get_buttons_for_obj(obj), 156 | }) 157 | return context 158 | 159 | 160 | @register.inclusion_tag("wagtailmodeladmin/includes/result_row_value.html", 161 | takes_context=True) 162 | def result_row_value_display(context, index=0): 163 | add_action_buttons = False 164 | item = context['item'] 165 | closing_tag = mark_safe(item[-5:]) 166 | request = context['request'] 167 | modeladmin = context['view'].model_admin 168 | field_name = modeladmin.get_list_display(request)[index] 169 | if field_name == modeladmin.get_list_display_add_buttons(request): 170 | add_action_buttons = True 171 | item = mark_safe(item[0:-5]) 172 | context.update({ 173 | 'item': item, 174 | 'add_action_buttons': add_action_buttons, 175 | 'closing_tag': closing_tag, 176 | }) 177 | return context 178 | 179 | 180 | @register.filter 181 | def get_content_type_for_obj(obj): 182 | """ 183 | Return the model name/"content type" as a string 184 | e.g BlogPage, NewsListingPage. 185 | Can be used with "slugify" to create CSS-friendly classnames 186 | Usage: {{ self|content_type|slugify }} 187 | """ 188 | return obj.__class__._meta.verbose_name 189 | -------------------------------------------------------------------------------- /wagtailmodeladmin/views.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import operator 3 | from collections import OrderedDict 4 | from functools import reduce 5 | 6 | from django.db import models 7 | from django import forms 8 | from django.db.models.fields.related import ForeignObjectRel 9 | from django.db.models.constants import LOOKUP_SEP 10 | from django.db.models.sql.constants import QUERY_TERMS 11 | from django.shortcuts import get_object_or_404, redirect, render 12 | from django.core.urlresolvers import reverse 13 | from django.template.defaultfilters import filesizeformat 14 | 15 | from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation 16 | from django.db.models.fields import FieldDoesNotExist 17 | 18 | from django.core.paginator import Paginator, InvalidPage 19 | 20 | from django.contrib.admin import FieldListFilter, widgets 21 | from django.contrib.auth.decorators import login_required 22 | from django.utils.decorators import method_decorator 23 | 24 | from django.contrib.admin.options import IncorrectLookupParameters 25 | from django.contrib.admin.exceptions import DisallowedModelAdminLookup 26 | from django.contrib.admin.utils import ( 27 | get_fields_from_path, lookup_needs_distinct, prepare_lookup_value, quote) 28 | 29 | from django.utils import six 30 | from django.utils.translation import ugettext as _ 31 | from django.utils.encoding import force_text 32 | from django.utils.text import capfirst 33 | from django.utils.http import urlencode 34 | from django.utils.safestring import mark_safe 35 | from django.utils.functional import cached_property 36 | from django.views.generic import TemplateView 37 | from django.views.generic.edit import FormView 38 | 39 | from wagtail.wagtailadmin import messages 40 | from wagtail.wagtailadmin.edit_handlers import ( 41 | ObjectList, extract_panel_definitions_from_model_class) 42 | from wagtail.wagtailimages.models import get_image_model, Filter 43 | try: 44 | from wagtail.wagtaildocs.models import get_document_model 45 | Document = get_document_model 46 | except ImportError: 47 | from wagtail.wagtaildocs.models import Document 48 | from wagtail.wagtailcore import __version__ as wagtail_version 49 | 50 | from .helpers import get_url_name 51 | from .forms import ParentChooserForm 52 | 53 | # IndexView settings 54 | ORDER_VAR = 'o' 55 | ORDER_TYPE_VAR = 'ot' 56 | PAGE_VAR = 'p' 57 | SEARCH_VAR = 'q' 58 | ERROR_FLAG = 'e' 59 | IGNORED_PARAMS = (ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR) 60 | 61 | # Page URL name settings 62 | # > v1.1 63 | PAGES_CREATE_URL_NAME = 'wagtailadmin_pages:add' 64 | PAGES_EDIT_URL_NAME = 'wagtailadmin_pages:edit' 65 | PAGES_UNPUBLISH_URL_NAME = 'wagtailadmin_pages:unpublish' 66 | PAGES_DELETE_URL_NAME = 'wagtailadmin_pages:delete' 67 | PAGES_COPY_URL_NAME = 'wagtailadmin_pages:copy' 68 | if wagtail_version.startswith('1.0') or wagtail_version.startswith('0.'): 69 | # < v1.1 70 | PAGES_CREATE_URL_NAME = 'wagtailadmin_pages_create' 71 | PAGES_EDIT_URL_NAME = 'wagtailadmin_pages_edit' 72 | PAGES_UNPUBLISH_URL_NAME = 'wagtailadmin_pages_unpublish' 73 | PAGES_DELETE_URL_NAME = 'wagtailadmin_pages_delete' 74 | PAGES_COPY_URL_NAME = 'wagtailadmin_pages_copy' 75 | 76 | 77 | def permission_denied_response(request): 78 | messages.error( 79 | request, _('Sorry, you do not have permission to access this area.')) 80 | return redirect('wagtailadmin_home') 81 | 82 | 83 | class WMABaseView(TemplateView): 84 | """ 85 | Groups together common functionality for all app views. 86 | """ 87 | model_admin = None 88 | meta_title = '' 89 | page_title = '' 90 | page_subtitle = '' 91 | 92 | def __init__(self, model_admin): 93 | self.model_admin = model_admin 94 | self.model = model_admin.model 95 | self.opts = model_admin.model._meta 96 | self.pk_attname = self.opts.pk.attname 97 | self.is_pagemodel = model_admin.is_pagemodel 98 | self.permission_helper = model_admin.permission_helper 99 | 100 | @method_decorator(login_required) 101 | def dispatch(self, request, *args, **kwargs): 102 | button_helper_class = self.model_admin.get_button_helper_class() 103 | self.button_helper = button_helper_class( 104 | self.model, self.permission_helper, request.user, 105 | self.model_admin.inspect_view_enabled) 106 | return super(WMABaseView, self).dispatch(request, *args, **kwargs) 107 | 108 | @cached_property 109 | def app_label(self): 110 | return capfirst(force_text(self.opts.app_label)) 111 | 112 | @cached_property 113 | def model_name(self): 114 | return capfirst(force_text(self.opts.verbose_name)) 115 | 116 | @cached_property 117 | def model_name_plural(self): 118 | return capfirst(force_text(self.opts.verbose_name_plural)) 119 | 120 | @cached_property 121 | def get_index_url(self): 122 | return self.model_admin.get_index_url() 123 | 124 | @cached_property 125 | def get_create_url(self): 126 | return self.model_admin.get_create_url() 127 | 128 | @cached_property 129 | def menu_icon(self): 130 | return self.model_admin.get_menu_icon() 131 | 132 | @cached_property 133 | def header_icon(self): 134 | return self.menu_icon 135 | 136 | def get_edit_url(self, obj): 137 | return reverse(get_url_name(self.opts, 'edit'), args=(obj.pk,)) 138 | 139 | def get_delete_url(self, obj): 140 | return reverse(get_url_name(self.opts, 'delete'), args=(obj.pk,)) 141 | 142 | def prime_session_for_redirection(self): 143 | self.request.session['return_to_index_url'] = self.get_index_url 144 | 145 | def get_page_title(self): 146 | return self.page_title or self.model_name_plural 147 | 148 | def get_meta_title(self): 149 | return self.meta_title or self.get_page_title() 150 | 151 | def get_base_queryset(self, request): 152 | return self.model_admin.get_queryset(request) 153 | 154 | 155 | class WMAFormView(WMABaseView, FormView): 156 | 157 | @property 158 | def media(self): 159 | return forms.Media( 160 | css={'all': self.model_admin.get_form_view_extra_css()}, 161 | js=self.model_admin.get_form_view_extra_js() 162 | ) 163 | 164 | def get_instance(self): 165 | return getattr(self, 'instance', None) or self.model() 166 | 167 | def get_edit_handler(self): 168 | if hasattr(self.model, 'edit_handler'): 169 | edit_handler = self.model.edit_handler 170 | else: 171 | panels = extract_panel_definitions_from_model_class(self.model) 172 | edit_handler = ObjectList(panels) 173 | return edit_handler.bind_to_model(self.model) 174 | 175 | def get_form_class(self): 176 | return self.get_edit_handler().get_form_class(self.model) 177 | 178 | def get_form_kwargs(self): 179 | kwargs = FormView.get_form_kwargs(self) 180 | kwargs.update({'instance': self.get_instance()}) 181 | return kwargs 182 | 183 | def get_context_data(self, **kwargs): 184 | form = self.get_form() 185 | edit_handler_class = self.get_edit_handler() 186 | instance = self.get_instance() 187 | return { 188 | 'view': self, 189 | 'is_multipart': form.is_multipart(), 190 | 'is_tabbed': getattr(self.model, 'edit_handler', False), 191 | 'edit_handler': edit_handler_class(instance=instance, form=form), 192 | } 193 | 194 | def get_success_url(self): 195 | return self.get_index_url 196 | 197 | def get_success_message(self, instance): 198 | return _("{model_name} '{instance}' created.").format( 199 | model_name=self.model_name, instance=instance) 200 | 201 | def get_success_message_buttons(self, instance): 202 | return [ 203 | messages.button(self.get_edit_url(instance), _('Edit')) 204 | ] 205 | 206 | def get_error_message(self): 207 | model_name = self.model_name.lower() 208 | return _("The %s could not be created due to errors.") % model_name 209 | 210 | def form_valid(self, form): 211 | instance = form.save() 212 | messages.success( 213 | self.request, self.get_success_message(instance), 214 | buttons=self.get_success_message_buttons(instance) 215 | ) 216 | return redirect(self.get_success_url()) 217 | 218 | def form_invalid(self, form): 219 | messages.error(self.request, self.get_error_message()) 220 | return self.render_to_response(self.get_context_data()) 221 | 222 | 223 | class ObjectSpecificView(WMABaseView): 224 | 225 | object_id = None 226 | instance = None 227 | 228 | def __init__(self, model_admin, object_id): 229 | super(ObjectSpecificView, self).__init__(model_admin) 230 | self.object_id = object_id 231 | self.pk_safe = quote(object_id) 232 | filter_kwargs = {} 233 | filter_kwargs[self.pk_attname] = self.pk_safe 234 | object_qs = model_admin.model._default_manager.get_queryset().filter( 235 | **filter_kwargs) 236 | self.instance = get_object_or_404(object_qs) 237 | 238 | def check_action_permitted(self): 239 | return True 240 | 241 | def allow_object_delete(self): 242 | user = self.request.user 243 | return self.permission_helper.can_delete_object(user, self.instance) 244 | 245 | def get_edit_url(self, obj=None): 246 | return reverse(get_url_name(self.opts, 'edit'), args=(self.pk_safe,)) 247 | 248 | def get_delete_url(self, obj=None): 249 | return reverse(get_url_name(self.opts, 'confirm_delete'), 250 | args=(self.pk_safe,)) 251 | 252 | 253 | class IndexView(WMABaseView): 254 | 255 | flf_class = FieldListFilter 256 | 257 | @method_decorator(login_required) 258 | def dispatch(self, request, *args, **kwargs): 259 | self.list_display = self.model_admin.get_list_display(request) 260 | self.list_filter = self.model_admin.get_list_filter(request) 261 | self.search_fields = self.model_admin.get_search_fields(request) 262 | self.items_per_page = self.model_admin.list_per_page 263 | self.select_related = self.model_admin.list_select_related 264 | request = self.request 265 | 266 | # Get search parameters from the query string. 267 | try: 268 | self.page_num = int(request.GET.get(PAGE_VAR, 0)) 269 | except ValueError: 270 | self.page_num = 0 271 | 272 | self.params = dict(request.GET.items()) 273 | if PAGE_VAR in self.params: 274 | del self.params[PAGE_VAR] 275 | if ERROR_FLAG in self.params: 276 | del self.params[ERROR_FLAG] 277 | 278 | self.query = request.GET.get(SEARCH_VAR, '') 279 | self.queryset = self.get_queryset(request) 280 | 281 | if not self.permission_helper.has_list_permission(request.user): 282 | return permission_denied_response(request) 283 | 284 | return super(IndexView, self).dispatch(request, *args, **kwargs) 285 | 286 | @property 287 | def media(self): 288 | return forms.Media( 289 | css={'all': self.model_admin.get_index_view_extra_css()}, 290 | js=self.model_admin.get_index_view_extra_js() 291 | ) 292 | 293 | def get_buttons_for_obj(self, obj): 294 | return self.button_helper.get_buttons_for_obj( 295 | obj, classnames_add=['button-small', 'button-secondary']) 296 | 297 | def get_search_results(self, request, queryset, search_term): 298 | """ 299 | Returns a tuple containing a queryset to implement the search, 300 | and a boolean indicating if the results may contain duplicates. 301 | """ 302 | # Apply keyword searches. 303 | def construct_search(field_name): 304 | if field_name.startswith('^'): 305 | return "%s__istartswith" % field_name[1:] 306 | elif field_name.startswith('='): 307 | return "%s__iexact" % field_name[1:] 308 | elif field_name.startswith('@'): 309 | return "%s__search" % field_name[1:] 310 | else: 311 | return "%s__icontains" % field_name 312 | 313 | use_distinct = False 314 | if self.search_fields and search_term: 315 | orm_lookups = [construct_search(str(search_field)) 316 | for search_field in self.search_fields] 317 | for bit in search_term.split(): 318 | or_queries = [models.Q(**{orm_lookup: bit}) 319 | for orm_lookup in orm_lookups] 320 | queryset = queryset.filter(reduce(operator.or_, or_queries)) 321 | if not use_distinct: 322 | for search_spec in orm_lookups: 323 | if lookup_needs_distinct(self.opts, search_spec): 324 | use_distinct = True 325 | break 326 | 327 | return queryset, use_distinct 328 | 329 | def lookup_allowed(self, lookup, value): 330 | # Check FKey lookups that are allowed, so that popups produced by 331 | # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to, 332 | # are allowed to work. 333 | for l in self.model._meta.related_fkey_lookups: 334 | for k, v in widgets.url_params_from_lookup_dict(l).items(): 335 | if k == lookup and v == value: 336 | return True 337 | 338 | parts = lookup.split(LOOKUP_SEP) 339 | 340 | # Last term in lookup is a query term (__exact, __startswith etc) 341 | # This term can be ignored. 342 | if len(parts) > 1 and parts[-1] in QUERY_TERMS: 343 | parts.pop() 344 | 345 | # Special case -- foo__id__exact and foo__id queries are implied 346 | # if foo has been specifically included in the lookup list; so 347 | # drop __id if it is the last part. However, first we need to find 348 | # the pk attribute name. 349 | rel_name = None 350 | for part in parts[:-1]: 351 | try: 352 | field, _, _, _ = self.model._meta.get_field_by_name(part) 353 | except FieldDoesNotExist: 354 | # Lookups on non-existent fields are ok, since they're ignored 355 | # later. 356 | return True 357 | if hasattr(field, 'rel'): 358 | if field.rel is None: 359 | # This property or relation doesn't exist, but it's allowed 360 | # since it's ignored in ChangeList.get_filters(). 361 | return True 362 | model = field.rel.to 363 | rel_name = field.rel.get_related_field().name 364 | elif isinstance(field, ForeignObjectRel): 365 | model = field.model 366 | rel_name = model._meta.pk.name 367 | else: 368 | rel_name = None 369 | if rel_name and len(parts) > 1 and parts[-1] == rel_name: 370 | parts.pop() 371 | 372 | if len(parts) == 1: 373 | return True 374 | clean_lookup = LOOKUP_SEP.join(parts) 375 | return clean_lookup in self.list_filter 376 | 377 | def get_filters_params(self, params=None): 378 | """ 379 | Returns all params except IGNORED_PARAMS 380 | """ 381 | if not params: 382 | params = self.params 383 | lookup_params = params.copy() # a dictionary of the query string 384 | # Remove all the parameters that are globally and systematically 385 | # ignored. 386 | for ignored in IGNORED_PARAMS: 387 | if ignored in lookup_params: 388 | del lookup_params[ignored] 389 | return lookup_params 390 | 391 | def get_filters(self, request): 392 | lookup_params = self.get_filters_params() 393 | use_distinct = False 394 | 395 | for key, value in lookup_params.items(): 396 | if not self.lookup_allowed(key, value): 397 | raise DisallowedModelAdminLookup( 398 | "Filtering by %s not allowed" % key) 399 | 400 | filter_specs = [] 401 | if self.list_filter: 402 | for list_filter in self.list_filter: 403 | if callable(list_filter): 404 | # This is simply a custom list filter class. 405 | spec = list_filter( 406 | request, 407 | lookup_params, 408 | self.model, 409 | self.model_admin) 410 | else: 411 | field_path = None 412 | if isinstance(list_filter, (tuple, list)): 413 | # This is a custom FieldListFilter class for a given 414 | # field. 415 | field, field_list_filter_class = list_filter 416 | else: 417 | # This is simply a field name, so use the default 418 | # FieldListFilter class that has been registered for 419 | # the type of the given field. 420 | field = list_filter 421 | field_list_filter_class = self.flf_class.create 422 | if not isinstance(field, models.Field): 423 | field_path = field 424 | field = get_fields_from_path(self.model, 425 | field_path)[-1] 426 | spec = field_list_filter_class( 427 | field, 428 | request, 429 | lookup_params, 430 | self.model, 431 | self.model_admin, 432 | field_path=field_path) 433 | 434 | # Check if we need to use distinct() 435 | use_distinct = ( 436 | use_distinct or lookup_needs_distinct(self.opts, 437 | field_path)) 438 | if spec and spec.has_output(): 439 | filter_specs.append(spec) 440 | 441 | # At this point, all the parameters used by the various ListFilters 442 | # have been removed from lookup_params, which now only contains other 443 | # parameters passed via the query string. We now loop through the 444 | # remaining parameters both to ensure that all the parameters are valid 445 | # fields and to determine if at least one of them needs distinct(). If 446 | # the lookup parameters aren't real fields, then bail out. 447 | try: 448 | for key, value in lookup_params.items(): 449 | lookup_params[key] = prepare_lookup_value(key, value) 450 | use_distinct = ( 451 | use_distinct or lookup_needs_distinct(self.opts, key)) 452 | return ( 453 | filter_specs, bool(filter_specs), lookup_params, use_distinct 454 | ) 455 | except FieldDoesNotExist as e: 456 | six.reraise( 457 | IncorrectLookupParameters, 458 | IncorrectLookupParameters(e), 459 | sys.exc_info()[2]) 460 | 461 | def get_query_string(self, new_params=None, remove=None): 462 | if new_params is None: 463 | new_params = {} 464 | if remove is None: 465 | remove = [] 466 | p = self.params.copy() 467 | for r in remove: 468 | for k in list(p): 469 | if k.startswith(r): 470 | del p[k] 471 | for k, v in new_params.items(): 472 | if v is None: 473 | if k in p: 474 | del p[k] 475 | else: 476 | p[k] = v 477 | return '?%s' % urlencode(sorted(p.items())) 478 | 479 | def _get_default_ordering(self): 480 | ordering = [] 481 | if self.model_admin.ordering: 482 | ordering = self.model_admin.ordering 483 | elif self.opts.ordering: 484 | ordering = self.opts.ordering 485 | return ordering 486 | 487 | def get_default_ordering(self, request): 488 | if self.model_admin.get_ordering(request): 489 | return self.model_admin.get_ordering(request) 490 | if self.opts.ordering: 491 | return self.opts.ordering 492 | return () 493 | 494 | def get_ordering_field(self, field_name): 495 | """ 496 | Returns the proper model field name corresponding to the given 497 | field_name to use for ordering. field_name may either be the name of a 498 | proper model field or the name of a method (on the admin or model) or a 499 | callable with the 'admin_order_field' attribute. Returns None if no 500 | proper model field name can be matched. 501 | """ 502 | try: 503 | field = self.opts.get_field(field_name) 504 | return field.name 505 | except FieldDoesNotExist: 506 | # See whether field_name is a name of a non-field 507 | # that allows sorting. 508 | if callable(field_name): 509 | attr = field_name 510 | elif hasattr(self.model_admin, field_name): 511 | attr = getattr(self.model_admin, field_name) 512 | else: 513 | attr = getattr(self.model, field_name) 514 | return getattr(attr, 'admin_order_field', None) 515 | 516 | def get_ordering(self, request, queryset): 517 | """ 518 | Returns the list of ordering fields for the change list. 519 | First we check the get_ordering() method in model admin, then we check 520 | the object's default ordering. Then, any manually-specified ordering 521 | from the query string overrides anything. Finally, a deterministic 522 | order is guaranteed by ensuring the primary key is used as the last 523 | ordering field. 524 | """ 525 | params = self.params 526 | ordering = list(self.get_default_ordering(request)) 527 | if ORDER_VAR in params: 528 | # Clear ordering and used params 529 | ordering = [] 530 | order_params = params[ORDER_VAR].split('.') 531 | for p in order_params: 532 | try: 533 | none, pfx, idx = p.rpartition('-') 534 | field_name = self.list_display[int(idx)] 535 | order_field = self.get_ordering_field(field_name) 536 | if not order_field: 537 | continue # No 'admin_order_field', skip it 538 | # reverse order if order_field has already "-" as prefix 539 | if order_field.startswith('-') and pfx == "-": 540 | ordering.append(order_field[1:]) 541 | else: 542 | ordering.append(pfx + order_field) 543 | except (IndexError, ValueError): 544 | continue # Invalid ordering specified, skip it. 545 | 546 | # Add the given query's ordering fields, if any. 547 | ordering.extend(queryset.query.order_by) 548 | 549 | # Ensure that the primary key is systematically present in the list of 550 | # ordering fields so we can guarantee a deterministic order across all 551 | # database backends. 552 | pk_name = self.opts.pk.name 553 | if not (set(ordering) & {'pk', '-pk', pk_name, '-' + pk_name}): 554 | # The two sets do not intersect, meaning the pk isn't present. So 555 | # we add it. 556 | ordering.append('-pk') 557 | 558 | return ordering 559 | 560 | def get_ordering_field_columns(self): 561 | """ 562 | Returns an OrderedDict of ordering field column numbers and asc/desc 563 | """ 564 | 565 | # We must cope with more than one column having the same underlying 566 | # sort field, so we base things on column numbers. 567 | ordering = self._get_default_ordering() 568 | ordering_fields = OrderedDict() 569 | if ORDER_VAR not in self.params: 570 | # for ordering specified on model_admin or model Meta, we don't 571 | # know the right column numbers absolutely, because there might be 572 | # morr than one column associated with that ordering, so we guess. 573 | for field in ordering: 574 | if field.startswith('-'): 575 | field = field[1:] 576 | order_type = 'desc' 577 | else: 578 | order_type = 'asc' 579 | for index, attr in enumerate(self.list_display): 580 | if self.get_ordering_field(attr) == field: 581 | ordering_fields[index] = order_type 582 | break 583 | else: 584 | for p in self.params[ORDER_VAR].split('.'): 585 | none, pfx, idx = p.rpartition('-') 586 | try: 587 | idx = int(idx) 588 | except ValueError: 589 | continue # skip it 590 | ordering_fields[idx] = 'desc' if pfx == '-' else 'asc' 591 | return ordering_fields 592 | 593 | def get_queryset(self, request): 594 | # First, we collect all the declared list filters. 595 | (self.filter_specs, self.has_filters, remaining_lookup_params, 596 | filters_use_distinct) = self.get_filters(request) 597 | 598 | # Then, we let every list filter modify the queryset to its liking. 599 | qs = self.get_base_queryset(request) 600 | for filter_spec in self.filter_specs: 601 | new_qs = filter_spec.queryset(request, qs) 602 | if new_qs is not None: 603 | qs = new_qs 604 | 605 | try: 606 | # Finally, we apply the remaining lookup parameters from the query 607 | # string (i.e. those that haven't already been processed by the 608 | # filters). 609 | qs = qs.filter(**remaining_lookup_params) 610 | except (SuspiciousOperation, ImproperlyConfigured): 611 | # Allow certain types of errors to be re-raised as-is so that the 612 | # caller can treat them in a special way. 613 | raise 614 | except Exception as e: 615 | # Every other error is caught with a naked except, because we don't 616 | # have any other way of validating lookup parameters. They might be 617 | # invalid if the keyword arguments are incorrect, or if the values 618 | # are not in the correct type, so we might get FieldError, 619 | # ValueError, ValidationError, or ?. 620 | raise IncorrectLookupParameters(e) 621 | 622 | if not qs.query.select_related: 623 | qs = self.apply_select_related(qs) 624 | 625 | # Set ordering. 626 | ordering = self.get_ordering(request, qs) 627 | qs = qs.order_by(*ordering) 628 | 629 | # Apply search results 630 | qs, search_use_distinct = self.get_search_results( 631 | request, qs, self.query) 632 | 633 | # Remove duplicates from results, if necessary 634 | if filters_use_distinct | search_use_distinct: 635 | return qs.distinct() 636 | else: 637 | return qs 638 | 639 | def apply_select_related(self, qs): 640 | if self.select_related is True: 641 | return qs.select_related() 642 | 643 | if self.select_related is False: 644 | if self.has_related_field_in_list_display(): 645 | return qs.select_related() 646 | 647 | if self.select_related: 648 | return qs.select_related(*self.select_related) 649 | return qs 650 | 651 | def has_related_field_in_list_display(self): 652 | for field_name in self.list_display: 653 | try: 654 | field = self.opts.get_field(field_name) 655 | except FieldDoesNotExist: 656 | pass 657 | else: 658 | if isinstance(field, models.ManyToOneRel): 659 | return True 660 | return False 661 | 662 | def get_context_data(self, request, *args, **kwargs): 663 | user = request.user 664 | all_count = self.get_base_queryset(request).count() 665 | queryset = self.get_queryset(request) 666 | result_count = queryset.count() 667 | has_add_permission = self.permission_helper.has_add_permission(user) 668 | paginator = Paginator(queryset, self.items_per_page) 669 | 670 | try: 671 | page_obj = paginator.page(self.page_num + 1) 672 | except InvalidPage: 673 | page_obj = paginator.page(1) 674 | 675 | context = { 676 | 'view': self, 677 | 'all_count': all_count, 678 | 'result_count': result_count, 679 | 'paginator': paginator, 680 | 'page_obj': page_obj, 681 | 'object_list': page_obj.object_list, 682 | 'has_add_permission': has_add_permission, 683 | } 684 | 685 | if self.is_pagemodel: 686 | allowed_parent_types = self.model.allowed_parent_page_types() 687 | user = request.user 688 | valid_parents = self.permission_helper.get_valid_parent_pages(user) 689 | valid_parent_count = valid_parents.count() 690 | context.update({ 691 | 'no_valid_parents': not valid_parent_count, 692 | 'required_parent_types': allowed_parent_types, 693 | }) 694 | return context 695 | 696 | def get(self, request, *args, **kwargs): 697 | context = self.get_context_data(request, *args, **kwargs) 698 | if request.session.get('return_to_index_url'): 699 | del(request.session['return_to_index_url']) 700 | return self.render_to_response(context) 701 | 702 | def get_template_names(self): 703 | return self.model_admin.get_index_template() 704 | 705 | 706 | class InspectView(ObjectSpecificView): 707 | 708 | page_title = _('Inspecting') 709 | 710 | def check_action_permitted(self): 711 | return self.permission_helper.has_list_permission(self.request.user) 712 | 713 | @method_decorator(login_required) 714 | def dispatch(self, request, *args, **kwargs): 715 | if not self.check_action_permitted(): 716 | return permission_denied_response(request) 717 | return super(InspectView, self).dispatch(request, *args, **kwargs) 718 | 719 | @property 720 | def media(self): 721 | return forms.Media( 722 | css={'all': self.model_admin.get_inspect_view_extra_css()}, 723 | js=self.model_admin.get_inspect_view_extra_js() 724 | ) 725 | 726 | def get_meta_title(self): 727 | return _('Inspecting %s') % self.model_name.lower() 728 | 729 | def get_page_subtitle(self): 730 | return self.instance 731 | 732 | def get_field_label(self, field_name, field=None): 733 | """ Return a label to display for a field """ 734 | label = None 735 | if field is not None: 736 | label = getattr(field, 'verbose_name', None) 737 | if label is None: 738 | label = getattr(field, 'name', None) 739 | if label is None: 740 | label = field_name 741 | return label.capitalize() 742 | 743 | def get_field_display_value(self, field_name, field=None): 744 | """ Return a display value for a field """ 745 | 746 | """ 747 | Firstly, check for a 'get_fieldname_display' property/method on 748 | the model, and return the value of that, if present. 749 | """ 750 | val_funct = getattr(self.instance, 'get_%s_display' % field_name, None) 751 | if val_funct is not None: 752 | if callable(val_funct): 753 | return val_funct() 754 | return val_funct 755 | 756 | """ 757 | Secondly, if we have a real field, we can try to display something 758 | more useful for it. 759 | """ 760 | if field is not None: 761 | try: 762 | field_type = field.get_internal_type() 763 | if ( 764 | field_type == 'ForeignKey' and 765 | field.related_model == get_image_model() 766 | ): 767 | # The field is an image 768 | return self.get_image_field_display(field_name, field) 769 | 770 | if ( 771 | field_type == 'ForeignKey' and 772 | field.related_model == Document 773 | ): 774 | # The field is a document 775 | return self.get_document_field_display(field_name, field) 776 | 777 | except AttributeError: 778 | pass 779 | 780 | """ 781 | Resort to getting the value of 'field_name' from the instance. 782 | """ 783 | return getattr(self.instance, field_name, 784 | self.model_admin.get_empty_value_display()) 785 | 786 | def get_image_field_display(self, field_name, field): 787 | """ Render an image """ 788 | image = getattr(self.instance, field_name) 789 | if image: 790 | fltr, _ = Filter.objects.get_or_create(spec='max-400x400') 791 | rendition = image.get_rendition(fltr) 792 | return rendition.img_tag 793 | return self.model_admin.get_empty_value_display() 794 | 795 | def get_document_field_display(self, field_name, field): 796 | """ Render a link to a document """ 797 | document = getattr(self.instance, field_name) 798 | if document: 799 | return mark_safe( 800 | '%s (%s, %s)' % ( 801 | document.url, 802 | document.title, 803 | document.file_extension.upper(), 804 | filesizeformat(document.file.size), 805 | ) 806 | ) 807 | return self.model_admin.get_empty_value_display() 808 | 809 | def get_dict_for_field(self, field_name): 810 | """ 811 | Return a dictionary containing `label` and `value` values to display 812 | for a field. 813 | """ 814 | try: 815 | field = self.model._meta.get_field(field_name) 816 | except FieldDoesNotExist: 817 | field = None 818 | return { 819 | 'label': self.get_field_label(field_name, field), 820 | 'value': self.get_field_display_value(field_name, field), 821 | } 822 | 823 | def get_fields_dict(self): 824 | """ 825 | Return a list of `label`/`value` dictionaries to represent the 826 | fiels named by the model_admin class's `get_inspect_view_fields` method 827 | """ 828 | fields = [] 829 | for field_name in self.model_admin.get_inspect_view_fields(): 830 | fields.append(self.get_dict_for_field(field_name)) 831 | return fields 832 | 833 | def get_context_data(self, **kwargs): 834 | buttons = self.button_helper.get_buttons_for_obj( 835 | self.instance, exclude=['inspect']) 836 | return { 837 | 'view': self, 838 | 'fields': self.get_fields_dict(), 839 | 'buttons': buttons, 840 | 'instance': self.instance, 841 | } 842 | 843 | def get_template_names(self): 844 | return self.model_admin.get_inspect_template() 845 | 846 | 847 | class CreateView(WMAFormView): 848 | page_title = _('New') 849 | 850 | def dispatch(self, request, *args, **kwargs): 851 | if not self.permission_helper.has_add_permission(request.user): 852 | return permission_denied_response(request) 853 | 854 | if self.is_pagemodel: 855 | self.prime_session_for_redirection() 856 | user = request.user 857 | parents = self.permission_helper.get_valid_parent_pages(user) 858 | parent_count = parents.count() 859 | 860 | # There's only one available parent for this page type for this 861 | # user, so we send them along with that as the chosen parent page 862 | if parent_count == 1: 863 | parent = parents.get() 864 | return redirect( 865 | PAGES_CREATE_URL_NAME, self.opts.app_label, 866 | self.opts.model_name, parent.pk) 867 | 868 | # The page can be added in multiple places, so redirect to the 869 | # choose_parent view so that the parent can be specified 870 | return redirect(self.model_admin.get_choose_parent_url()) 871 | return super(CreateView, self).dispatch(request, *args, **kwargs) 872 | 873 | def get_meta_title(self): 874 | return _('Create new %s') % self.model_name.lower() 875 | 876 | def get_page_subtitle(self): 877 | return self.model_name 878 | 879 | def get_template_names(self): 880 | return self.model_admin.get_create_template() 881 | 882 | 883 | class ChooseParentView(WMABaseView): 884 | def dispatch(self, request, *args, **kwargs): 885 | if not self.permission_helper.has_add_permission(request.user): 886 | return permission_denied_response(request) 887 | return super(ChooseParentView, self).dispatch(request, *args, **kwargs) 888 | 889 | def get_page_title(self): 890 | return _('Add %s') % self.model_name 891 | 892 | def get_form(self, request): 893 | parents = self.permission_helper.get_valid_parent_pages(request.user) 894 | return ParentChooserForm(parents, request.POST or None) 895 | 896 | def get(self, request, *args, **kwargs): 897 | form = self.get_form(request) 898 | context = {'view': self, 'form': form} 899 | return render(request, self.get_template(), context) 900 | 901 | def post(self, request, *args, **kargs): 902 | form = self.get_form(request) 903 | if form.is_valid(): 904 | parent = form.cleaned_data['parent_page'] 905 | return redirect(PAGES_CREATE_URL_NAME, self.opts.app_label, 906 | self.opts.model_name, quote(parent.pk)) 907 | context = {'view': self, 'form': form} 908 | return render(request, self.get_template(), context) 909 | 910 | def get_template(self): 911 | return self.model_admin.get_choose_parent_template() 912 | 913 | 914 | class EditView(ObjectSpecificView, CreateView): 915 | page_title = _('Editing') 916 | 917 | def check_action_permitted(self): 918 | user = self.request.user 919 | return self.permission_helper.can_edit_object(user, self.instance) 920 | 921 | @method_decorator(login_required) 922 | def dispatch(self, request, *args, **kwargs): 923 | if not self.check_action_permitted(): 924 | return permission_denied_response(request) 925 | if self.is_pagemodel: 926 | self.prime_session_for_redirection() 927 | return redirect(PAGES_EDIT_URL_NAME, self.object_id) 928 | return super(CreateView, self).dispatch(request, *args, **kwargs) 929 | 930 | def get_meta_title(self): 931 | return _('Editing %s') % self.model_name.lower() 932 | 933 | def get_page_subtitle(self): 934 | return self.instance 935 | 936 | def get_success_message(self, instance): 937 | return _("{model_name} '{instance}' updated.").format( 938 | model_name=self.model_name, instance=instance) 939 | 940 | def get_error_message(self): 941 | model_name = self.model_name.lower() 942 | return _("The %s could not be saved due to errors.") % model_name 943 | 944 | def get_template_names(self): 945 | return self.model_admin.get_edit_template() 946 | 947 | 948 | class ConfirmDeleteView(ObjectSpecificView): 949 | page_title = _('Delete') 950 | 951 | def check_action_permitted(self): 952 | user = self.request.user 953 | return self.permission_helper.can_delete_object(user, self.instance) 954 | 955 | @method_decorator(login_required) 956 | def dispatch(self, request, *args, **kwargs): 957 | if not self.check_action_permitted(): 958 | return permission_denied_response(request) 959 | if self.is_pagemodel: 960 | self.prime_session_for_redirection() 961 | return redirect(PAGES_DELETE_URL_NAME, self.object_id) 962 | return super(ConfirmDeleteView, self).dispatch(request, *args, 963 | **kwargs) 964 | 965 | def get_meta_title(self): 966 | return _('Confirm deletion of %s') % self.model_name.lower() 967 | 968 | def get_page_subtitle(self): 969 | return self.instance 970 | 971 | def confirmation_message(self): 972 | return _( 973 | "Are you sure you want to delete this %s? If other things in your " 974 | "site are related to it, they may also be affected." 975 | ) % self.model_name 976 | 977 | def delete_instance(self): 978 | self.instance.delete() 979 | 980 | def get(self, request, *args, **kwargs): 981 | context = {'view': self, 'instance': self.instance} 982 | return self.render_to_response(context) 983 | 984 | def post(self, request, *args, **kwargs): 985 | if request.POST: 986 | try: 987 | self.delete_instance() 988 | messages.success( 989 | request, 990 | _("{model} '{instance}' deleted.").format( 991 | model=self.model_name, instance=self.instance)) 992 | return redirect(self.get_index_url) 993 | except models.ProtectedError: 994 | messages.error( 995 | request, _( 996 | "{model} '{instance}' could not be deleted." 997 | ).format(model=self.model_name, instance=self.instance)) 998 | 999 | linked_objects = [] 1000 | for rel in self.model._meta.get_all_related_objects(): 1001 | if rel.on_delete == models.PROTECT: 1002 | qs = getattr(self.instance, rel.get_accessor_name()) 1003 | for obj in qs.all(): 1004 | linked_objects.append(obj) 1005 | 1006 | context = { 1007 | 'view': self, 1008 | 'instance': self.instance, 1009 | 'error_protected': True, 1010 | 'linked_objects': linked_objects, 1011 | } 1012 | return self.render_to_response(context) 1013 | 1014 | def get_template_names(self): 1015 | return self.model_admin.get_confirm_delete_template() 1016 | 1017 | 1018 | class UnpublishRedirectView(ObjectSpecificView): 1019 | def check_action_permitted(self): 1020 | user = self.request.user 1021 | return self.permission_helper.can_unpublish_object(user, self.instance) 1022 | 1023 | @method_decorator(login_required) 1024 | def dispatch(self, request, *args, **kwargs): 1025 | if not self.check_action_permitted(): 1026 | return permission_denied_response(request) 1027 | self.prime_session_for_redirection() 1028 | return redirect(PAGES_UNPUBLISH_URL_NAME, self.object_id) 1029 | 1030 | 1031 | class CopyRedirectView(ObjectSpecificView): 1032 | def check_action_permitted(self): 1033 | user = self.request.user 1034 | return self.permission_helper.can_copy_object(user, self.instance) 1035 | 1036 | @method_decorator(login_required) 1037 | def dispatch(self, request, *args, **kwargs): 1038 | if not self.check_action_permitted(): 1039 | return permission_denied_response(request) 1040 | self.prime_session_for_redirection() 1041 | return redirect(PAGES_COPY_URL_NAME, self.object_id) 1042 | --------------------------------------------------------------------------------