├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── admin_view_permission ├── __init__.py ├── admin.py ├── apps.py ├── enums.py ├── locale │ └── zh_Hans │ │ └── LC_MESSAGES │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── fix_proxy_permissions.py └── utils.py ├── docs ├── Makefile ├── command.rst ├── conf.py ├── configuration.rst ├── index.rst ├── installation.rst └── uninstall.rst ├── pytest.ini ├── requirements-debug.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── settings.py ├── test_app ├── __init__.py ├── admin.py ├── apps.py └── models.py ├── tests ├── __init__.py ├── functional │ ├── __init__.py │ ├── test_fix_proxy_permissions.py │ ├── test_others.py │ └── test_views.py ├── helpers.py └── unit │ ├── __init__.py │ ├── test_admin.py │ ├── test_apps.py │ └── test_utils.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Some files 2 | *~ 3 | *.pyc 4 | .python-version 5 | 6 | # Some dirs 7 | .idea 8 | dist 9 | build 10 | *.egg-info 11 | env 12 | venv 13 | .coverage 14 | .cache 15 | htmlcov 16 | docs/_build 17 | docs/_static 18 | docs/_templates 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | cache: pip 6 | 7 | # diferrent python versions 8 | python: 9 | - "2.7" 10 | - "3.4" 11 | - "3.6" 12 | 13 | # diferrent django versions 14 | env: 15 | - DJANGO_VERSION=1.8.* 16 | - DJANGO_VERSION=1.9.* 17 | - DJANGO_VERSION=1.10.* 18 | - DJANGO_VERSION=1.11.* 19 | - DJANGO_VERSION=2.0.* 20 | 21 | # install dependencies 22 | install: 23 | - pip install -r requirements-debug.txt 24 | - | 25 | if [[ $DJANGO_VERSION =~ ^2.*$ && $TRAVIS_PYTHON_VERSION == "2.7" ]]; then 26 | echo "Installing Django 2 in python 2 will fail. Keep the installed one" 27 | else 28 | pip install Django==$DJANGO_VERSION 29 | fi 30 | 31 | # print pip packages 32 | before_script: 33 | - pip freeze 34 | 35 | # run tests 36 | script: 37 | - flake8 . 38 | - isort -c -df 39 | - PYTHONPATH=`pwd` py.test --cov-report html --cov=admin_view_permission -v tests/ 40 | 41 | # report coverage to coveralls.io 42 | after_success: coveralls 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Context Information Security 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-exclude tests * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Admin View Permission 3 | ===================== 4 | 5 | .. image:: https://travis-ci.org/ctxis/django-admin-view-permission.svg?branch=master 6 | :target: https://travis-ci.org/ctxis/django-admin-view-permission 7 | :alt: Build Status 8 | .. image:: https://coveralls.io/repos/github/ctxis/django-admin-view-permission/badge.svg?branch=master 9 | :target: https://coveralls.io/github/ctxis/django-admin-view-permission?branch=master 10 | :alt: Coverage Status 11 | .. image:: https://codeclimate.com/github/ctxis/django-admin-view-permission/badges/gpa.svg 12 | :target: https://codeclimate.com/github/ctxis/django-admin-view-permission 13 | :alt: Code Climate 14 | 15 | Reusable application which provides a view permission for the existing models. 16 | 17 | Requirements 18 | ------------ 19 | 20 | * Django 21 | 22 | Support 23 | ------- 24 | 25 | The package is *deprecated* for *Django 2.1*. Django added the functionality 26 | into the `core `_ ( 27 | the 2 implementations are different). You should use this package only if you 28 | use Django < 2.1. 29 | 30 | * If you have installed this package by accident to your Django 2.1 31 | project, it won't affect the build-in view permission which comes 32 | with Django. 33 | * If you have upgraded you application to use Django > 2.1 just uninstall 34 | this package 35 | 36 | * Django: 1.8, 1.9, 1.10, 1.11, 2.0 37 | * Python: 2.7, 3.4, 3.5, 3.6 38 | 39 | Compatible with `django-parler `_'s translatable models. To verify which django-parler version our test suite runs against, check ``requirements-debug.txt``. You do not need django-parler to install django-admin-view-permission. 40 | 41 | Documentation 42 | ------------- 43 | For a full documentation you can visit: http://django-admin-view-permission.readthedocs.org/ 44 | 45 | Setup 46 | ----- 47 | 48 | * ``pip install django-admin-view-permission`` 49 | 50 | and then add ``admin_view_permission`` at the INSTALLED_APPS like this:: 51 | 52 | INSTALLED_APPS = [ 53 | 'admin_view_permission', 54 | 'django.contrib.admin', 55 | ... 56 | ] 57 | 58 | and finally run ``python manage.py migrate``. 59 | 60 | | You need to place the ``admin_view_permission`` before ``django.contrib.admin`` in INSTALLED_APPS. 61 | 62 | 63 | In case of a customized AdminSite in order to apply the view permission, you 64 | should inherit from the ``AdminViewPermissionAdminSite`` class:: 65 | 66 | from admin_view_permission.admin import AdminViewPermissionAdminSite 67 | 68 | class MyAdminSite(AdminViewPermissionAdminSite): 69 | ... 70 | 71 | 72 | Configuration 73 | ------------- 74 | 75 | This app provides a setting:: 76 | 77 | ADMIN_VIEW_PERMISSION_MODELS = [ 78 | 'auth.User', 79 | ... 80 | ] 81 | 82 | in which you can provide which models you want to be added the view permission. 83 | If you don't specify this setting then the view permission will be applied to 84 | all the models. 85 | 86 | Uninstall 87 | --------- 88 | 89 | 1. Remove the ``admin_view_permission`` from your ``INSTALLED_APPS`` setting 90 | 2. Delete the view permissions from the database:: 91 | 92 | from django.contrib.auth.models import Permission 93 | permissions = Permission.objects.filter(codename__startswith='view') 94 | permissions.delete() 95 | 96 | It will be helpful to check if the queryset contains only the view 97 | permissions and not anything else (for example: custom permission added) 98 | -------------------------------------------------------------------------------- /admin_view_permission/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'admin_view_permission.apps.AdminViewPermissionConfig' 2 | -------------------------------------------------------------------------------- /admin_view_permission/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from collections import OrderedDict 4 | 5 | from django.apps import apps 6 | from django.conf import settings 7 | from django.contrib import admin 8 | from django.contrib.admin.options import TO_FIELD_VAR 9 | from django.contrib.admin.templatetags.admin_modify import register 10 | from django.contrib.admin.templatetags.admin_modify import \ 11 | submit_row as original_submit_row 12 | from django.contrib.admin.utils import flatten, unquote 13 | from django.contrib.admin.views.main import ChangeList 14 | from django.contrib.auth import get_permission_codename 15 | from django.core.exceptions import PermissionDenied 16 | from django.utils.encoding import force_text 17 | from django.utils.module_loading import import_string 18 | from django.utils.text import capfirst 19 | from django.utils.translation import ugettext as _ 20 | 21 | from .utils import get_model_name 22 | 23 | try: 24 | from django.urls import NoReverseMatch, reverse 25 | except ImportError: 26 | # django < 2.0 27 | from django.core.urlresolvers import NoReverseMatch, reverse 28 | 29 | 30 | @register.inclusion_tag('admin/submit_line.html', takes_context=True) 31 | def submit_row(context): 32 | """submit buttons context change""" 33 | ctx = original_submit_row(context) 34 | ctx.update({ 35 | 'show_save_as_new': context.get( 36 | 'show_save_as_new', ctx['show_save_as_new']), 37 | 'show_save_and_add_another': context.get( 38 | 'show_save_and_add_another', ctx['show_save_and_add_another']), 39 | 'show_save_and_continue': context.get( 40 | 'show_save_and_continue', ctx['show_save_and_continue']), 41 | 'show_save': context.get( 42 | 'show_save', ctx['show_save']), 43 | }) 44 | return ctx 45 | 46 | 47 | class AdminViewPermissionChangeList(ChangeList): 48 | def __init__(self, request, *args, **kwargs): 49 | super(AdminViewPermissionChangeList, self).__init__( 50 | request, *args, **kwargs) 51 | self.request = request 52 | 53 | # If user has only view permission change the title of the changelist 54 | # view 55 | if self.model_admin.has_view_permission(self.request) and \ 56 | not self.model_admin._has_change_only_permission(self.request): 57 | if self.is_popup: 58 | title = _('Select %s') 59 | else: 60 | title = _('Select %s to view') 61 | self.title = title % force_text(self.opts.verbose_name) 62 | 63 | 64 | class AdminViewPermissionBaseModelAdmin(admin.options.BaseModelAdmin): 65 | def _has_change_only_permission(self, request, obj=None): 66 | return super(AdminViewPermissionBaseModelAdmin, 67 | self).has_change_permission(request, obj) 68 | 69 | def get_model_perms(self, request): 70 | """ 71 | Returns a dict of all perms for this model. This dict has the keys 72 | ``add``, ``change``, ``delete`` and ``view`` mapping to the True/False 73 | for each of those actions. 74 | """ 75 | return { 76 | 'add': self.has_add_permission(request), 77 | 'change': self.has_change_permission(request), 78 | 'delete': self.has_delete_permission(request), 79 | 'view': self.has_view_permission(request) 80 | } 81 | 82 | def has_view_permission(self, request, obj=None): 83 | """ 84 | Returns True if the given request has permission to view an object. 85 | Can be overridden by the user in subclasses. 86 | """ 87 | opts = self.opts 88 | codename = get_permission_codename('view', opts) 89 | return request.user.has_perm("%s.%s" % (opts.app_label, codename)) 90 | 91 | def has_change_permission(self, request, obj=None): 92 | """ 93 | Override this method in order to return True whenever a user has view 94 | permission and avoid re-implementing the change_view and 95 | changelist_view views. Also, added an extra argument to determine 96 | whenever this function will return the original response 97 | """ 98 | change_permission = super(AdminViewPermissionBaseModelAdmin, 99 | self).has_change_permission(request, obj) 100 | if change_permission or self.has_view_permission(request, obj): 101 | return True 102 | 103 | return change_permission 104 | 105 | def get_excluded_fields(self): 106 | """ 107 | Check if we have no excluded fields defined as we never want to 108 | show those (to any user) 109 | """ 110 | if self.exclude is None: 111 | exclude = [] 112 | else: 113 | exclude = list(self.exclude) 114 | 115 | # logic taken from: django.contrib.admin.options.ModelAdmin#get_form 116 | if self.exclude is None and hasattr( 117 | self.form, '_meta') and self.form._meta.exclude: 118 | # Take the custom ModelForm's Meta.exclude into account only 119 | # if the ModelAdmin doesn't define its own. 120 | exclude.extend(self.form._meta.exclude) 121 | 122 | return exclude 123 | 124 | def get_fields(self, request, obj=None): 125 | """ 126 | If the user has only the view permission return these readonly fields 127 | which are in fields attr 128 | """ 129 | if ((self.has_view_permission(request, obj) and ( 130 | obj and not self._has_change_only_permission(request, obj))) or ( 131 | obj is None and not self.has_add_permission(request))): 132 | fields = super( 133 | AdminViewPermissionBaseModelAdmin, 134 | self).get_fields(request, obj) 135 | excluded_fields = self.get_excluded_fields() 136 | readonly_fields = self.get_readonly_fields(request, obj) 137 | new_fields = [i for i in flatten(fields) if 138 | i in readonly_fields and 139 | i not in excluded_fields] 140 | 141 | return new_fields 142 | else: 143 | return super(AdminViewPermissionBaseModelAdmin, self).get_fields( 144 | request, obj) 145 | 146 | def get_readonly_fields(self, request, obj=None): 147 | """ 148 | Return all fields as readonly for the view permission 149 | """ 150 | # get read_only fields specified on the admin class is available 151 | # (needed for @property fields) 152 | readonly_fields = super(AdminViewPermissionBaseModelAdmin, 153 | self).get_readonly_fields(request, obj) 154 | 155 | if ((self.has_view_permission(request, obj) and ( 156 | obj and not self._has_change_only_permission(request, obj))) or ( 157 | obj is None and not self.has_add_permission(request))): 158 | if self.fields: 159 | # Set as readonly fields the specified fields 160 | readonly_fields = flatten(self.fields) 161 | else: 162 | readonly_fields = ( 163 | list(readonly_fields) + 164 | [field.name for field in self.opts.fields 165 | if field.editable] + 166 | [field.name for field in self.opts.many_to_many 167 | if field.editable] 168 | ) 169 | 170 | # remove duplicates whilst preserving order 171 | readonly_fields = list(OrderedDict.fromkeys(readonly_fields)) 172 | 173 | # Try to remove id if user has not specified fields and 174 | # readonly fields 175 | try: 176 | readonly_fields.remove('id') 177 | except ValueError: 178 | pass 179 | 180 | # Special case for User model 181 | if get_model_name(self.model) == settings.AUTH_USER_MODEL: 182 | try: 183 | readonly_fields.remove('password') 184 | except ValueError: 185 | pass 186 | 187 | # Remove from the readonly_fields list the excluded fields 188 | # specified on the form or the modeladmin 189 | excluded_fields = self.get_excluded_fields() 190 | if excluded_fields: 191 | readonly_fields = [ 192 | f for f in readonly_fields if f not in excluded_fields 193 | ] 194 | 195 | # django-parler compatibility: if this model is translatable, 196 | # ensure its fields are set to readonly too. 197 | if hasattr(self.model, '_parler_meta'): 198 | readonly_fields += list( 199 | self.model._parler_meta._fields_to_model.keys() 200 | ) 201 | 202 | return tuple(readonly_fields) 203 | 204 | def get_prepopulated_fields(self, request, obj=None): 205 | """ 206 | If user has view only permission do not return any prepopulated 207 | configuration, since it fails when creating a form 208 | """ 209 | is_add = obj is None and self.has_add_permission(request) 210 | is_change = obj is not None \ 211 | and self._has_change_only_permission(request, obj) 212 | if is_add or is_change: 213 | return super(AdminViewPermissionBaseModelAdmin, self)\ 214 | .get_prepopulated_fields(request, obj) 215 | return {} 216 | 217 | def get_actions(self, request): 218 | """ 219 | Override this funciton to remove the actions from the changelist view 220 | """ 221 | actions = super(AdminViewPermissionBaseModelAdmin, self).get_actions( 222 | request) 223 | 224 | can_delete = self.has_delete_permission(request) 225 | 226 | if not can_delete and 'delete_selected' in actions: 227 | del actions['delete_selected'] 228 | 229 | if self._has_change_only_permission(request): 230 | return actions 231 | elif can_delete: 232 | # If user has no change permission, but has delete 233 | # We assume that self.admin_site.actions contains "delete" action 234 | return OrderedDict( 235 | (name, (func, name, desc)) 236 | for func, name, desc in actions.values() 237 | if name in dict(self.admin_site.actions).keys() 238 | ) 239 | 240 | return OrderedDict() 241 | 242 | 243 | class AdminViewPermissionInlineModelAdmin(AdminViewPermissionBaseModelAdmin, 244 | admin.options.InlineModelAdmin): 245 | def get_queryset(self, request): 246 | """ 247 | Returns a QuerySet of all model instances that can be edited by the 248 | admin site. This is used by changelist_view. 249 | """ 250 | if self.has_view_permission(request) and \ 251 | not self._has_change_only_permission(request): 252 | return super(AdminViewPermissionInlineModelAdmin, self)\ 253 | .get_queryset(request) 254 | else: 255 | # TODO: Somehow super executes admin.options.InlineModelAdmin 256 | # get_queryset and AdminViewPermissionBaseModelAdmin which is 257 | # convinient 258 | return super(AdminViewPermissionInlineModelAdmin, self)\ 259 | .get_queryset(request) 260 | 261 | 262 | class AdminViewPermissionModelAdmin(AdminViewPermissionBaseModelAdmin, 263 | admin.ModelAdmin): 264 | def get_changelist(self, request, **kwargs): 265 | """ 266 | Returns the ChangeList class for use on the changelist page. 267 | """ 268 | return AdminViewPermissionChangeList 269 | 270 | def get_inline_instances(self, request, obj=None): 271 | inline_instances = [] 272 | for inline_class in self.inlines: 273 | new_class = type( 274 | str('DynamicAdminViewPermissionInlineModelAdmin'), 275 | (inline_class, AdminViewPermissionInlineModelAdmin), 276 | dict(inline_class.__dict__)) 277 | 278 | inline = new_class(self.model, self.admin_site) 279 | if request: 280 | if not (inline.has_view_permission(request, obj) or 281 | inline.has_add_permission(request) or 282 | inline._has_change_only_permission(request, obj) or 283 | inline.has_delete_permission(request, obj)): 284 | continue 285 | if inline.has_view_permission(request, obj) and \ 286 | not inline._has_change_only_permission(request, obj): 287 | inline.can_delete = False 288 | if not inline.has_add_permission(request): 289 | inline.max_num = 0 290 | inline_instances.append(inline) 291 | 292 | return inline_instances 293 | 294 | def change_view(self, request, object_id, form_url='', extra_context=None): 295 | """ 296 | Override this function to hide the sumbit row from the user who has 297 | view only permission 298 | """ 299 | to_field = request.POST.get( 300 | TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR) 301 | ) 302 | model = self.model 303 | opts = model._meta 304 | 305 | # TODO: Overriding the change_view costs 1 query more (one from us 306 | # and another from the super) 307 | obj = self.get_object(request, unquote(object_id), to_field) 308 | 309 | if self.has_view_permission(request, obj) and \ 310 | not self._has_change_only_permission(request, obj): 311 | extra_context = extra_context or {} 312 | extra_context['title'] = _('View %s') % force_text( 313 | opts.verbose_name) 314 | 315 | extra_context['show_save'] = False 316 | extra_context['show_save_and_continue'] = False 317 | extra_context['show_save_and_add_another'] = False 318 | extra_context['show_save_as_new'] = False 319 | 320 | inlines = self.get_inline_instances(request, obj) 321 | for inline in inlines: 322 | if (inline._has_change_only_permission(request, obj) or 323 | inline.has_add_permission(request)): 324 | extra_context['show_save'] = True 325 | extra_context['show_save_and_continue'] = True 326 | break 327 | 328 | return super(AdminViewPermissionModelAdmin, self).change_view( 329 | request, object_id, form_url, extra_context) 330 | 331 | def changelist_view(self, request, extra_context=None): 332 | resp = super(AdminViewPermissionModelAdmin, self).changelist_view( 333 | request, extra_context) 334 | if self.has_view_permission(request) and \ 335 | not self._has_change_only_permission(request): 336 | if hasattr(resp, 'context_data') and 'cl' in resp.context_data: 337 | resp.context_data['cl'].formset = None 338 | 339 | return resp 340 | 341 | 342 | class AdminViewPermissionUserAdmin(AdminViewPermissionModelAdmin): 343 | def user_change_password(self, request, id, form_url=''): 344 | if not self._has_change_only_permission(request): 345 | raise PermissionDenied 346 | 347 | return super(AdminViewPermissionUserAdmin, self).user_change_password( 348 | request, id, form_url) 349 | 350 | def get_form(self, request, obj=None, **kwargs): 351 | form = super(AdminViewPermissionUserAdmin, self).get_form( 352 | request, obj, **kwargs) 353 | 354 | UserCreationForm = import_string( 355 | 'django.contrib.auth.forms.UserCreationForm') 356 | if UserCreationForm in form.__bases__: 357 | return form 358 | 359 | if 'password' in form.base_fields: 360 | password_field = form.base_fields['password'] 361 | elif 'password2' in form.base_fields: 362 | password_field = form.base_fields['password2'] 363 | else: 364 | password_field = None 365 | 366 | if password_field: 367 | # TODO: I don't like this at all. Find another way to change the 368 | # TODO: help_text 369 | if not self._has_change_only_permission(request): 370 | password_field.help_text = _( 371 | "Raw passwords are not stored, so there is no way to " 372 | "see this user's password." 373 | ) 374 | else: 375 | password_field.help_text = _( 376 | "Raw passwords are not stored, so there is no way to see " 377 | "this user's password, but you can change the password " 378 | "using this form." 379 | ) 380 | 381 | return form 382 | 383 | 384 | class AdminViewPermissionAdminSite(admin.AdminSite): 385 | def _get_admin_class(self, admin_class, is_user_model): 386 | if admin_class: 387 | if is_user_model: 388 | mutable_admin_class_dict = admin_class.__dict__.copy() 389 | mutable_admin_class_dict.update({ 390 | 'user_change_password': 391 | AdminViewPermissionUserAdmin.user_change_password, 392 | 'get_form': AdminViewPermissionUserAdmin.get_form, 393 | }) 394 | # The following won't work if someone overrides the 395 | # user_change_password view 396 | admin_class = type( 397 | str('DynamicAdminViewPermissionModelAdmin'), 398 | (AdminViewPermissionUserAdmin, admin_class), 399 | dict(mutable_admin_class_dict), 400 | ) 401 | else: 402 | admin_class = type( 403 | str('DynamicAdminViewPermissionModelAdmin'), 404 | (admin_class, AdminViewPermissionModelAdmin), 405 | dict(admin_class.__dict__), 406 | ) 407 | else: 408 | admin_class = AdminViewPermissionModelAdmin 409 | 410 | return admin_class 411 | 412 | def register(self, model_or_iterable, admin_class=None, **options): 413 | """ 414 | Create a new ModelAdmin class which inherits from the original and 415 | the above and register all models with that 416 | """ 417 | SETTINGS_MODELS = getattr( 418 | settings, 'ADMIN_VIEW_PERMISSION_MODELS', None) 419 | 420 | models = model_or_iterable 421 | if not isinstance(model_or_iterable, (tuple, list)): 422 | models = tuple([model_or_iterable]) 423 | 424 | is_user_model = settings.AUTH_USER_MODEL in [ 425 | get_model_name(i) for i in models] 426 | 427 | if SETTINGS_MODELS or (SETTINGS_MODELS is not None and len( 428 | SETTINGS_MODELS) == 0): 429 | for model in models: 430 | model_name = get_model_name(model) 431 | if model_name in SETTINGS_MODELS: 432 | admin_class = self._get_admin_class( 433 | admin_class, is_user_model) 434 | 435 | super(AdminViewPermissionAdminSite, self).register( 436 | [model], admin_class, **options) 437 | else: 438 | admin_class = self._get_admin_class(admin_class, is_user_model) 439 | super(AdminViewPermissionAdminSite, self).register( 440 | model_or_iterable, admin_class, **options) 441 | 442 | def _build_app_dict(self, request, label=None): 443 | """ 444 | Builds the app dictionary. Takes an optional label parameters to filter 445 | models of a specific app. 446 | """ 447 | app_dict = {} 448 | 449 | if label: 450 | models = { 451 | m: m_a for m, m_a in self._registry.items() 452 | if m._meta.app_label == label 453 | } 454 | else: 455 | models = self._registry 456 | 457 | for model, model_admin in models.items(): 458 | app_label = model._meta.app_label 459 | 460 | has_module_perms = model_admin.has_module_permission(request) 461 | if not has_module_perms: 462 | if label: 463 | raise PermissionDenied 464 | continue 465 | 466 | perms = model_admin.get_model_perms(request) 467 | 468 | # Check whether user has any perm for this module. 469 | # If so, add the module to the model_list. 470 | if True not in perms.values(): 471 | continue 472 | 473 | info = (app_label, model._meta.model_name) 474 | model_dict = { 475 | 'name': capfirst(model._meta.verbose_name_plural), 476 | 'object_name': model._meta.object_name, 477 | 'perms': perms, 478 | } 479 | if perms.get('change') or perms.get('view'): 480 | try: 481 | model_dict['admin_url'] = reverse( 482 | 'admin:%s_%s_changelist' % info, current_app=self.name) 483 | except NoReverseMatch: 484 | pass 485 | if perms.get('add'): 486 | try: 487 | model_dict['add_url'] = reverse('admin:%s_%s_add' % info, 488 | current_app=self.name) 489 | except NoReverseMatch: 490 | pass 491 | 492 | if app_label in app_dict: 493 | app_dict[app_label]['models'].append(model_dict) 494 | else: 495 | app_dict[app_label] = { 496 | 'name': apps.get_app_config(app_label).verbose_name, 497 | 'app_label': app_label, 498 | 'app_url': reverse( 499 | 'admin:app_list', 500 | kwargs={'app_label': app_label}, 501 | current_app=self.name, 502 | ), 503 | 'has_module_perms': has_module_perms, 504 | 'models': [model_dict], 505 | } 506 | 507 | if label: 508 | return app_dict.get(label) 509 | 510 | return app_dict 511 | -------------------------------------------------------------------------------- /admin_view_permission/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import warnings 4 | 5 | from django.apps import AppConfig 6 | from django.apps import apps as global_apps 7 | from django.conf import settings 8 | from django.contrib import admin 9 | from django.db.models.signals import post_migrate 10 | 11 | from .admin import AdminViewPermissionAdminSite 12 | from .enums import DjangoVersion 13 | from .utils import django_version, get_model_name 14 | 15 | 16 | def update_permissions(sender, app_config, verbosity, apps=global_apps, 17 | **kwargs): 18 | settings_models = getattr(settings, 'ADMIN_VIEW_PERMISSION_MODELS', None) 19 | 20 | # TODO: Maybe look at the registry not in all models 21 | for app in apps.get_app_configs(): 22 | for model in app.get_models(): 23 | view_permission = 'view_%s' % model._meta.model_name 24 | if settings_models or (settings_models is not None and len( 25 | settings_models) == 0): 26 | model_name = get_model_name(model) 27 | if model_name in settings_models and view_permission not in \ 28 | [perm[0] for perm in model._meta.permissions]: 29 | model._meta.permissions += ( 30 | (view_permission, 31 | 'Can view %s' % model._meta.model_name),) 32 | else: 33 | if view_permission not in [perm[0] for perm in 34 | model._meta.permissions]: 35 | model._meta.permissions += ( 36 | ('view_%s' % model._meta.model_name, 37 | 'Can view %s' % model._meta.model_name),) 38 | 39 | 40 | class AdminViewPermissionConfig(AppConfig): 41 | name = 'admin_view_permission' 42 | 43 | def ready(self): 44 | if django_version() == DjangoVersion.DJANGO_21: 45 | # Disable silently the package for Django => 2.1. We don't override 46 | # admin_site neither the default ModelAdmin. 47 | warnings.warn( 48 | 'The package `admin_view_permission is deprecated in ' 49 | 'Django 2.1. Django added this functionality into the core.', 50 | DeprecationWarning 51 | ) 52 | return 53 | 54 | if not isinstance(admin.site, AdminViewPermissionAdminSite): 55 | admin.site = AdminViewPermissionAdminSite('admin') 56 | admin.sites.site = admin.site 57 | 58 | post_migrate.connect(update_permissions) 59 | -------------------------------------------------------------------------------- /admin_view_permission/enums.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | class DjangoVersion(object): 5 | (DJANGO_18, DJANGO_19, DJANGO_110, DJANGO_111, DJANGO_20, 6 | DJANGO_21) = range(0, 6) 7 | -------------------------------------------------------------------------------- /admin_view_permission/locale/zh_Hans/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 | # yangxiaoyong , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-09-13 18:20+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: admin.py:52 22 | #, python-format 23 | msgid "Select %s" 24 | msgstr "选择 %s" 25 | 26 | #: admin.py:54 27 | #, python-format 28 | msgid "Select %s to view" 29 | msgstr "选择需要查看的 %s" 30 | 31 | #: admin.py:276 32 | #, python-format 33 | msgid "View %s" 34 | msgstr "查看 %s" 35 | -------------------------------------------------------------------------------- /admin_view_permission/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/admin_view_permission/management/__init__.py -------------------------------------------------------------------------------- /admin_view_permission/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/admin_view_permission/management/commands/__init__.py -------------------------------------------------------------------------------- /admin_view_permission/management/commands/fix_proxy_permissions.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from django.apps import apps 4 | from django.contrib.auth.models import Permission 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.core.management.base import BaseCommand 7 | 8 | from admin_view_permission.apps import update_permissions 9 | from admin_view_permission.utils import get_all_permissions 10 | 11 | 12 | class Command(BaseCommand): 13 | """ 14 | Add permissions for proxy model. This is needed because of the 15 | bug https://code.djangoproject.com/ticket/11154. 16 | 17 | When a permission is created for a proxy model, it actually 18 | creates it for it's base model app_label (eg: for "article" 19 | instead of "about", for the About proxy model). 20 | """ 21 | help = "Fix permissions for proxy models." 22 | 23 | def handle(self, *args, **options): 24 | # We need to execute the post migration callback manually in order 25 | # to append the view permission on the proxy model. Then the following 26 | # script will create the appropriate content type and move the 27 | # permissions under this. If we don't call the callback the script 28 | # will create only the basic permissions (add, change, delete) 29 | update_permissions( 30 | apps.get_app_config('admin_view_permission'), 31 | apps.get_app_config('admin_view_permission'), 32 | verbosity=1, 33 | interactive=True, 34 | using='default', 35 | ) 36 | 37 | for model in apps.get_models(): 38 | opts = model._meta 39 | ctype, created = ContentType.objects.get_or_create( 40 | app_label=opts.app_label, 41 | model=opts.object_name.lower(), 42 | ) 43 | 44 | for codename, name in get_all_permissions(opts, ctype): 45 | perm, created = Permission.objects.get_or_create( 46 | codename=codename, 47 | content_type=ctype, 48 | defaults={'name': name}, 49 | ) 50 | if created: 51 | self.delete_parent_perms(perm) 52 | self.stdout.write('Adding permission {}\n'.format(perm)) 53 | 54 | def delete_parent_perms(self, perm): 55 | # Try to delete the permission attached to the parent model 56 | # if exists 57 | parent_perms = Permission.objects.filter( 58 | codename=perm.codename, 59 | ).exclude( 60 | content_type__app_label=perm.content_type.app_label, 61 | ) 62 | 63 | if parent_perms.exists(): 64 | copied_parent_perms = list(copy.deepcopy(parent_perms)) 65 | parent_perms.delete() 66 | for parent_perm in copied_parent_perms: 67 | self.stdout.write('Delete permission {}\n'.format(parent_perm)) 68 | -------------------------------------------------------------------------------- /admin_view_permission/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import django 4 | from django.contrib.auth.management import _get_all_permissions 5 | 6 | from .enums import DjangoVersion 7 | 8 | 9 | def django_version(): 10 | if django.get_version().startswith('1.8'): 11 | return DjangoVersion.DJANGO_18 12 | elif django.get_version().startswith('1.9'): 13 | return DjangoVersion.DJANGO_19 14 | elif django.get_version().startswith('1.10'): 15 | return DjangoVersion.DJANGO_110 16 | elif django.get_version().startswith('1.11'): 17 | return DjangoVersion.DJANGO_111 18 | elif django.get_version().startswith('2.0'): 19 | return DjangoVersion.DJANGO_20 20 | elif django.get_version().startswith('2.1'): 21 | return DjangoVersion.DJANGO_21 22 | 23 | 24 | def get_model_name(model): 25 | if django_version() == DjangoVersion.DJANGO_18: 26 | return '%s.%s' % (model._meta.app_label, model._meta.object_name) 27 | 28 | return model._meta.label 29 | 30 | 31 | def get_all_permissions(opts, ctype=None): 32 | if django_version() < DjangoVersion.DJANGO_110: 33 | return _get_all_permissions(opts, ctype) 34 | 35 | return _get_all_permissions(opts) 36 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoAdminViewPermission.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoAdminViewPermission.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoAdminViewPermission" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoAdminViewPermission" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/command.rst: -------------------------------------------------------------------------------- 1 | Management commands 2 | =================== 3 | 4 | The admin view permission provides one management command which fixes the 5 | permissions on the proxy models. 6 | 7 | 8 | fix_proxy_permissions 9 | --------------------- 10 | This command will create the appropriate entries on the `ContentType` and 11 | `Permission` models. Then it will delete the permissions, which are created 12 | from django `migrate` command and are associated with the parent model. More 13 | information you can find `here `_. 14 | 15 | Example 16 | ~~~~~~~ 17 | :: 18 | 19 | python manage.py fix_proxy_permissions -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django Admin View Permission documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 5 10:18:06 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix(es) of source filenames. 38 | # You can specify multiple suffix as a list of string: 39 | # 40 | # source_suffix = ['.rst', '.md'] 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | # 45 | # source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'Django Admin View Permission' 52 | copyright = u'2016, Context Information Security' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = u'0.1' 60 | # The full version, including alpha/beta/rc tags. 61 | release = u'0.1' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # There are two options for replacing |today|: either, you set today to some 71 | # non-false value, then it is used: 72 | # 73 | # today = '' 74 | # 75 | # Else, today_fmt is used as the format for a strftime call. 76 | # 77 | # today_fmt = '%B %d, %Y' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | # This patterns also effect to html_static_path and html_extra_path 82 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | # 87 | # default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | # 91 | # add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | # 96 | # add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | # 101 | # show_authors = False 102 | 103 | # The name of the Pygments (syntax highlighting) style to use. 104 | pygments_style = 'sphinx' 105 | 106 | # A list of ignored prefixes for module index sorting. 107 | # modindex_common_prefix = [] 108 | 109 | # If true, keep warnings as "system message" paragraphs in the built documents. 110 | # keep_warnings = False 111 | 112 | # If true, `todo` and `todoList` produce output, else they produce nothing. 113 | todo_include_todos = False 114 | 115 | 116 | # -- Options for HTML output ---------------------------------------------- 117 | 118 | # The theme to use for HTML and HTML Help pages. See the documentation for 119 | # a list of builtin themes. 120 | # 121 | html_theme = 'default' 122 | 123 | # Theme options are theme-specific and customize the look and feel of a theme 124 | # further. For a list of options available for each theme, see the 125 | # documentation. 126 | # 127 | # html_theme_options = {} 128 | 129 | # Add any paths that contain custom themes here, relative to this directory. 130 | # html_theme_path = [] 131 | 132 | # The name for this set of Sphinx documents. 133 | # " v documentation" by default. 134 | # 135 | # html_title = u'Django Admin View Permission v0.1' 136 | 137 | # A shorter title for the navigation bar. Default is the same as html_title. 138 | # 139 | # html_short_title = None 140 | 141 | # The name of an image file (relative to this directory) to place at the top 142 | # of the sidebar. 143 | # 144 | # html_logo = None 145 | 146 | # The name of an image file (relative to this directory) to use as a favicon of 147 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 148 | # pixels large. 149 | # 150 | # html_favicon = None 151 | 152 | # Add any paths that contain custom static files (such as style sheets) here, 153 | # relative to this directory. They are copied after the builtin static files, 154 | # so a file named "default.css" will overwrite the builtin "default.css". 155 | html_static_path = ['_static'] 156 | 157 | # Add any extra paths that contain custom files (such as robots.txt or 158 | # .htaccess) here, relative to this directory. These files are copied 159 | # directly to the root of the documentation. 160 | # 161 | # html_extra_path = [] 162 | 163 | # If not None, a 'Last updated on:' timestamp is inserted at every page 164 | # bottom, using the given strftime format. 165 | # The empty string is equivalent to '%b %d, %Y'. 166 | # 167 | # html_last_updated_fmt = None 168 | 169 | # If true, SmartyPants will be used to convert quotes and dashes to 170 | # typographically correct entities. 171 | # 172 | # html_use_smartypants = True 173 | 174 | # Custom sidebar templates, maps document names to template names. 175 | # 176 | # html_sidebars = {} 177 | 178 | # Additional templates that should be rendered to pages, maps page names to 179 | # template names. 180 | # 181 | # html_additional_pages = {} 182 | 183 | # If false, no module index is generated. 184 | # 185 | # html_domain_indices = True 186 | 187 | # If false, no index is generated. 188 | # 189 | # html_use_index = True 190 | 191 | # If true, the index is split into individual pages for each letter. 192 | # 193 | # html_split_index = False 194 | 195 | # If true, links to the reST sources are added to the pages. 196 | # 197 | # html_show_sourcelink = True 198 | 199 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 200 | # 201 | # html_show_sphinx = True 202 | 203 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 204 | # 205 | # html_show_copyright = True 206 | 207 | # If true, an OpenSearch description file will be output, and all pages will 208 | # contain a tag referring to it. The value of this option must be the 209 | # base URL from which the finished HTML is served. 210 | # 211 | # html_use_opensearch = '' 212 | 213 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 214 | # html_file_suffix = None 215 | 216 | # Language to be used for generating the HTML full-text search index. 217 | # Sphinx supports the following languages: 218 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 219 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 220 | # 221 | # html_search_language = 'en' 222 | 223 | # A dictionary with options for the search language support, empty by default. 224 | # 'ja' uses this config value. 225 | # 'zh' user can custom change `jieba` dictionary path. 226 | # 227 | # html_search_options = {'type': 'default'} 228 | 229 | # The name of a javascript file (relative to the configuration directory) that 230 | # implements a search results scorer. If empty, the default will be used. 231 | # 232 | # html_search_scorer = 'scorer.js' 233 | 234 | # Output file base name for HTML help builder. 235 | htmlhelp_basename = 'DjangoAdminViewPermissiondoc' 236 | 237 | # -- Options for LaTeX output --------------------------------------------- 238 | 239 | latex_elements = { 240 | # The paper size ('letterpaper' or 'a4paper'). 241 | # 242 | # 'papersize': 'letterpaper', 243 | 244 | # The font size ('10pt', '11pt' or '12pt'). 245 | # 246 | # 'pointsize': '10pt', 247 | 248 | # Additional stuff for the LaTeX preamble. 249 | # 250 | # 'preamble': '', 251 | 252 | # Latex figure (float) alignment 253 | # 254 | # 'figure_align': 'htbp', 255 | } 256 | 257 | # Grouping the document tree into LaTeX files. List of tuples 258 | # (source start file, target name, title, 259 | # author, documentclass [howto, manual, or own class]). 260 | latex_documents = [ 261 | (master_doc, 'DjangoAdminViewPermission.tex', u'Django Admin View Permission Documentation', 262 | u'Context Information Security', 'manual'), 263 | ] 264 | 265 | # The name of an image file (relative to this directory) to place at the top of 266 | # the title page. 267 | # 268 | # latex_logo = None 269 | 270 | # For "manual" documents, if this is true, then toplevel headings are parts, 271 | # not chapters. 272 | # 273 | # latex_use_parts = False 274 | 275 | # If true, show page references after internal links. 276 | # 277 | # latex_show_pagerefs = False 278 | 279 | # If true, show URL addresses after external links. 280 | # 281 | # latex_show_urls = False 282 | 283 | # Documents to append as an appendix to all manuals. 284 | # 285 | # latex_appendices = [] 286 | 287 | # If false, no module index is generated. 288 | # 289 | # latex_domain_indices = True 290 | 291 | 292 | # -- Options for manual page output --------------------------------------- 293 | 294 | # One entry per manual page. List of tuples 295 | # (source start file, name, description, authors, manual section). 296 | man_pages = [ 297 | (master_doc, 'djangoadminviewpermission', u'Django Admin View Permission Documentation', 298 | [u'Context Information Security'], 1) 299 | ] 300 | 301 | # If true, show URL addresses after external links. 302 | # 303 | # man_show_urls = False 304 | 305 | 306 | # -- Options for Texinfo output ------------------------------------------- 307 | 308 | # Grouping the document tree into Texinfo files. List of tuples 309 | # (source start file, target name, title, author, 310 | # dir menu entry, description, category) 311 | texinfo_documents = [ 312 | (master_doc, 'DjangoAdminViewPermission', u'Django Admin View Permission Documentation', 313 | u'Context Information Security', 'DjangoAdminViewPermission', 'One line description of project.', 314 | 'Miscellaneous'), 315 | ] 316 | 317 | # Documents to append as an appendix to all manuals. 318 | # 319 | # texinfo_appendices = [] 320 | 321 | # If false, no module index is generated. 322 | # 323 | # texinfo_domain_indices = True 324 | 325 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 326 | # 327 | # texinfo_show_urls = 'footnote' 328 | 329 | # If true, do not generate a @detailmenu in the "Top" node's menu. 330 | # 331 | # texinfo_no_detailmenu = False 332 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | The admin view permission provides one setting that you can add in your project's 5 | settings module to customize its behavior. 6 | 7 | ADMIN_VIEW_PERMISSION_MODELS 8 | ---------------------------- 9 | 10 | This setting defines which models you want to be added the view permission. If 11 | you don't specify this setting then the view permission will be applied to all 12 | the models. 13 | 14 | Example 15 | ~~~~~~~ 16 | :: 17 | 18 | ADMIN_VIEW_PERMISSION_MODELS = [ 19 | 'auth.User', 20 | ... 21 | ] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django Admin View Permission documentation master file, created by 2 | sphinx-quickstart on Tue Jul 5 10:18:06 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django Admin View Permission's documentation! 7 | ======================================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | installation 15 | configuration 16 | command 17 | uninstall 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Getting the code 5 | ---------------- 6 | 7 | The recommended way to install the ``Admin View Permission`` is via pip_:: 8 | 9 | $ pip install django-admin-view-permission 10 | 11 | To test an upcoming release, you can install the in-development version 12 | instead with the following command:: 13 | 14 | $ pip install -e git+https://github.com/django-admin-view-permission/django-admin-view-permission.git#egg=django-admin-view-permission 15 | 16 | Requirements 17 | ------------ 18 | 19 | * Django 20 | 21 | Support 22 | ------- 23 | 24 | * Django: 1.8, 1.9, 1.10, 1.11, 2.0 25 | * Python: 2.7, 3.4, 3.5, 3.6 26 | 27 | Setup 28 | ----- 29 | 30 | Make sure that ``'django.contrib.admin'`` is set up properly and add 31 | ``'admin_view_permission'`` to your ``INSTALLED_APPS`` setting:: 32 | 33 | INSTALLED_APPS = [ 34 | 'admin_view_permission', 35 | # ... 36 | 'django.contrib.admin', 37 | # ... 38 | ] 39 | 40 | Finally, run ``python manage.py migrate`` to create the view permissions. 41 | 42 | In case of a customized AdminSite in order to apply the view permission, you 43 | should inherit from the ```AdminViewPermissionAdminSite``` class:: 44 | 45 | from admin_view_permission.admin import AdminViewPermissionAdminSite 46 | 47 | class MyAdminSite(AdminViewPermissionAdminSite): 48 | ... 49 | 50 | 51 | .. _pip: https://pip.pypa.io/ -------------------------------------------------------------------------------- /docs/uninstall.rst: -------------------------------------------------------------------------------- 1 | Uninstall 2 | ========= 3 | 4 | To remove the application completely firstly remove the ``admin_view_permission`` 5 | from your ``INSTALLED_APPS`` setting and then open a debug shell and execute 6 | the following commands in order to remove these extra permissions from the 7 | database:: 8 | 9 | from django.contrib.auth.models import Permission 10 | permissions = Permission.objects.filter(codename__startswith='view') 11 | permissions.delete() 12 | 13 | .. note:: Before delete the permission would be helpful to check if the 14 | permissions queryset contains only the view permissions and not anything else. 15 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | django_find_project=false 3 | DJANGO_SETTINGS_MODULE=tests.settings -------------------------------------------------------------------------------- /requirements-debug.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | # Style tools 4 | flake8 5 | flake8-isort 6 | 7 | # Unit-testing 8 | py>=1.5.0 9 | pytest-django 10 | pytest-xdist 11 | pytest-cov 12 | coveralls 13 | parameterized 14 | model_mommy 15 | mock 16 | 17 | # Docs 18 | sphinx 19 | sphinx-autobuild 20 | 21 | # To test django-parler compatibility 22 | django-parler==1.9.1 23 | 24 | # To parse HTML in tests 25 | beautifulsoup4==4.6.0 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.8; python_version > '2.7' 2 | Django>=1.8,<2.0; python_version <= '2.7' 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F405, W504 3 | exclude = env,docs 4 | 5 | [isort] 6 | skip=env 7 | include_trailing_comma=true 8 | multi_line_output=3 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import find_packages, setup 5 | 6 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 7 | README = readme.read() 8 | 9 | # allow setup.py to be run from any path 10 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 11 | 12 | if sys.version_info.major > 2: 13 | install_requires = ['Django>=1.8'] 14 | else: 15 | install_requires = ['Django>=1.8,<2.0'] 16 | 17 | setup( 18 | name='django-admin-view-permission', 19 | version='1.9', 20 | packages=find_packages(), 21 | include_package_data=True, 22 | license='BSD License', 23 | description='A simple Django app which adds view permissions.', 24 | long_description=README, 25 | keywords=['django', 'admin'], 26 | url='http://django-admin-view-permission.readthedocs.org/', 27 | classifiers=[ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Environment :: Web Environment', 30 | 'Framework :: Django', 31 | 'Framework :: Django :: 1.9', 32 | 'Framework :: Django :: 2.0', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: BSD License', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3.4', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Topic :: Internet :: WWW/HTTP', 42 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 43 | ], 44 | install_requires=install_requires, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_test project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '_ph=4u3(-7a%3m9du-&jq2x2yh22+w^zyf2bam69*n4(&w_8+$' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'admin_view_permission', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'tests.test_app' 42 | ] 43 | 44 | # For version of Django smaller than 1.10 45 | MIDDLEWARE_CLASSES = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | # For versions of Django bigger or equal than 1.10 56 | MIDDLEWARE = [ 57 | 'django.middleware.security.SecurityMiddleware', 58 | 'django.contrib.sessions.middleware.SessionMiddleware', 59 | 'django.middleware.common.CommonMiddleware', 60 | 'django.middleware.csrf.CsrfViewMiddleware', 61 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 62 | 'django.contrib.messages.middleware.MessageMiddleware', 63 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 64 | ] 65 | 66 | ROOT_URLCONF = 'tests.urls' 67 | 68 | TEMPLATES = [ 69 | { 70 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 71 | 'DIRS': [], 72 | 'APP_DIRS': True, 73 | 'OPTIONS': { 74 | 'context_processors': [ 75 | 'django.template.context_processors.debug', 76 | 'django.template.context_processors.request', 77 | 'django.contrib.auth.context_processors.auth', 78 | 'django.contrib.messages.context_processors.messages', 79 | ], 80 | }, 81 | }, 82 | ] 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 86 | 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.sqlite3', 90 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [] 99 | 100 | 101 | # Internationalization 102 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 103 | 104 | LANGUAGE_CODE = 'en-us' 105 | 106 | TIME_ZONE = 'UTC' 107 | 108 | USE_I18N = True 109 | 110 | USE_L10N = True 111 | 112 | USE_TZ = True 113 | 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 117 | 118 | STATIC_URL = '/static/' 119 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/tests/test_app/__init__.py -------------------------------------------------------------------------------- /tests/test_app/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib import admin 4 | from parler.admin import TranslatableAdmin 5 | 6 | from admin_view_permission import admin as view_admin 7 | 8 | from .models import * # noqa: F403 9 | 10 | 11 | # Modeladmin for the UI 12 | class StackedModelAdmin(admin.StackedInline): 13 | model = TestModel2 14 | 15 | 16 | class TabularModelAdmin(admin.TabularInline): 17 | model = TestModel3 18 | 19 | 20 | class DefaultModelAdmin(admin.ModelAdmin): 21 | inlines = [ 22 | StackedModelAdmin, 23 | TabularModelAdmin 24 | ] 25 | 26 | 27 | admin.site.register(TestModel1, DefaultModelAdmin) 28 | 29 | 30 | class TestModelParlerAdmin(TranslatableAdmin, admin.ModelAdmin): 31 | model = TestModelParler 32 | 33 | 34 | admin.site.register(TestModelParler, TestModelParlerAdmin) 35 | 36 | 37 | # Modeladmin for testing 38 | class StackedModelAdmin1(admin.StackedInline): 39 | model = TestModel4 40 | 41 | 42 | class TabularModelAdmin2(admin.TabularInline): 43 | model = TestModel6 44 | 45 | 46 | class ModelAdmin1(view_admin.AdminViewPermissionModelAdmin): 47 | inlines = [ 48 | StackedModelAdmin1, 49 | TabularModelAdmin2, 50 | ] 51 | -------------------------------------------------------------------------------- /tests/test_app/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class TestAppConfig(AppConfig): 7 | name = 'test_app' 8 | -------------------------------------------------------------------------------- /tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | from parler.models import TranslatableModel, TranslatedFields 5 | 6 | 7 | class TestModel0(models.Model): 8 | var1 = models.CharField(max_length=200) 9 | var2 = models.TextField() 10 | var3 = models.IntegerField() 11 | 12 | 13 | class TestModel1(models.Model): 14 | var1 = models.CharField(max_length=200) 15 | var2 = models.TextField() 16 | var3 = models.IntegerField() 17 | var4 = models.ManyToManyField(TestModel0) 18 | var5 = models.TextField(editable=False) 19 | 20 | @property 21 | def var6(self): 22 | return 'readonly_field' 23 | 24 | 25 | class TestModel2(models.Model): 26 | var1 = models.ForeignKey(TestModel1, on_delete=models.CASCADE) 27 | var2 = models.CharField(max_length=200) 28 | var3 = models.TextField() 29 | var4 = models.IntegerField 30 | 31 | 32 | class TestModel3(models.Model): 33 | var1 = models.ForeignKey(TestModel1, on_delete=models.CASCADE) 34 | var2 = models.CharField(max_length=200) 35 | var3 = models.TextField() 36 | var4 = models.IntegerField() 37 | 38 | 39 | class TestModel4(models.Model): 40 | var1 = models.ForeignKey(TestModel1, on_delete=models.CASCADE) 41 | var2 = models.CharField(max_length=200) 42 | var3 = models.TextField() 43 | var4 = models.IntegerField() 44 | 45 | 46 | # Copy of the TestModel1 to exam model with different key 47 | class TestModel5(models.Model): 48 | var0 = models.AutoField(primary_key=True) 49 | var1 = models.CharField(max_length=200) 50 | var2 = models.TextField() 51 | var3 = models.IntegerField() 52 | var4 = models.ManyToManyField(TestModel0) 53 | 54 | 55 | # Copy of the TestModel4 to exam model with different key 56 | class TestModel6(models.Model): 57 | var0 = models.AutoField(primary_key=True) 58 | var1 = models.ForeignKey(TestModel1, on_delete=models.CASCADE) 59 | var2 = models.CharField(max_length=200) 60 | var3 = models.TextField() 61 | var4 = models.IntegerField() 62 | 63 | 64 | class TestModelParler(TranslatableModel): 65 | var1 = models.CharField(max_length=200) 66 | var2 = models.TextField() 67 | var3 = models.IntegerField() 68 | 69 | translations = TranslatedFields( 70 | var4=models.CharField(max_length=20), 71 | var5=models.TextField(), 72 | ) 73 | 74 | @property 75 | def var6(self): 76 | return 'readonly_field' 77 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/tests/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/tests/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/tests/functional/test_fix_proxy_permissions.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.contrib.auth.models import Permission 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.core.management import call_command 7 | from django.test import TestCase 8 | 9 | User = get_user_model() 10 | 11 | 12 | class TestFixProxyPermission(TestCase): 13 | 14 | @classmethod 15 | def setUpClass(cls): 16 | super(TestFixProxyPermission, cls).setUpClass() 17 | 18 | class Meta: 19 | proxy = True 20 | 21 | attrs = { 22 | '__module__': 'tests.test_app.models', 23 | 'Meta': Meta, 24 | } 25 | 26 | cls.proxy_model = type(str('AppTestProxyModel'), (User, ), 27 | attrs.copy()) 28 | 29 | def test_fix_proxy_permissions_without_command_and_migration(self): 30 | model = self.proxy_model._meta.model_name 31 | ctypes = ContentType.objects.filter(model=model) 32 | permissions = Permission.objects.filter( 33 | codename__contains='apptestproxymodel') 34 | 35 | assert ctypes.count() == 0 36 | assert permissions.count() == 0 37 | 38 | def test_fix_proxy_permissions_without_migration(self): 39 | call_command('fix_proxy_permissions') 40 | model = self.proxy_model._meta.model_name 41 | ctypes = ContentType.objects.filter(model=model) 42 | permissions = Permission.objects.filter( 43 | codename__contains='apptestproxymodel') 44 | 45 | assert ctypes.count() == 1 46 | assert permissions.count() == 4 47 | 48 | def test_fix_proxy_permissions(self): 49 | call_command('migrate') 50 | call_command('fix_proxy_permissions') 51 | model = self.proxy_model._meta.model_name 52 | ctypes = ContentType.objects.filter(model=model) 53 | permissions = Permission.objects.filter( 54 | codename__contains='apptestproxymodel') 55 | 56 | assert ctypes.count() == 1 57 | assert permissions.count() == 4 58 | -------------------------------------------------------------------------------- /tests/tests/functional/test_others.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib import admin 4 | from django.test import SimpleTestCase 5 | 6 | from admin_view_permission.admin import AdminViewPermissionModelAdmin 7 | 8 | 9 | class TestTestAppModelAdminOverride(SimpleTestCase): 10 | 11 | def test_testapp_modeladmin_override(self): 12 | for model in admin.site._registry: 13 | assert isinstance( 14 | admin.site._registry[model], AdminViewPermissionModelAdmin) 15 | assert isinstance( 16 | admin.site._registry[model], admin.ModelAdmin) 17 | -------------------------------------------------------------------------------- /tests/tests/functional/test_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from bs4 import BeautifulSoup 4 | from django import VERSION 5 | from django.conf import settings 6 | from django.test import override_settings 7 | from model_mommy import mommy 8 | 9 | from tests.tests.helpers import AdminViewPermissionViewsTestCase 10 | 11 | try: 12 | from django.urls import reverse 13 | except ImportError: 14 | # django < 2.0 15 | from django.core.urlresolvers import reverse 16 | 17 | 18 | class TestModelAdminViews(AdminViewPermissionViewsTestCase): 19 | 20 | # admin index 21 | 22 | def test_index_view_from_simple_user(self): 23 | self.client.login( 24 | username='user_with_v_perm_on_model1', 25 | password='simple_user', 26 | ) 27 | response = self.client.get(reverse('admin:index')) 28 | 29 | assert len(response.context['app_list']) == 1 30 | assert response.context['app_list'][0]['app_label'] == 'test_app' 31 | assert len(response.context['app_list'][0]['models']) == 1 32 | assert (response.context['app_list'][0]['models'][0]['object_name'] == 33 | 'TestModel1') 34 | 35 | def test_index_view_from_super_user(self): 36 | self.client.login(username='super_user', password='super_user') 37 | response = self.client.get(reverse('admin:index')) 38 | 39 | assert len(response.context['app_list']) == 2 40 | 41 | # changeview 42 | 43 | def test_changelist_view_get_from_user_with_v_perm_on_model1(self): 44 | self.client.login( 45 | username='user_with_v_perm_on_model1', 46 | password='simple_user', 47 | ) 48 | response = self.client.get( 49 | reverse('admin:%s_%s_changelist' % ('test_app', 'testmodel1')), 50 | ) 51 | 52 | assert response.status_code == 200 53 | assert response.context['title'] == 'Select test model1 to view' 54 | 55 | def test_changelist_view_get_from_user_with_vd_perm_on_model1(self): 56 | self.client.login( 57 | username='user_with_vd_perm_on_model1', 58 | password='simple_user', 59 | ) 60 | response = self.client.get( 61 | reverse('admin:%s_%s_changelist' % ('test_app', 'testmodel1')), 62 | ) 63 | 64 | assert response.status_code == 200 65 | assert response.context['title'] == 'Select test model1 to view' 66 | 67 | def test_changelist_view_post_from_user_with_vd_perm_on_model1(self): 68 | obj = mommy.make('test_app.TestModel1') 69 | data = { 70 | 'index': ['0'], 71 | 'action': ['delete_selected'], 72 | 'select_across': ['0'], 73 | '_selected_action': [str(obj.pk)] 74 | } 75 | self.client.login( 76 | username='user_with_vd_perm_on_model1', 77 | password='simple_user', 78 | ) 79 | response = self.client.post( 80 | reverse('admin:%s_%s_changelist' % ('test_app', 'testmodel1')), 81 | data=data, 82 | ) 83 | 84 | assert response.status_code == 200 85 | assert response.context['title'] == 'Are you sure?' 86 | 87 | def test_changelist_view_get_from_simple_user_as_popup(self): 88 | self.client.login( 89 | username='user_with_v_perm_on_model1', 90 | password='simple_user', 91 | ) 92 | response = self.client.get( 93 | (reverse('admin:%s_%s_changelist' % ('test_app', 'testmodel1')) + 94 | '?_to_field=id&_popup=1'), 95 | ) 96 | 97 | assert response.status_code == 200 98 | assert response.context['title'] == 'Select test model1' 99 | 100 | def test_changelist_view_get_from_super_user(self): 101 | self.client.login(username='super_user', password='super_user') 102 | response = self.client.get( 103 | reverse('admin:%s_%s_changelist' % ('test_app', 'testmodel1')), 104 | ) 105 | 106 | assert response.status_code == 200 107 | assert response.context['title'] == 'Select test model1 to change' 108 | 109 | # history 110 | 111 | def test_history_view_from_simple_user(self): 112 | obj = mommy.make('test_app.TestModel1') 113 | self.client.login( 114 | username='user_with_v_perm_on_model1', 115 | password='simple_user', 116 | ) 117 | response = self.client.get( 118 | reverse('admin:%s_%s_history' % ('test_app', 'testmodel1'), 119 | args=(obj.pk,)), 120 | ) 121 | 122 | assert response.status_code == 200 123 | 124 | def test_history_view_from_super_user(self): 125 | obj = mommy.make('test_app.TestModel1') 126 | self.client.login(username='super_user', password='super_user') 127 | response = self.client.get( 128 | reverse('admin:%s_%s_history' % ('test_app', 'testmodel1'), 129 | args=(obj.pk,)), 130 | ) 131 | 132 | assert response.status_code == 200 133 | 134 | # add 135 | 136 | def test_add_view_from_simple_user(self): 137 | self.client.login( 138 | username='user_with_v_perm_on_model1', 139 | password='simple_user', 140 | ) 141 | response = self.client.get( 142 | reverse('admin:%s_%s_add' % ('test_app', 'testmodel1')), 143 | ) 144 | 145 | assert response.status_code == 403 146 | 147 | def test_add_view_from_super_user(self): 148 | self.client.login(username='super_user', password='super_user') 149 | response = self.client.get( 150 | reverse('admin:%s_%s_add' % ('test_app', 'testmodel1')), 151 | ) 152 | 153 | assert response.status_code == 200 154 | 155 | # change 156 | 157 | def test_change_view_from_simple_user(self): 158 | obj = mommy.make('test_app.TestModel1') 159 | self.client.login( 160 | username='user_with_v_perm_on_model1', 161 | password='simple_user', 162 | ) 163 | response = self.client.get( 164 | reverse('admin:%s_%s_change' % ('test_app', 'testmodel1'), 165 | args=(obj.pk,)), 166 | ) 167 | 168 | assert response.status_code == 200 169 | 170 | def test_change_view_from_simple_user_translatable(self): 171 | """ 172 | Smoke test: check if the change view renders for a django-parler model. 173 | """ 174 | # Ensure parler's templates are registered through INSTALLED_APPS, 175 | # but only for this test. This mimics the install steps at 176 | # http://django-parler.readthedocs.io/en/latest/quickstart.html. 177 | current_installed_apps = settings.INSTALLED_APPS 178 | installed_apps_with_parler = current_installed_apps + ['parler'] 179 | 180 | with override_settings(INSTALLED_APPS=installed_apps_with_parler): 181 | obj = mommy.make('test_app.TestModelParler') 182 | self.client.login( 183 | username='user_with_v_perm_on_model1parler', 184 | password='simple_user', 185 | ) 186 | response = self.client.get( 187 | reverse( 188 | 'admin:%s_%s_change' % ( 189 | 'test_app', 'testmodelparler' 190 | ), 191 | args=(obj.pk,) 192 | ), 193 | ) 194 | assert response.status_code == 200 195 | 196 | # var4 is a translatable field on this model. Check that it is 197 | # marked as read only under Django versions supporting it. 198 | if VERSION[0:2] == (1, 11) or VERSION[0] == 2: 199 | bs = BeautifulSoup(response.content.decode('utf-8'), 200 | 'html.parser') 201 | var4_tags = bs.select('.field-var4') 202 | assert len(var4_tags) == 1 203 | assert len(var4_tags[0].select('.readonly')) == 1 204 | 205 | def test_change_view_from_simple_user_unauthorized_post(self): 206 | obj = mommy.make('test_app.TestModel1') 207 | self.client.login( 208 | username='user_with_v_perm_on_model1', 209 | password='simple_user', 210 | ) 211 | data = { 212 | 'var1': 'test', 213 | 'var2': 'test', 214 | 'var3': 1, 215 | 'var4': mommy.make('test_app.TestModel0'), 216 | 'testmodel2_set-TOTAL_FORMS': 0, 217 | 'testmodel2_set-INITIAL_FORMS': 0, 218 | 'testmodel3_set-TOTAL_FORMS': 0, 219 | 'testmodel3_set-INITIAL_FORMS': 0 220 | } 221 | response = self.client.post( 222 | reverse('admin:%s_%s_change' % ('test_app', 'testmodel1'), 223 | args=(obj.pk,)), 224 | data, 225 | ) 226 | obj.refresh_from_db() 227 | 228 | assert response.status_code == 302 229 | assert obj.var1 != 'test' 230 | assert obj.var2 != 'test' 231 | assert obj.var3 != 1 232 | 233 | def test_change_view_from_super_user(self): 234 | obj = mommy.make('test_app.TestModel1') 235 | self.client.login(username='super_user', password='super_user') 236 | response = self.client.get( 237 | reverse('admin:%s_%s_change' % ('test_app', 'testmodel1'), 238 | args=(obj.pk, )), 239 | ) 240 | 241 | assert response.status_code == 200 242 | 243 | # delete 244 | 245 | def test_delete_view_from_simple_user(self): 246 | obj = mommy.make('test_app.TestModel1') 247 | self.client.login( 248 | username='user_with_v_perm_on_model1', 249 | password='simple_user', 250 | ) 251 | response = self.client.get( 252 | reverse('admin:%s_%s_delete' % ('test_app', 'testmodel1'), 253 | args=(obj.pk,)), 254 | ) 255 | 256 | assert response.status_code == 403 257 | 258 | def test_delete_view_from_simple_user_unauthorized_post(self): 259 | obj = mommy.make('test_app.TestModel1') 260 | self.client.login( 261 | username='user_with_v_perm_on_model1', 262 | password='simple_user', 263 | ) 264 | response = self.client.post( 265 | reverse('admin:%s_%s_delete' % ('test_app', 'testmodel1'), 266 | args=(obj.pk,)), 267 | ) 268 | 269 | obj.refresh_from_db() 270 | 271 | assert response.status_code == 403 272 | assert obj 273 | 274 | def test_delete_view_from_super_user(self): 275 | obj = mommy.make('test_app.TestModel1') 276 | self.client.login(username='super_user', password='super_user') 277 | response = self.client.get( 278 | reverse('admin:%s_%s_delete' % ('test_app', 'testmodel1'), 279 | args=(obj.pk,)), 280 | ) 281 | 282 | assert response.status_code == 200 283 | -------------------------------------------------------------------------------- /tests/tests/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf.urls import url 4 | from django.contrib.auth import get_user_model 5 | from django.contrib.auth.models import Permission 6 | from django.test import Client, TestCase 7 | from model_mommy import mommy 8 | from model_mommy.recipe import seq 9 | 10 | SIMPLE_USERNAME = seq('simple_user_') 11 | SUPER_USERNAME = seq('super_user_') 12 | 13 | 14 | def create_simple_user(username=None): 15 | if not username: 16 | username = SIMPLE_USERNAME 17 | 18 | simple_user = mommy.make(get_user_model(), username=username) 19 | simple_user.set_password('simple_user') 20 | # Django 1.8 compatibility 21 | simple_user.is_staff = True 22 | simple_user.save() 23 | 24 | return simple_user 25 | 26 | 27 | def create_super_user(username=None): 28 | if not username: 29 | username = SUPER_USERNAME 30 | 31 | super_user = mommy.make( 32 | get_user_model(), 33 | username=username, is_superuser=True) 34 | super_user.set_password('super_user') 35 | # Django 1.8 compatibility 36 | super_user.is_staff = True 37 | super_user.save() 38 | 39 | return super_user 40 | 41 | 42 | def create_urlconf(admin_site): 43 | return type( 44 | str('Urlconf'), 45 | (object,), 46 | {'urlpatterns': [ 47 | url('test_admin/', admin_site.urls) 48 | ]} 49 | ) 50 | 51 | 52 | class DataMixin(object): 53 | 54 | @classmethod 55 | def setUpTestData(cls): 56 | # Permissions 57 | cls.add_permission_model1 = Permission.objects.get( 58 | name='Can add test model1') 59 | cls.view_permission_model1 = Permission.objects.get( 60 | name='Can view testmodel1') 61 | cls.change_permission_model1 = Permission.objects.get( 62 | name='Can change test model1') 63 | cls.delete_permission_model1 = Permission.objects.get( 64 | name='Can delete test model1') 65 | 66 | cls.view_permission_model1parler = Permission.objects.get( 67 | name='Can view testmodelparler' 68 | ) 69 | cls.view_permission_model1parlertranslation = Permission.objects.get( 70 | name='Can view testmodelparlertranslation' 71 | ) 72 | 73 | cls.add_permission_model4 = Permission.objects.get( 74 | name='Can add test model4') 75 | cls.view_permission_model4 = Permission.objects.get( 76 | name='Can view testmodel4') 77 | cls.change_permission_model4 = Permission.objects.get( 78 | name='Can change test model4') 79 | cls.delete_permission_model4 = Permission.objects.get( 80 | name='Can delete test model4') 81 | 82 | cls.add_permission_model5 = Permission.objects.get( 83 | name='Can add test model5') 84 | cls.view_permission_model5 = Permission.objects.get( 85 | name='Can view testmodel5') 86 | cls.change_permission_model5 = Permission.objects.get( 87 | name='Can change test model5') 88 | cls.delete_permission_model5 = Permission.objects.get( 89 | name='Can delete test model5') 90 | 91 | cls.add_permission_model6 = Permission.objects.get( 92 | name='Can add test model6') 93 | cls.view_permission_model6 = Permission.objects.get( 94 | name='Can view testmodel6') 95 | cls.change_permission_model6 = Permission.objects.get( 96 | name='Can change test model6') 97 | cls.delete_permission_model6 = Permission.objects.get( 98 | name='Can delete test model6') 99 | 100 | 101 | class AdminViewPermissionViewsTestCase(DataMixin, TestCase): 102 | 103 | @classmethod 104 | def setUpTestData(cls): 105 | super(AdminViewPermissionViewsTestCase, cls).setUpTestData() 106 | 107 | cls.user_with_v_perm_on_model1 = create_simple_user( 108 | username='user_with_v_perm_on_model1', 109 | ) 110 | cls.user_with_v_perm_on_model1.user_permissions.add( 111 | cls.view_permission_model1, 112 | ) 113 | 114 | cls.user_with_vd_perm_on_moedl1 = create_simple_user( 115 | username='user_with_vd_perm_on_model1', 116 | ) 117 | cls.user_with_vd_perm_on_moedl1.user_permissions.add( 118 | cls.view_permission_model1, 119 | cls.delete_permission_model1, 120 | ) 121 | 122 | cls.user_with_v_perm_on_model1parler = create_simple_user( 123 | username='user_with_v_perm_on_model1parler' 124 | ) 125 | cls.user_with_v_perm_on_model1parler.user_permissions.add( 126 | cls.view_permission_model1parler, 127 | ) 128 | cls.user_with_v_perm_on_model1parler.user_permissions.add( 129 | cls.view_permission_model1parlertranslation, 130 | ) 131 | 132 | cls.super_user = create_super_user(username='super_user') 133 | 134 | def setUp(self): 135 | self.client = Client() 136 | 137 | def tearDown(self): 138 | self.client.logout() 139 | -------------------------------------------------------------------------------- /tests/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/django-admin-view-permission/f901b3d88c4e28f05b92c361e50d28c50e02543a/tests/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/tests/unit/test_admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from collections import OrderedDict, namedtuple 4 | 5 | import pytest 6 | from django import forms 7 | from django.contrib import admin 8 | from django.contrib.admin import AdminSite 9 | from django.test import ( 10 | RequestFactory, 11 | SimpleTestCase, 12 | TestCase, 13 | override_settings, 14 | ) 15 | from model_mommy import mommy 16 | from nose_parameterized import parameterized 17 | 18 | from admin_view_permission.admin import ( 19 | AdminViewPermissionAdminSite, 20 | AdminViewPermissionInlineModelAdmin, 21 | AdminViewPermissionModelAdmin, 22 | ) 23 | from tests.test_app.admin import ModelAdmin1 24 | from tests.test_app.models import TestModel1, TestModel5 25 | from tests.tests.helpers import ( 26 | DataMixin, 27 | create_simple_user, 28 | create_super_user, 29 | create_urlconf, 30 | ) 31 | 32 | try: 33 | from django.urls import reverse 34 | except ImportError: 35 | # django < 2.0 36 | from django.core.urlresolvers import reverse 37 | 38 | 39 | class TestAdminViewPermissionBaseModelAdmin(DataMixin, TestCase): 40 | 41 | @classmethod 42 | def setUpTestData(cls): 43 | super(TestAdminViewPermissionBaseModelAdmin, cls).setUpTestData() 44 | 45 | # Users 46 | cls.user_without_permissions = create_simple_user() 47 | 48 | cls.user_with_a_perm_on_model1 = create_simple_user() 49 | cls.user_with_a_perm_on_model1.user_permissions.add( 50 | cls.add_permission_model1) 51 | 52 | cls.user_with_v_perm_on_model1 = create_simple_user() 53 | cls.user_with_v_perm_on_model1.user_permissions.add( 54 | cls.view_permission_model1) 55 | 56 | cls.user_with_c_perm_on_model1 = create_simple_user() 57 | cls.user_with_c_perm_on_model1.user_permissions.add( 58 | cls.change_permission_model1) 59 | 60 | cls.user_with_d_perm_on_model1 = create_simple_user() 61 | cls.user_with_d_perm_on_model1.user_permissions.add( 62 | cls.delete_permission_model1) 63 | 64 | cls.user_with_av_perm_on_model1 = create_simple_user() 65 | cls.user_with_av_perm_on_model1.user_permissions.add( 66 | cls.add_permission_model1, 67 | cls.view_permission_model1) 68 | 69 | cls.user_with_cv_perm_on_model1 = create_simple_user() 70 | cls.user_with_cv_perm_on_model1.user_permissions.add( 71 | cls.change_permission_model1, 72 | cls.view_permission_model1) 73 | 74 | cls.user_with_dv_perm_on_model1 = create_simple_user() 75 | cls.user_with_dv_perm_on_model1.user_permissions.add( 76 | cls.delete_permission_model1, 77 | cls.view_permission_model1) 78 | 79 | cls.user_with_avc_perm_on_model1 = create_simple_user() 80 | cls.user_with_avc_perm_on_model1.user_permissions.add( 81 | cls.add_permission_model1, 82 | cls.view_permission_model1, 83 | cls.change_permission_model1) 84 | 85 | cls.user_with_avcd_perm_on_model1 = create_simple_user() 86 | cls.user_with_avcd_perm_on_model1.user_permissions.add( 87 | cls.add_permission_model1, 88 | cls.view_permission_model1, 89 | cls.change_permission_model1, 90 | cls.delete_permission_model1) 91 | 92 | cls.user_with_v_perm_on_model5 = create_simple_user() 93 | cls.user_with_v_perm_on_model5.user_permissions.add( 94 | cls.view_permission_model5) 95 | 96 | cls.super_user = create_super_user() 97 | 98 | @classmethod 99 | def setUpClass(cls): 100 | super(TestAdminViewPermissionBaseModelAdmin, cls).setUpClass() 101 | cls.factory = RequestFactory() 102 | 103 | def setUp(self): 104 | self.admin_site = AdminSite(name='test_admin') 105 | 106 | RequestUser = namedtuple('RequestUser', 'user, view') 107 | 108 | # Modeladmin 109 | 110 | def _modeladmin_simple(self): 111 | self.admin_site.register(TestModel1, ModelAdmin1) 112 | return ModelAdmin1(TestModel1, self.admin_site) 113 | 114 | def _modeladmin_with_id_on_fields(self): 115 | self.admin_site.register(TestModel1, ModelAdmin1) 116 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 117 | modeladmin.fields = ['id'] 118 | modeladmin.readonly_fields = ('id', ) 119 | 120 | return modeladmin 121 | 122 | def _modeladmin_with_property_on_fields(self): 123 | self.admin_site.register(TestModel1, ModelAdmin1) 124 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 125 | modeladmin.fields = ['var1', 'var2', 'var3', 'var4', 'var5', 'var6'] 126 | 127 | return modeladmin 128 | 129 | def _modeladmin_with_exclude_fields(self): 130 | self.admin_site.register(TestModel1, ModelAdmin1) 131 | modeladmin = ModelAdmin1(TestModel1, ModelAdmin1) 132 | modeladmin.exclude = ['var1'] 133 | 134 | return modeladmin 135 | 136 | def _modeladmin_with_tuple_as_fields(self): 137 | self.admin_site.register(TestModel1, ModelAdmin1) 138 | modeladmin = ModelAdmin1(TestModel1, ModelAdmin1) 139 | modeladmin.fields = (('var1', 'var2'), 'var3', 'var4', 'var5', 'var6') 140 | 141 | return modeladmin 142 | 143 | def _modeladmin_with_form_containing_exclude_fields(self): 144 | 145 | class TestModel1Form(forms.ModelForm): 146 | class Meta: 147 | model = TestModel1 148 | exclude = ['var1'] 149 | 150 | self.admin_site.register(TestModel1, ModelAdmin1) 151 | modeladmin = ModelAdmin1(TestModel1, ModelAdmin1) 152 | modeladmin.form = TestModel1Form 153 | 154 | return modeladmin 155 | 156 | def _modeladmin_with_func_on_fields(self): 157 | self.admin_site.register(TestModel1, ModelAdmin1) 158 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 159 | modeladmin.fields = ['var1', 'func'] 160 | 161 | return modeladmin 162 | 163 | # Objects 164 | 165 | def _obj_simple(self, obj_params): 166 | return TestModel1() 167 | 168 | GeneralParams = namedtuple( 169 | 'GeneralParams', 'name, request_user, obj_func, obj_params, ' 170 | 'modeladmin_func, result') 171 | 172 | general_params = [ 173 | # Add objects 174 | GeneralParams( 175 | name='add_from_a_simple_user_without_permissions', 176 | request_user=RequestUser('user_without_permissions', 'add'), 177 | obj_func=None, 178 | obj_params={}, 179 | modeladmin_func=_modeladmin_simple, 180 | result={ 181 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 182 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 183 | 'has_view_permission': False, 184 | 'has_change_permission': { 185 | 'default': False, 186 | 'change_only': False, 187 | }, 188 | } 189 | ), 190 | GeneralParams( 191 | name='add_from_a_simple_user_with_add_permission', 192 | request_user=RequestUser('user_with_a_perm_on_model1', 'add'), 193 | obj_func=None, 194 | obj_params={}, 195 | modeladmin_func=_modeladmin_simple, 196 | result={ 197 | 'get_readonly_fields': (), 198 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 199 | 'has_view_permission': False, 200 | 'has_change_permission': { 201 | 'default': False, 202 | 'change_only': False, 203 | }, 204 | } 205 | ), 206 | GeneralParams( 207 | name='add_from_a_simple_user_with_view_permission', 208 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 209 | obj_func=None, 210 | obj_params={}, 211 | modeladmin_func=_modeladmin_simple, 212 | result={ 213 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 214 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 215 | 'has_view_permission': True, 216 | 'has_change_permission': { 217 | 'default': True, 218 | 'change_only': False, 219 | }, 220 | } 221 | ), 222 | GeneralParams( 223 | name='add_from_a_simple_user_with_view_permission_and_id_on' 224 | '_fields', 225 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 226 | obj_func=None, 227 | obj_params={}, 228 | modeladmin_func=_modeladmin_with_id_on_fields, 229 | result={ 230 | 'get_readonly_fields': ('id', ), 231 | 'get_fields': ['id'], 232 | 'has_view_permission': True, 233 | 'has_change_permission': { 234 | 'default': True, 235 | 'change_only': False, 236 | }, 237 | } 238 | ), 239 | # View permission only, var5 is a non-editable field and var6 is a 240 | # property field. Get_readonly_fields will return this field but the 241 | # change_view will raise FieldError on change_permission. This is 242 | # normal because the default modeladmin requires those field to be on 243 | # the readonly_fields option 244 | GeneralParams( 245 | name='add_from_a_simple_user_with_view_permission_and_property_on' 246 | '_fields', 247 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 248 | obj_func=None, 249 | obj_params={}, 250 | modeladmin_func=_modeladmin_with_property_on_fields, 251 | result={ 252 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4', 'var5', 253 | 'var6'), 254 | 'get_fields': ['var1', 'var2', 'var3', 'var4', 'var5', 'var6'], 255 | 'has_view_permission': True, 256 | 'has_change_permission': { 257 | 'default': True, 258 | 'change_only': False, 259 | }, 260 | } 261 | ), 262 | GeneralParams( 263 | name='add_from_a_simple_user_with_view_permission_and_tuple_as' 264 | '_fields', 265 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 266 | obj_func=None, 267 | obj_params={}, 268 | modeladmin_func=_modeladmin_with_tuple_as_fields, 269 | result={ 270 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4', 'var5', 271 | 'var6'), 272 | 'get_fields': ['var1', 'var2', 'var3', 'var4', 'var5', 'var6'], 273 | 'has_view_permission': True, 274 | 'has_change_permission': { 275 | 'default': True, 276 | 'change_only': False, 277 | }, 278 | } 279 | ), 280 | GeneralParams( 281 | name='add_from_a_simple_user_with_view_permission_and_func_on' 282 | '_fields', 283 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 284 | obj_func=None, 285 | obj_params={}, 286 | modeladmin_func=_modeladmin_with_func_on_fields, 287 | result={ 288 | 'get_readonly_fields': ('var1', 'func'), 289 | 'get_fields': ['var1', 'func'], 290 | 'has_view_permission': True, 291 | 'has_change_permission': { 292 | 'default': True, 293 | 'change_only': False, 294 | }, 295 | } 296 | ), 297 | GeneralParams( 298 | name='add_from_a_simple_user_with_view_permission_and_exclude' 299 | '_fields', 300 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 301 | obj_func=None, 302 | obj_params={}, 303 | modeladmin_func=_modeladmin_with_exclude_fields, 304 | result={ 305 | 'get_readonly_fields': ('var2', 'var3', 'var4'), 306 | 'get_fields': ['var2', 'var3', 'var4'], 307 | 'has_view_permission': True, 308 | 'has_change_permission': { 309 | 'default': True, 310 | 'change_only': False, 311 | }, 312 | } 313 | ), 314 | GeneralParams( 315 | name='add_from_a_simple_user_with_change_permission', 316 | request_user=RequestUser('user_with_c_perm_on_model1', 'add'), 317 | obj_func=None, 318 | obj_params={}, 319 | modeladmin_func=_modeladmin_simple, 320 | result={ 321 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 322 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 323 | 'has_view_permission': False, 324 | 'has_change_permission': { 325 | 'default': True, 326 | 'change_only': True, 327 | }, 328 | } 329 | ), 330 | GeneralParams( 331 | name='add_from_a_simple_user_with_delete_permission', 332 | request_user=RequestUser('user_with_d_perm_on_model1', 'add'), 333 | obj_func=None, 334 | obj_params={}, 335 | modeladmin_func=_modeladmin_simple, 336 | result={ 337 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 338 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 339 | 'has_view_permission': False, 340 | 'has_change_permission': { 341 | 'default': False, 342 | 'change_only': False, 343 | }, 344 | } 345 | ), 346 | GeneralParams( 347 | name='add_from_a_simple_user_with_add_view_permission', 348 | request_user=RequestUser('user_with_av_perm_on_model1', 'add'), 349 | obj_func=None, 350 | obj_params={}, 351 | modeladmin_func=_modeladmin_simple, 352 | result={ 353 | 'get_readonly_fields': (), 354 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 355 | 'has_view_permission': True, 356 | 'has_change_permission': { 357 | 'default': True, 358 | 'change_only': False, 359 | }, 360 | } 361 | ), 362 | GeneralParams( 363 | name='add_from_a_simple_user_with_change_view_permission', 364 | request_user=RequestUser('user_with_cv_perm_on_model1', 'add'), 365 | obj_func=None, 366 | obj_params={}, 367 | modeladmin_func=_modeladmin_simple, 368 | result={ 369 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 370 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 371 | 'has_view_permission': True, 372 | 'has_change_permission': { 373 | 'default': True, 374 | 'change_only': True, 375 | }, 376 | } 377 | ), 378 | GeneralParams( 379 | name='add_from_a_simple_user_with_delete_view_permission', 380 | request_user=RequestUser('user_with_dv_perm_on_model1', 'add'), 381 | obj_func=None, 382 | obj_params={}, 383 | modeladmin_func=_modeladmin_simple, 384 | result={ 385 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 386 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 387 | 'has_view_permission': True, 388 | 'has_change_permission': { 389 | 'default': True, 390 | 'change_only': False, 391 | }, 392 | } 393 | ), 394 | GeneralParams( 395 | name='add_from_a_simple_user_with_add_view_change_permission', 396 | request_user=RequestUser('user_with_avc_perm_on_model1', 'add'), 397 | obj_func=None, 398 | obj_params={}, 399 | modeladmin_func=_modeladmin_simple, 400 | result={ 401 | 'get_readonly_fields': (), 402 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 403 | 'has_view_permission': True, 404 | 'has_change_permission': { 405 | 'default': True, 406 | 'change_only': True, 407 | }, 408 | } 409 | ), 410 | GeneralParams( 411 | name='add_from_a_simple_user_with_all_permissions', 412 | request_user=RequestUser('user_with_avcd_perm_on_model1', 'add'), 413 | obj_func=None, 414 | obj_params={}, 415 | modeladmin_func=_modeladmin_simple, 416 | result={ 417 | 'get_readonly_fields': (), 418 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 419 | 'has_view_permission': True, 420 | 'has_change_permission': { 421 | 'default': True, 422 | 'change_only': True, 423 | }, 424 | } 425 | ), 426 | GeneralParams( 427 | name='add_from_a_super_user', 428 | request_user=RequestUser('super_user', 'add'), 429 | obj_func=None, 430 | obj_params={}, 431 | modeladmin_func=_modeladmin_simple, 432 | result={ 433 | 'get_readonly_fields': (), 434 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 435 | 'has_view_permission': True, 436 | 'has_change_permission': { 437 | 'default': True, 438 | 'change_only': True, 439 | }, 440 | } 441 | ), 442 | 443 | # Change objects 444 | # TODO: exam why this happening, we expect all the fields 445 | GeneralParams( 446 | name='change_from_a_simple_user_without_permissions', 447 | request_user=RequestUser('user_without_permissions', 'change'), 448 | obj_func=_obj_simple, 449 | obj_params={}, 450 | modeladmin_func=_modeladmin_simple, 451 | result={ 452 | 'get_readonly_fields': (), 453 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 454 | 'has_view_permission': False, 455 | 'has_change_permission': { 456 | 'default': False, 457 | 'change_only': False, 458 | }, 459 | } 460 | ), 461 | # TODO: exam why this happening, we expect all the fields 462 | GeneralParams( 463 | name='change_from_a_simple_user_with_add_permission', 464 | request_user=RequestUser('user_with_a_perm_on_model1', 'change'), 465 | obj_func=_obj_simple, 466 | obj_params={}, 467 | modeladmin_func=_modeladmin_simple, 468 | result={ 469 | 'get_readonly_fields': (), 470 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 471 | 'has_view_permission': False, 472 | 'has_change_permission': { 473 | 'default': False, 474 | 'change_only': False, 475 | }, 476 | } 477 | ), 478 | GeneralParams( 479 | name='change_from_a_simple_user_with_view_permission', 480 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 481 | obj_func=_obj_simple, 482 | obj_params={}, 483 | modeladmin_func=_modeladmin_simple, 484 | result={ 485 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 486 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 487 | 'has_view_permission': True, 488 | 'has_change_permission': { 489 | 'default': True, 490 | 'change_only': False, 491 | }, 492 | } 493 | ), 494 | GeneralParams( 495 | name='change_from_a_simple_user_with_view_permission_and_id_on' 496 | '_fields', 497 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 498 | obj_func=_obj_simple, 499 | obj_params={}, 500 | modeladmin_func=_modeladmin_with_id_on_fields, 501 | result={ 502 | 'get_readonly_fields': ('id',), 503 | 'get_fields': ['id'], 504 | 'has_view_permission': True, 505 | 'has_change_permission': { 506 | 'default': True, 507 | 'change_only': False, 508 | }, 509 | } 510 | ), 511 | # View permission only, var5 is a non-editable field and var6 is a 512 | # propery field. Get_readonly_fields will return this field but the 513 | # change_view will raise FieldError on change_permission. This is 514 | # normal because the default modeladmin requires those field to be on 515 | # the readonly_fields option 516 | GeneralParams( 517 | name='change_from_a_simple_user_with_view_permission_and_property' 518 | '_on_fields', 519 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 520 | obj_func=_obj_simple, 521 | obj_params={}, 522 | modeladmin_func=_modeladmin_with_property_on_fields, 523 | result={ 524 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4', 'var5', 525 | 'var6'), 526 | 'get_fields': ['var1', 'var2', 'var3', 'var4', 'var5', 'var6'], 527 | 'has_view_permission': True, 528 | 'has_change_permission': { 529 | 'default': True, 530 | 'change_only': False, 531 | }, 532 | } 533 | ), 534 | GeneralParams( 535 | name='change_from_a_simple_user_with_view_permission_and_tuple_as' 536 | 'fields', 537 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 538 | obj_func=_obj_simple, 539 | obj_params={}, 540 | modeladmin_func=_modeladmin_with_tuple_as_fields, 541 | result={ 542 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4', 'var5', 543 | 'var6'), 544 | 'get_fields': ['var1', 'var2', 'var3', 'var4', 'var5', 'var6'], 545 | 'has_view_permission': True, 546 | 'has_change_permission': { 547 | 'default': True, 548 | 'change_only': False, 549 | }, 550 | } 551 | ), 552 | GeneralParams( 553 | name='change_from_a_simple_user_with_view_permission_and_func_on' 554 | '_fields', 555 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 556 | obj_func=_obj_simple, 557 | obj_params={}, 558 | modeladmin_func=_modeladmin_with_func_on_fields, 559 | result={ 560 | 'get_readonly_fields': ('var1', 'func'), 561 | 'get_fields': ['var1', 'func'], 562 | 'has_view_permission': True, 563 | 'has_change_permission': { 564 | 'default': True, 565 | 'change_only': False, 566 | }, 567 | } 568 | ), 569 | GeneralParams( 570 | name='change_from_a_simple_user_with_view_permission_and_exclude' 571 | '_fields', 572 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 573 | obj_func=_obj_simple, 574 | obj_params={}, 575 | modeladmin_func=_modeladmin_with_exclude_fields, 576 | result={ 577 | 'get_readonly_fields': ('var2', 'var3', 'var4'), 578 | 'get_fields': ['var2', 'var3', 'var4'], 579 | 'has_view_permission': True, 580 | 'has_change_permission': { 581 | 'default': True, 582 | 'change_only': False, 583 | }, 584 | } 585 | ), 586 | GeneralParams( 587 | name='change_from_a_simple_user_with_view_permission_and_custom_' 588 | 'form_with_exclude', 589 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 590 | obj_func=_obj_simple, 591 | obj_params={}, 592 | modeladmin_func=_modeladmin_with_form_containing_exclude_fields, 593 | result={ 594 | 'get_readonly_fields': ('var2', 'var3', 'var4'), 595 | 'get_fields': ['var2', 'var3', 'var4'], 596 | 'has_view_permission': True, 597 | 'has_change_permission': { 598 | 'default': True, 599 | 'change_only': False, 600 | }, 601 | } 602 | ), 603 | GeneralParams( 604 | name='change_from_a_simple_user_with_change_permission', 605 | request_user=RequestUser('user_with_c_perm_on_model1', 'change'), 606 | obj_func=_obj_simple, 607 | obj_params={}, 608 | modeladmin_func=_modeladmin_simple, 609 | result={ 610 | 'get_readonly_fields': (), 611 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 612 | 'has_view_permission': False, 613 | 'has_change_permission': { 614 | 'default': True, 615 | 'change_only': True, 616 | }, 617 | } 618 | ), 619 | # TODO: exam why this happening, we expect all the fields 620 | GeneralParams( 621 | name='change_from_a_simple_user_with_delete_permission', 622 | request_user=RequestUser('user_with_d_perm_on_model1', 'change'), 623 | obj_func=_obj_simple, 624 | obj_params={}, 625 | modeladmin_func=_modeladmin_simple, 626 | result={ 627 | 'get_readonly_fields': (), 628 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 629 | 'has_view_permission': False, 630 | 'has_change_permission': { 631 | 'default': False, 632 | 'change_only': False, 633 | }, 634 | } 635 | ), 636 | GeneralParams( 637 | name='change_from_a_simple_user_with_add_view_permission', 638 | request_user=RequestUser('user_with_av_perm_on_model1', 'change'), 639 | obj_func=_obj_simple, 640 | obj_params={}, 641 | modeladmin_func=_modeladmin_simple, 642 | result={ 643 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 644 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 645 | 'has_view_permission': True, 646 | 'has_change_permission': { 647 | 'default': True, 648 | 'change_only': False, 649 | }, 650 | } 651 | ), 652 | GeneralParams( 653 | name='change_from_a_simple_user_with_change_view_permission', 654 | request_user=RequestUser('user_with_cv_perm_on_model1', 'change'), 655 | obj_func=_obj_simple, 656 | obj_params={}, 657 | modeladmin_func=_modeladmin_simple, 658 | result={ 659 | 'get_readonly_fields': (), 660 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 661 | 'has_view_permission': True, 662 | 'has_change_permission': { 663 | 'default': True, 664 | 'change_only': True, 665 | }, 666 | } 667 | ), 668 | GeneralParams( 669 | name='change_from_a_simple_user_with_delete_view_permission', 670 | request_user=RequestUser('user_with_dv_perm_on_model1', 'change'), 671 | obj_func=_obj_simple, 672 | obj_params={}, 673 | modeladmin_func=_modeladmin_simple, 674 | result={ 675 | 'get_readonly_fields': ('var1', 'var2', 'var3', 'var4'), 676 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 677 | 'has_view_permission': True, 678 | 'has_change_permission': { 679 | 'default': True, 680 | 'change_only': False, 681 | }, 682 | } 683 | ), 684 | # TODO: exam why actions return something. We expect to return None 685 | GeneralParams( 686 | name='change_from_a_simple_user_with_add_view_change_permission', 687 | request_user=RequestUser('user_with_avc_perm_on_model1', 'change'), 688 | obj_func=_obj_simple, 689 | obj_params={}, 690 | modeladmin_func=_modeladmin_simple, 691 | result={ 692 | 'get_readonly_fields': (), 693 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 694 | 'has_view_permission': True, 695 | 'has_change_permission': { 696 | 'default': True, 697 | 'change_only': True, 698 | }, 699 | } 700 | ), 701 | GeneralParams( 702 | name='change_from_a_simple_user_with_all_permissions', 703 | request_user=RequestUser( 704 | 'user_with_avcd_perm_on_model1', 'change'), 705 | obj_func=_obj_simple, 706 | obj_params={}, 707 | modeladmin_func=_modeladmin_simple, 708 | result={ 709 | 'get_readonly_fields': (), 710 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 711 | 'has_view_permission': True, 712 | 'has_change_permission': { 713 | 'default': True, 714 | 'change_only': True, 715 | }, 716 | } 717 | ), 718 | GeneralParams( 719 | name='change_from_a_super_user', 720 | request_user=RequestUser('super_user', 'change'), 721 | obj_func=_obj_simple, 722 | obj_params={}, 723 | modeladmin_func=_modeladmin_simple, 724 | result={ 725 | 'get_readonly_fields': (), 726 | 'get_fields': ['var1', 'var2', 'var3', 'var4'], 727 | 'has_view_permission': True, 728 | 'has_change_permission': { 729 | 'default': True, 730 | 'change_only': True, 731 | }, 732 | } 733 | ), 734 | ] 735 | 736 | @parameterized.expand(general_params) 737 | def test_get_readonly_fields(self, name, request_user, obj_func, 738 | obj_params, modeladmin_func, result): 739 | modeladmin = modeladmin_func(self) 740 | obj = obj_func(self, obj_params) if obj_func else None 741 | url_args = (obj.pk,) if obj else () 742 | url = reverse( 743 | 'test_admin:test_app_testmodel1_%s' % request_user.view, 744 | args=url_args, 745 | urlconf=create_urlconf(self.admin_site), 746 | ) 747 | request = self.factory.get(url) 748 | request.user = getattr(self, request_user.user) 749 | readonly_fields = modeladmin.get_readonly_fields(request, obj) 750 | 751 | assert readonly_fields == result['get_readonly_fields'] 752 | 753 | def test_get_readonly_fields__add_with_custom_id(self): 754 | """ 755 | Normally the user with view permisssion will get a PermissionDenied 756 | from the add_view. The function must return all the fields as 757 | readonly 758 | """ 759 | modeladmin = ModelAdmin1(TestModel5, self.admin_site) 760 | self.admin_site.register(TestModel5, ModelAdmin1) 761 | url = reverse( 762 | 'test_admin:test_app_testmodel5_add', 763 | urlconf=create_urlconf(self.admin_site), 764 | ) 765 | request = self.factory.get(url) 766 | request.user = self.user_with_v_perm_on_model5 767 | readonly_fields = modeladmin.get_readonly_fields(request) 768 | 769 | assert readonly_fields == ('var0', 'var1', 'var2', 'var3', 'var4') 770 | 771 | def test_get_readonly_fields__change_with_custom_id(self): 772 | modeladmin = ModelAdmin1(TestModel5, self.admin_site) 773 | self.admin_site.register(TestModel5, ModelAdmin1) 774 | obj = TestModel5() 775 | url = reverse( 776 | 'test_admin:test_app_testmodel5_change', 777 | args=(1, ), 778 | urlconf=create_urlconf(self.admin_site), 779 | ) 780 | request = self.factory.get(url) 781 | request.user = self.user_with_v_perm_on_model5 782 | readonly_fields = modeladmin.get_readonly_fields(request, obj) 783 | 784 | assert readonly_fields == ('var0', 'var1', 'var2', 'var3', 'var4') 785 | 786 | @parameterized.expand(general_params) 787 | def test_get_fields(self, name, request_user, obj_func, obj_params, 788 | modeladmin_func, result): 789 | modeladmin = modeladmin_func(self) 790 | obj = obj_func(self, obj_params) if obj_func else None 791 | url_args = (obj.pk,) if obj else () 792 | url = reverse( 793 | 'admin:test_app_testmodel1_%s' % request_user.view, 794 | args=url_args, 795 | urlconf=create_urlconf(self.admin_site), 796 | ) 797 | request = self.factory.get(url) 798 | request.user = getattr(self, request_user.user) 799 | readonly_fields = modeladmin.get_fields(request, obj) 800 | 801 | assert readonly_fields == result['get_fields'] 802 | 803 | @parameterized.expand(general_params) 804 | def test_has_view_permission(self, name, request_user, obj_func, 805 | obj_params, modeladmin_func, result): 806 | modeladmin = modeladmin_func(self) 807 | obj = obj_func(self, obj_params) if obj_func else None 808 | url_args = (obj.pk,) if obj else () 809 | url = reverse( 810 | 'test_admin:test_app_testmodel1_%s' % request_user.view, 811 | args=url_args, 812 | urlconf=create_urlconf(self.admin_site), 813 | ) 814 | request = self.factory.get(url) 815 | request.user = getattr(self, request_user.user) 816 | has_view_permission = modeladmin.has_view_permission(request) 817 | 818 | assert has_view_permission == result['has_view_permission'] 819 | 820 | @parameterized.expand(general_params) 821 | def test_has_change_permission(self, name, request_user, obj_func, 822 | obj_params, modeladmin_func, result): 823 | modeladmin = modeladmin_func(self) 824 | obj = obj_func(self, obj_params) if obj_func else None 825 | url_args = (obj.pk,) if obj else () 826 | url = reverse( 827 | 'test_admin:test_app_testmodel1_%s' % request_user.view, 828 | args=url_args, 829 | urlconf=create_urlconf(self.admin_site) 830 | ) 831 | request = self.factory.get(url) 832 | request.user = getattr(self, request_user.user) 833 | has_change_permission_default = modeladmin.has_change_permission( 834 | request, obj=obj) 835 | has_change_permission_only = \ 836 | modeladmin._has_change_only_permission(request, obj=obj) 837 | 838 | assert (has_change_permission_default == 839 | result['has_change_permission']['default']) 840 | assert (has_change_permission_only == 841 | result['has_change_permission']['change_only']) 842 | 843 | ActionParams = namedtuple( 844 | 'ActionParam', 'name, request_user, modeladmin_func, result') 845 | 846 | action_params = [ 847 | ActionParams( 848 | name='simple_user_without_permissions', 849 | request_user=RequestUser('user_without_permissions', 'add'), 850 | modeladmin_func=_modeladmin_simple, 851 | result={'get_actions': lambda x: len(x) == 0}, 852 | ), 853 | ActionParams( 854 | name='add_from_a_simple_user_with_add_permission', 855 | request_user=RequestUser('user_with_a_perm_on_model1', 'add'), 856 | modeladmin_func=_modeladmin_simple, 857 | result={'get_actions': lambda x: len(x) == 0}, 858 | ), 859 | ActionParams( 860 | name='add_from_a_simple_user_with_view_permission', 861 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 862 | modeladmin_func=_modeladmin_simple, 863 | result={'get_actions': lambda x: x == OrderedDict()}, 864 | ), 865 | ActionParams( 866 | name='add_from_a_simple_user_with_change_permission', 867 | request_user=RequestUser('user_with_c_perm_on_model1', 'add'), 868 | modeladmin_func=_modeladmin_simple, 869 | result={'get_actions': lambda x: len(x) == 0}, 870 | ), 871 | ActionParams( 872 | name='add_from_a_simple_user_with_delete_permission', 873 | request_user=RequestUser('user_with_d_perm_on_model1', 'add'), 874 | modeladmin_func=_modeladmin_simple, 875 | result={'get_actions': lambda x: len(x) == 1}, 876 | ), 877 | ActionParams( 878 | name='add_from_a_simple_user_with_add_view_permission', 879 | request_user=RequestUser('user_with_av_perm_on_model1', 'add'), 880 | modeladmin_func=_modeladmin_simple, 881 | result={'get_actions': lambda x: x == OrderedDict()}, 882 | ), 883 | ActionParams( 884 | name='add_from_a_simple_user_with_change_view_permission', 885 | request_user=RequestUser('user_with_cv_perm_on_model1', 'add'), 886 | modeladmin_func=_modeladmin_simple, 887 | result={'get_actions': lambda x: len(x) == 0}, 888 | ), 889 | ActionParams( 890 | name='add_from_a_simple_user_with_delete_view_permission', 891 | request_user=RequestUser('user_with_dv_perm_on_model1', 'add'), 892 | modeladmin_func=_modeladmin_simple, 893 | result={'get_actions': lambda x: len(x) == 1}, 894 | ), 895 | ActionParams( 896 | name='add_from_a_simple_user_with_add_view_change_permission', 897 | request_user=RequestUser('user_with_avc_perm_on_model1', 'add'), 898 | modeladmin_func=_modeladmin_simple, 899 | result={'get_actions': lambda x: len(x) == 0}, 900 | ), 901 | ActionParams( 902 | name='add_from_a_simple_user_with_all_permissions', 903 | request_user=RequestUser('user_with_avcd_perm_on_model1', 'add'), 904 | modeladmin_func=_modeladmin_simple, 905 | result={'get_actions': lambda x: len(x) == 1}, 906 | ), 907 | ActionParams( 908 | name='add_from_a_super_user', 909 | request_user=RequestUser('super_user', 'add'), 910 | modeladmin_func=_modeladmin_simple, 911 | result={'get_actions': lambda x: len(x) == 1}, 912 | ), 913 | 914 | ] 915 | 916 | @parameterized.expand(action_params) 917 | def test_get_actions(self, name, request_user, modeladmin_func, result): 918 | modeladmin = modeladmin_func(self) 919 | url = reverse( 920 | 'admin:test_app_testmodel1_%s' % request_user.view, 921 | urlconf=create_urlconf(self.admin_site), 922 | ) 923 | request = self.factory.get(url) 924 | request.user = getattr(self, request_user.user) 925 | actions = modeladmin.get_actions(request) 926 | 927 | assert result['get_actions'](actions) 928 | 929 | 930 | class TestAdminViewPermissionModelAdmin(DataMixin, TestCase): 931 | 932 | @classmethod 933 | def setUpTestData(cls): 934 | super(TestAdminViewPermissionModelAdmin, cls).setUpTestData() 935 | 936 | # Users 937 | cls.user_without_permissions = create_simple_user() 938 | 939 | cls.user_with_a_perm = create_simple_user() 940 | cls.user_with_a_perm.user_permissions.add( 941 | cls.add_permission_model1, 942 | ) 943 | 944 | cls.user_with_v_perm_on_model1 = create_simple_user() 945 | cls.user_with_v_perm_on_model1.user_permissions.add( 946 | cls.view_permission_model1, 947 | ) 948 | 949 | cls.user_with_av_perm_on_model1 = create_simple_user() 950 | cls.user_with_av_perm_on_model1.user_permissions.add( 951 | cls.add_permission_model1, 952 | cls.view_permission_model1) 953 | 954 | cls.user_with_cv_perm_on_model1 = create_simple_user() 955 | cls.user_with_cv_perm_on_model1.user_permissions.add( 956 | cls.change_permission_model1, 957 | cls.view_permission_model1) 958 | 959 | cls.user_with_dv_perm_on_model1 = create_simple_user() 960 | cls.user_with_dv_perm_on_model1.user_permissions.add( 961 | cls.delete_permission_model1, 962 | cls.view_permission_model1) 963 | 964 | cls.user_with_v_perm_on_model1_4 = create_simple_user() 965 | cls.user_with_v_perm_on_model1_4.user_permissions.add( 966 | cls.view_permission_model1, 967 | cls.view_permission_model4, 968 | ) 969 | 970 | cls.user_with_av_perm_on_model1_4 = create_simple_user() 971 | cls.user_with_av_perm_on_model1_4.user_permissions.add( 972 | cls.view_permission_model1, 973 | cls.add_permission_model4, 974 | ) 975 | 976 | cls.user_with_cv_perm_on_model1_4 = create_simple_user() 977 | cls.user_with_cv_perm_on_model1_4.user_permissions.add( 978 | cls.view_permission_model1, 979 | cls.change_permission_model4, 980 | ) 981 | 982 | cls.user_with_dv_perm_on_model1_4 = create_simple_user() 983 | cls.user_with_dv_perm_on_model1_4.user_permissions.add( 984 | cls.view_permission_model1, 985 | cls.delete_permission_model4, 986 | ) 987 | 988 | cls.user_with_vcd_perm_on_model1_4 = create_simple_user() 989 | cls.user_with_vcd_perm_on_model1_4.user_permissions.add( 990 | cls.view_permission_model1, 991 | cls.change_permission_model4, 992 | cls.delete_permission_model4, 993 | ) 994 | 995 | cls.user_with_v_perm_on_model1_4_6 = create_simple_user() 996 | cls.user_with_v_perm_on_model1_4_6.user_permissions.add( 997 | cls.view_permission_model1, 998 | cls.view_permission_model4, 999 | cls.view_permission_model6, 1000 | ) 1001 | 1002 | cls.user_with_cv_perm_on_model1_4_6 = create_simple_user() 1003 | cls.user_with_cv_perm_on_model1_4_6.user_permissions.add( 1004 | cls.view_permission_model1, 1005 | cls.view_permission_model4, 1006 | cls.change_permission_model4, 1007 | cls.view_permission_model6, 1008 | cls.change_permission_model6, 1009 | ) 1010 | 1011 | cls.user_with_av_perm_on_model1_4_6 = create_simple_user() 1012 | cls.user_with_av_perm_on_model1_4_6.user_permissions.add( 1013 | cls.view_permission_model1, 1014 | cls.view_permission_model4, 1015 | cls.add_permission_model4, 1016 | cls.view_permission_model6, 1017 | cls.add_permission_model6, 1018 | ) 1019 | 1020 | cls.super_user = create_super_user() 1021 | 1022 | @classmethod 1023 | def setUpClass(cls): 1024 | super(TestAdminViewPermissionModelAdmin, cls).setUpClass() 1025 | cls.factory = RequestFactory() 1026 | 1027 | def setUp(self): 1028 | self.admin_site = AdminSite('test_admin') 1029 | 1030 | RequestUser = namedtuple('RequestUser', 'user, view') 1031 | 1032 | # Modeladmin 1033 | 1034 | def _modeladmin_simple(self): 1035 | self.admin_site.register(TestModel1, ModelAdmin1) 1036 | return ModelAdmin1(TestModel1, self.admin_site) 1037 | 1038 | def _modeladmin_with_id_on_fields(self): 1039 | self.admin_site.register(TestModel1, ModelAdmin1) 1040 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 1041 | modeladmin.fields = ['id'] 1042 | modeladmin.readonly_fields = ('id',) 1043 | 1044 | return modeladmin 1045 | 1046 | def _modeladmin_with_property_on_fields(self): 1047 | self.admin_site.register(TestModel1, ModelAdmin1) 1048 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 1049 | modeladmin.fields = ['var1', 'var2', 'var3', 'var4', 'var5', 'var6'] 1050 | 1051 | return modeladmin 1052 | 1053 | def _modeladmin_with_list_editable(self): 1054 | self.admin_site.register(TestModel1, ModelAdmin1) 1055 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 1056 | modeladmin.list_display = ['var1', 'var2'] 1057 | modeladmin.list_editable = ['var2'] 1058 | 1059 | return modeladmin 1060 | 1061 | def _modeladmin_with_func_on_fields(self): 1062 | self.admin_site.register(TestModel1, ModelAdmin1) 1063 | modeladmin = ModelAdmin1(TestModel1, self.admin_site) 1064 | modeladmin.fields = ['var1', 'func'] 1065 | 1066 | return modeladmin 1067 | 1068 | def _modeladmin_with_custom_id(self): 1069 | return ModelAdmin1(TestModel5, self.admin_site) 1070 | 1071 | # Objects 1072 | 1073 | def _obj_simple(self, obj_params): 1074 | return mommy.make('test_app.TestModel1', **obj_params) 1075 | 1076 | GeneralParams = namedtuple( 1077 | 'GeneralParams', 'name, request_user, obj_func, obj_params, ' 1078 | 'modeladmin_func, result') 1079 | 1080 | general_params = [ 1081 | # Add objects 1082 | GeneralParams( 1083 | name='add_from_a_user_with_view_permission_on_testmodel1', 1084 | request_user=RequestUser('user_with_v_perm_on_model1', 'add'), 1085 | obj_func=None, 1086 | obj_params={}, 1087 | modeladmin_func=_modeladmin_simple, 1088 | result={ 1089 | 'get_inline_instances': {'count': 0, 'inlines': None}, 1090 | 'get_model_perms': { 1091 | 'add': False, 1092 | 'change': True, 1093 | 'delete': False, 1094 | 'view': True, 1095 | }, 1096 | 'change_view': None, 1097 | } 1098 | ), 1099 | GeneralParams( 1100 | name='add_from_a_user_with_view_permission_on_testmodel1_4', 1101 | request_user=RequestUser('user_with_v_perm_on_model1_4', 'add'), 1102 | obj_func=None, 1103 | obj_params={}, 1104 | modeladmin_func=_modeladmin_simple, 1105 | result={ 1106 | 'get_inline_instances': { 1107 | 'count': 1, 1108 | 'inlines': [ 1109 | {'can_delete': False, 'max_num': 0, 1110 | 'class': AdminViewPermissionInlineModelAdmin} 1111 | ] 1112 | }, 1113 | 'get_model_perms': { 1114 | 'add': False, 1115 | 'change': True, 1116 | 'delete': False, 1117 | 'view': True, 1118 | }, 1119 | 'change_view': None, 1120 | } 1121 | ), 1122 | GeneralParams( 1123 | name='add_from_a_user_with_view_permission_on_testmodel1_4_6', 1124 | request_user=RequestUser('user_with_v_perm_on_model1_4_6', 'add'), 1125 | obj_func=None, 1126 | obj_params={}, 1127 | modeladmin_func=_modeladmin_simple, 1128 | result={ 1129 | 'get_inline_instances': { 1130 | 'count': 2, 1131 | 'inlines': [ 1132 | {'can_delete': False, 'max_num': 0, 1133 | 'class': AdminViewPermissionInlineModelAdmin}, 1134 | {'can_delete': False, 'max_num': 0, 1135 | 'class': AdminViewPermissionInlineModelAdmin}, 1136 | ] 1137 | }, 1138 | 'get_model_perms': { 1139 | 'add': False, 1140 | 'change': True, 1141 | 'delete': False, 1142 | 'view': True, 1143 | }, 1144 | 'change_view': None, 1145 | } 1146 | ), 1147 | GeneralParams( 1148 | name='add_from_a_user_with_change_view_permission_on_' 1149 | 'testmodel1_4_6', 1150 | request_user=RequestUser('user_with_cv_perm_on_model1_4_6', 'add'), 1151 | obj_func=None, 1152 | obj_params={}, 1153 | modeladmin_func=_modeladmin_simple, 1154 | result={ 1155 | 'get_inline_instances': { 1156 | 'count': 2, 1157 | 'inlines': [ 1158 | {'can_delete': True, 'max_num': 0, 1159 | 'class': AdminViewPermissionInlineModelAdmin}, 1160 | {'can_delete': True, 'max_num': 0, 1161 | 'class': AdminViewPermissionInlineModelAdmin}, 1162 | ] 1163 | }, 1164 | 'get_model_perms': { 1165 | 'add': False, 1166 | 'change': True, 1167 | 'delete': False, 1168 | 'view': True, 1169 | }, 1170 | 'change_view': None, 1171 | } 1172 | ), 1173 | GeneralParams( 1174 | name='add_from_a_user_with_add_view_permission_on_' 1175 | 'testmodel1_4_6', 1176 | request_user=RequestUser('user_with_av_perm_on_model1_4_6', 'add'), 1177 | obj_func=None, 1178 | obj_params={}, 1179 | modeladmin_func=_modeladmin_simple, 1180 | result={ 1181 | 'get_inline_instances': { 1182 | 'count': 2, 1183 | 'inlines': [ 1184 | {'can_delete': False, 'max_num': None, 1185 | 'class': AdminViewPermissionInlineModelAdmin}, 1186 | {'can_delete': False, 'max_num': None, 1187 | 'class': AdminViewPermissionInlineModelAdmin}, 1188 | ] 1189 | }, 1190 | 'get_model_perms': { 1191 | 'add': False, 1192 | 'change': True, 1193 | 'delete': False, 1194 | 'view': True, 1195 | }, 1196 | 'change_view': None, 1197 | } 1198 | ), 1199 | GeneralParams( 1200 | name='add_from_a_super_user', 1201 | request_user=RequestUser('super_user', 'add'), 1202 | obj_func=None, 1203 | obj_params={}, 1204 | modeladmin_func=_modeladmin_simple, 1205 | result={ 1206 | 'get_inline_instances': { 1207 | 'count': 2, 1208 | 'inlines': [ 1209 | {'can_delete': True, 'max_num': None, 1210 | 'class': AdminViewPermissionInlineModelAdmin}, 1211 | {'can_delete': True, 'max_num': None, 1212 | 'class': AdminViewPermissionInlineModelAdmin}, 1213 | ] 1214 | }, 1215 | 'get_model_perms': { 1216 | 'add': True, 1217 | 'change': True, 1218 | 'delete': True, 1219 | 'view': True, 1220 | }, 1221 | 'change_view': None, 1222 | } 1223 | ), 1224 | 1225 | # Change objects 1226 | GeneralParams( 1227 | name='change_from_a_user_with_view_permission_on_testmodel1', 1228 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 1229 | obj_func=_obj_simple, 1230 | obj_params={}, 1231 | modeladmin_func=_modeladmin_simple, 1232 | result={ 1233 | 'get_inline_instances': {'count': 0, 'inlines': None}, 1234 | 'get_model_perms': { 1235 | 'add': False, 1236 | 'change': True, 1237 | 'delete': False, 1238 | 'view': True, 1239 | }, 1240 | 'change_view': { 1241 | 'status_code': 200, 1242 | 'context_data': { 1243 | 'title': 'View test model1', 1244 | 'show_save': False, 1245 | 'show_save_and_continue': False, 1246 | 'show_save_and_add_another': False, 1247 | 'show_save_as_new': False, 1248 | } 1249 | }, 1250 | } 1251 | ), 1252 | GeneralParams( 1253 | name='change_from_a_simple_user_with_view_permission_and_property' 1254 | '_on_fields', 1255 | request_user=RequestUser('user_with_v_perm_on_model1', 'change'), 1256 | obj_func=_obj_simple, 1257 | obj_params={}, 1258 | modeladmin_func=_modeladmin_with_property_on_fields, 1259 | result={ 1260 | 'get_inline_instances': {'count': 0, 'inlines': None}, 1261 | 'get_model_perms': { 1262 | 'add': False, 1263 | 'change': True, 1264 | 'delete': False, 1265 | 'view': True, 1266 | }, 1267 | 'change_view': { 1268 | 'status_code': 200, 1269 | 'context_data': { 1270 | 'title': 'View test model1', 1271 | 'show_save': False, 1272 | 'show_save_and_continue': False, 1273 | 'show_save_and_add_another': False, 1274 | 'show_save_as_new': False, 1275 | } 1276 | }, 1277 | } 1278 | ), 1279 | GeneralParams( 1280 | name='change_from_a_user_with_view_permission_on_testmodel1_4', 1281 | request_user=RequestUser('user_with_v_perm_on_model1_4', 'change'), 1282 | obj_func=_obj_simple, 1283 | obj_params={}, 1284 | modeladmin_func=_modeladmin_simple, 1285 | result={ 1286 | 'get_inline_instances': { 1287 | 'count': 1, 1288 | 'inlines': [ 1289 | {'can_delete': False, 'max_num': 0, 1290 | 'class': AdminViewPermissionInlineModelAdmin} 1291 | ] 1292 | }, 1293 | 'get_model_perms': { 1294 | 'add': False, 1295 | 'change': True, 1296 | 'delete': False, 1297 | 'view': True, 1298 | }, 1299 | 'change_view': { 1300 | 'status_code': 200, 1301 | 'context_data': { 1302 | 'title': 'View test model1', 1303 | 'show_save': False, 1304 | 'show_save_and_continue': False, 1305 | 'show_save_and_add_another': False, 1306 | 'show_save_as_new': False, 1307 | } 1308 | }, 1309 | } 1310 | ), 1311 | GeneralParams( 1312 | name='change_from_a_user_with_add_view_permission_on_' 1313 | 'testmodel1_4', 1314 | request_user=RequestUser( 1315 | 'user_with_av_perm_on_model1_4', 'change'), 1316 | obj_func=_obj_simple, 1317 | obj_params={}, 1318 | modeladmin_func=_modeladmin_simple, 1319 | result={ 1320 | 'get_inline_instances': { 1321 | 'count': 1, 1322 | 'inlines': [ 1323 | {'can_delete': True, 'max_num': None, 1324 | 'class': AdminViewPermissionInlineModelAdmin} 1325 | ] 1326 | }, 1327 | 'get_model_perms': { 1328 | 'add': False, 1329 | 'change': True, 1330 | 'delete': False, 1331 | 'view': True, 1332 | }, 1333 | 'change_view': { 1334 | 'status_code': 200, 1335 | 'context_data': { 1336 | 'title': 'View test model1', 1337 | 'show_save': True, 1338 | 'show_save_and_continue': True, 1339 | 'show_save_and_add_another': False, 1340 | 'show_save_as_new': False, 1341 | } 1342 | }, 1343 | } 1344 | ), 1345 | GeneralParams( 1346 | name='change_from_a_user_with_change_view_permission_on_' 1347 | 'testmodel1_4', 1348 | request_user=RequestUser( 1349 | 'user_with_cv_perm_on_model1_4', 'change'), 1350 | obj_func=_obj_simple, 1351 | obj_params={}, 1352 | modeladmin_func=_modeladmin_simple, 1353 | result={ 1354 | 'get_inline_instances': { 1355 | 'count': 1, 1356 | 'inlines': [ 1357 | {'can_delete': True, 'max_num': 0, 1358 | 'class': AdminViewPermissionInlineModelAdmin} 1359 | ] 1360 | }, 1361 | 'get_model_perms': { 1362 | 'add': False, 1363 | 'change': True, 1364 | 'delete': False, 1365 | 'view': True, 1366 | }, 1367 | 'change_view': { 1368 | 'status_code': 200, 1369 | 'context_data': { 1370 | 'title': 'View test model1', 1371 | 'show_save': True, 1372 | 'show_save_and_continue': True, 1373 | 'show_save_and_add_another': False, 1374 | 'show_save_as_new': False, 1375 | } 1376 | }, 1377 | } 1378 | ), 1379 | # Here it's bit weird, but by default django needs change and delete 1380 | # permission in order to be able to delete an inline, see next test. 1381 | GeneralParams( 1382 | name='change_from_a_user_with_delete_view_permission_on_' 1383 | 'testmodel1_4', 1384 | request_user=RequestUser( 1385 | 'user_with_dv_perm_on_model1_4', 'change'), 1386 | obj_func=_obj_simple, 1387 | obj_params={}, 1388 | modeladmin_func=_modeladmin_simple, 1389 | result={ 1390 | 'get_inline_instances': { 1391 | 'count': 1, 1392 | 'inlines': [ 1393 | {'can_delete': True, 'max_num': 0, 1394 | 'class': AdminViewPermissionInlineModelAdmin} 1395 | ] 1396 | }, 1397 | 'get_model_perms': { 1398 | 'add': False, 1399 | 'change': True, 1400 | 'delete': False, 1401 | 'view': True, 1402 | }, 1403 | 'change_view': { 1404 | 'status_code': 200, 1405 | 'context_data': { 1406 | 'title': 'View test model1', 1407 | 'show_save': False, 1408 | 'show_save_and_continue': False, 1409 | 'show_save_and_add_another': False, 1410 | 'show_save_as_new': False, 1411 | } 1412 | }, 1413 | } 1414 | ), 1415 | GeneralParams( 1416 | name='change_from_a_user_with_view_change_delete_permission_on_' 1417 | 'testmodel1_4', 1418 | request_user=RequestUser( 1419 | 'user_with_vcd_perm_on_model1_4', 'change'), 1420 | obj_func=_obj_simple, 1421 | obj_params={}, 1422 | modeladmin_func=_modeladmin_simple, 1423 | result={ 1424 | 'get_inline_instances': { 1425 | 'count': 1, 1426 | 'inlines': [ 1427 | {'can_delete': True, 'max_num': 0, 1428 | 'class': AdminViewPermissionInlineModelAdmin} 1429 | ] 1430 | }, 1431 | 'get_model_perms': { 1432 | 'add': False, 1433 | 'change': True, 1434 | 'delete': False, 1435 | 'view': True, 1436 | }, 1437 | 'change_view': { 1438 | 'status_code': 200, 1439 | 'context_data': { 1440 | 'title': 'View test model1', 1441 | 'show_save': True, 1442 | 'show_save_and_continue': True, 1443 | 'show_save_and_add_another': False, 1444 | 'show_save_as_new': False, 1445 | } 1446 | }, 1447 | } 1448 | ), 1449 | GeneralParams( 1450 | name='change_from_a_user_with_view_permission_on_testmodel1_4_6', 1451 | request_user=RequestUser( 1452 | 'user_with_v_perm_on_model1_4_6', 'change'), 1453 | obj_func=_obj_simple, 1454 | obj_params={}, 1455 | modeladmin_func=_modeladmin_simple, 1456 | result={ 1457 | 'get_inline_instances': { 1458 | 'count': 2, 1459 | 'inlines': [ 1460 | {'can_delete': False, 'max_num': 0, 1461 | 'class': AdminViewPermissionInlineModelAdmin}, 1462 | {'can_delete': False, 'max_num': 0, 1463 | 'class': AdminViewPermissionInlineModelAdmin}, 1464 | ] 1465 | }, 1466 | 'get_model_perms': { 1467 | 'add': False, 1468 | 'change': True, 1469 | 'delete': False, 1470 | 'view': True, 1471 | }, 1472 | 'change_view': { 1473 | 'status_code': 200, 1474 | 'context_data': { 1475 | 'title': 'View test model1', 1476 | 'show_save': False, 1477 | 'show_save_and_continue': False, 1478 | 'show_save_and_add_another': False, 1479 | 'show_save_as_new': False, 1480 | } 1481 | }, 1482 | } 1483 | ), 1484 | GeneralParams( 1485 | name='change_from_a_user_with_change_view_permission_on_' 1486 | 'testmodel1_4_6', 1487 | request_user=RequestUser( 1488 | 'user_with_cv_perm_on_model1_4_6', 'change'), 1489 | obj_func=_obj_simple, 1490 | obj_params={}, 1491 | modeladmin_func=_modeladmin_simple, 1492 | result={ 1493 | 'get_inline_instances': { 1494 | 'count': 2, 1495 | 'inlines': [ 1496 | {'can_delete': True, 'max_num': 0, 1497 | 'class': AdminViewPermissionInlineModelAdmin}, 1498 | {'can_delete': True, 'max_num': 0, 1499 | 'class': AdminViewPermissionInlineModelAdmin}, 1500 | ] 1501 | }, 1502 | 'get_model_perms': { 1503 | 'add': False, 1504 | 'change': True, 1505 | 'delete': False, 1506 | 'view': True, 1507 | }, 1508 | 'change_view': { 1509 | 'status_code': 200, 1510 | 'context_data': { 1511 | 'title': 'View test model1', 1512 | 'show_save': True, 1513 | 'show_save_and_continue': True, 1514 | 'show_save_and_add_another': False, 1515 | 'show_save_as_new': False, 1516 | } 1517 | }, 1518 | } 1519 | ), 1520 | GeneralParams( 1521 | name='change_from_a_user_with_add_view_permission_on_' 1522 | 'testmodel1_4_6', 1523 | request_user=RequestUser( 1524 | 'user_with_av_perm_on_model1_4_6', 'change'), 1525 | obj_func=_obj_simple, 1526 | obj_params={}, 1527 | modeladmin_func=_modeladmin_simple, 1528 | result={ 1529 | 'get_inline_instances': { 1530 | 'count': 2, 1531 | 'inlines': [ 1532 | {'can_delete': False, 'max_num': None, 1533 | 'class': AdminViewPermissionInlineModelAdmin}, 1534 | {'can_delete': False, 'max_num': None, 1535 | 'class': AdminViewPermissionInlineModelAdmin}, 1536 | ] 1537 | }, 1538 | 'get_model_perms': { 1539 | 'add': False, 1540 | 'change': True, 1541 | 'delete': False, 1542 | 'view': True, 1543 | }, 1544 | 'change_view': { 1545 | 'status_code': 200, 1546 | 'context_data': { 1547 | 'title': 'View test model1', 1548 | 'show_save': True, 1549 | 'show_save_and_continue': True, 1550 | 'show_save_and_add_another': False, 1551 | 'show_save_as_new': False, 1552 | } 1553 | }, 1554 | } 1555 | ), 1556 | GeneralParams( 1557 | name='change_from_a_super_user', 1558 | request_user=RequestUser('super_user', 'change'), 1559 | obj_func=_obj_simple, 1560 | obj_params={}, 1561 | modeladmin_func=_modeladmin_simple, 1562 | result={ 1563 | 'get_inline_instances': { 1564 | 'count': 2, 1565 | 'inlines': [ 1566 | {'can_delete': True, 'max_num': None, 1567 | 'class': AdminViewPermissionInlineModelAdmin}, 1568 | {'can_delete': True, 'max_num': None, 1569 | 'class': AdminViewPermissionInlineModelAdmin}, 1570 | ] 1571 | }, 1572 | 'get_model_perms': { 1573 | 'add': True, 1574 | 'change': True, 1575 | 'delete': True, 1576 | 'view': True, 1577 | }, 1578 | 'change_view': { 1579 | 'status_code': 200, 1580 | 'context_data': { 1581 | 'title': 'Change test model1', 1582 | } 1583 | }, 1584 | } 1585 | ), 1586 | ] 1587 | 1588 | @parameterized.expand(general_params) 1589 | def test_get_inline_instances(self, name, request_user, obj_func, 1590 | obj_params, modeladmin_func, result): 1591 | modeladmin = modeladmin_func(self) 1592 | obj = obj_func(self, obj_params) if obj_func else None 1593 | url_args = (obj.pk,) if obj else () 1594 | url = reverse( 1595 | 'test_admin:test_app_testmodel1_%s' % request_user.view, 1596 | args=url_args, 1597 | urlconf=create_urlconf(self.admin_site), 1598 | ) 1599 | request = self.factory.get(url) 1600 | request.user = getattr(self, request_user.user) 1601 | inlines = modeladmin.get_inline_instances(request, obj) 1602 | 1603 | assert len(inlines) == result['get_inline_instances']['count'] 1604 | if result['get_inline_instances']: 1605 | for i, inline in enumerate(inlines): 1606 | assert (inline.can_delete == 1607 | result['get_inline_instances']['inlines'][i][ 1608 | 'can_delete']) 1609 | assert (inline.max_num == 1610 | result['get_inline_instances']['inlines'][i][ 1611 | 'max_num']) 1612 | 1613 | @parameterized.expand(general_params) 1614 | def test_get_model_perms(self, name, request_user, obj_func, 1615 | obj_params, modeladmin_func, result): 1616 | modeladmin = modeladmin_func(self) 1617 | obj = obj_func(self, obj_params) if obj_func else None 1618 | url_args = (obj.pk,) if obj else () 1619 | url = reverse( 1620 | 'test_admin:test_app_testmodel1_%s' % request_user.view, 1621 | args=url_args, 1622 | urlconf=create_urlconf(self.admin_site), 1623 | ) 1624 | request = self.factory.get(url) 1625 | request.user = getattr(self, request_user.user) 1626 | model_perms = modeladmin.get_model_perms(request) 1627 | 1628 | assert model_perms == result['get_model_perms'] 1629 | 1630 | @parameterized.expand(general_params) 1631 | def test_change_view(self, name, request_user, obj_func, obj_params, 1632 | modeladmin_func, result): 1633 | if not result['change_view']: 1634 | pytest.skip('not a case') 1635 | 1636 | context_data = result['change_view']['context_data'] 1637 | modeladmin = modeladmin_func(self) 1638 | obj = obj_func(self, obj_params) if obj_func else None 1639 | url_args = (obj.pk,) if obj else () 1640 | url = reverse( 1641 | 'test_admin:test_app_testmodel1_%s' % request_user.view, 1642 | args=url_args, 1643 | urlconf=create_urlconf(self.admin_site) 1644 | ) 1645 | 1646 | request = self.factory.get(url) 1647 | request.user = getattr(self, request_user.user) 1648 | response = modeladmin.change_view(request, str(obj.pk)) 1649 | 1650 | assert response.status_code == result['change_view']['status_code'] 1651 | assert response.context_data['title'] == context_data['title'] 1652 | if 'show_save' in response.context_data: 1653 | assert (response.context_data['show_save'] == 1654 | context_data['show_save']) 1655 | if 'show_save_and_continue' in response.context_data: 1656 | assert (response.context_data['show_save_and_continue'] == 1657 | context_data['show_save_and_continue']) 1658 | if 'show_save_and_add_another' in response.context_data: 1659 | assert (response.context_data['show_save_and_add_another'] == 1660 | context_data['show_save_and_add_another']) 1661 | if 'show_save_as_new' in response.context_data: 1662 | assert (response.context_data['show_save_as_new'] == 1663 | context_data['show_save_as_new']) 1664 | 1665 | @parameterized.expand([ 1666 | ('user_with_v_perm_on_model1', 200, lambda x: x is None), 1667 | ('super_user', 200, lambda x: x is None), 1668 | ]) 1669 | def test_changelist_view__without_list_editable(self, user, status_code, 1670 | cl_formset): 1671 | modeladmin = self._modeladmin_simple() 1672 | url = reverse( 1673 | 'test_admin:test_app_testmodel1_changelist', 1674 | urlconf=create_urlconf(self.admin_site) 1675 | ) 1676 | 1677 | request = self.factory.get(url) 1678 | request.user = getattr(self, user) 1679 | response = modeladmin.changelist_view(request) 1680 | 1681 | assert response.status_code == status_code 1682 | assert cl_formset(response.context_data['cl'].formset) 1683 | 1684 | @parameterized.expand([ 1685 | ('user_with_v_perm_on_model1', 200, lambda x: x is None), 1686 | ('user_with_av_perm_on_model1', 200, lambda x: x is None), 1687 | ('user_with_cv_perm_on_model1', 200, lambda x: x is not None), 1688 | ('user_with_dv_perm_on_model1', 200, lambda x: x is None), 1689 | ('super_user', 200, lambda x: x is not None), 1690 | ]) 1691 | def test_changelist_view__with_list_editable(self, user, status_code, 1692 | cl_formset): 1693 | modeladmin = self._modeladmin_with_list_editable() 1694 | url = reverse( 1695 | 'test_admin:test_app_testmodel1_changelist', 1696 | urlconf=create_urlconf(self.admin_site) 1697 | ) 1698 | 1699 | request = self.factory.get(url) 1700 | request.user = getattr(self, user) 1701 | response = modeladmin.changelist_view(request) 1702 | 1703 | assert response.status_code == status_code 1704 | assert cl_formset(response.context_data['cl'].formset) 1705 | 1706 | 1707 | class TestAdminViewPermissionAdminSite(SimpleTestCase): 1708 | 1709 | def setUp(self): 1710 | self.admin_site = AdminViewPermissionAdminSite('admin') 1711 | 1712 | def test_register__1(self): 1713 | self.admin_site.register(TestModel1) 1714 | assert isinstance(self.admin_site._registry[TestModel1], 1715 | AdminViewPermissionModelAdmin) 1716 | 1717 | def test_register__2(self): 1718 | modeladmin1 = type(str('TestModelAdmin1'), (admin.ModelAdmin, ), {}) 1719 | self.admin_site.register(TestModel1, modeladmin1) 1720 | assert isinstance(self.admin_site._registry[TestModel1], 1721 | AdminViewPermissionModelAdmin) 1722 | assert isinstance(self.admin_site._registry[TestModel1], 1723 | modeladmin1) 1724 | 1725 | @override_settings(ADMIN_VIEW_PERMISSION_MODELS=['test_app.TestModel1', ]) 1726 | def test_register__3(self): 1727 | self.admin_site.register(TestModel1) 1728 | assert isinstance(self.admin_site._registry[TestModel1], 1729 | AdminViewPermissionModelAdmin) 1730 | 1731 | @override_settings(ADMIN_VIEW_PERMISSION_MODELS=['test_app.TestModel1', ]) 1732 | def test_register__4(self): 1733 | modeladmin1 = type(str('TestModelAdmin1'), (admin.ModelAdmin, ), {}) 1734 | self.admin_site.register(TestModel1, modeladmin1) 1735 | assert isinstance(self.admin_site._registry[TestModel1], 1736 | AdminViewPermissionModelAdmin) 1737 | assert isinstance(self.admin_site._registry[TestModel1], 1738 | modeladmin1) 1739 | 1740 | @override_settings(ADMIN_VIEW_PERMISSION_MODELS=[]) 1741 | def test_register__5(self): 1742 | self.admin_site.register(TestModel1) 1743 | assert not isinstance(self.admin_site._registry[TestModel1], 1744 | AdminViewPermissionModelAdmin) 1745 | 1746 | @override_settings(ADMIN_VIEW_PERMISSION_MODELS=()) 1747 | def test_register__6(self): 1748 | self.admin_site.register(TestModel1) 1749 | assert not isinstance(self.admin_site._registry[TestModel1], 1750 | AdminViewPermissionModelAdmin) 1751 | -------------------------------------------------------------------------------- /tests/tests/unit/test_apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import apps 4 | from django.db import models 5 | from django.db.models.signals import post_migrate 6 | from django.test import TestCase, override_settings 7 | 8 | 9 | class TestAdminViewPermissionConfig(TestCase): 10 | 11 | def setUp(self): 12 | class Meta: 13 | permissions = ( 14 | ("copy_apptestmodel3", "Can copy apptestmodel3"), 15 | ) 16 | 17 | attrs_1 = { 18 | '__module__': 'tests.test_app.models', 19 | } 20 | attrs_2 = { 21 | '__module__': 'tests.test_app.models', 22 | 'Meta': Meta, 23 | } 24 | 25 | self.appconfig = apps.get_app_config('test_app') 26 | self.model1 = type(str('AppTestModel1'), (models.Model, ), 27 | attrs_1.copy()) 28 | self.model2 = type(str('AppTestModel2'), (models.Model, ), 29 | attrs_1.copy()) 30 | self.model3 = type(str('AppTestModel3'), (models.Model, ), 31 | attrs_2.copy()) 32 | 33 | def _trigger_signal(self): 34 | post_migrate.send( 35 | sender=self.appconfig, 36 | app_config=self.appconfig, 37 | verbosity=1, 38 | interactive=True, 39 | using='default') 40 | 41 | @override_settings( 42 | ADMIN_VIEW_PERMISSION_MODELS=['test_app.AppTestModel1', ] 43 | ) 44 | def test_ready__with_one_model(self): 45 | self._trigger_signal() 46 | self.assertEqual(self.model1._meta.permissions, 47 | [('view_apptestmodel1', 'Can view apptestmodel1'), ]) 48 | self.assertEqual(self.model2._meta.permissions, []) 49 | 50 | @override_settings( 51 | ADMIN_VIEW_PERMISSION_MODELS=[] 52 | ) 53 | def test_ready__without_model_list(self): 54 | self._trigger_signal() 55 | self.assertEqual(self.model1._meta.permissions, []) 56 | self.assertEqual(self.model2._meta.permissions, []) 57 | 58 | @override_settings( 59 | ADMIN_VIEW_PERMISSION_MODELS=() 60 | ) 61 | def test_ready__without_model_tuple(self): 62 | self._trigger_signal() 63 | self.assertEqual(self.model1._meta.permissions, []) 64 | self.assertEqual(self.model2._meta.permissions, []) 65 | 66 | @override_settings( 67 | ADMIN_VIEW_PERMISSION_MODELS=None 68 | ) 69 | def test_ready__with_none(self): 70 | self._trigger_signal() 71 | self.assertEqual(self.model1._meta.permissions, 72 | [('view_apptestmodel1', 'Can view apptestmodel1'), ]) 73 | self.assertEqual(self.model2._meta.permissions, 74 | [('view_apptestmodel2', 'Can view apptestmodel2'), ]) 75 | 76 | @override_settings( 77 | ADMIN_VIEW_PERMISSION_MODELS=['test_app.AppTestModel3', ] 78 | ) 79 | def test_ready__with_other_permissions(self): 80 | self._trigger_signal() 81 | self.assertEqual(self.model3._meta.permissions, 82 | ((u'copy_apptestmodel3', u'Can copy apptestmodel3'), 83 | (u'view_apptestmodel3', u'Can view apptestmodel3'))) 84 | 85 | @override_settings( 86 | ADMIN_VIEW_PERMISSION_MODELS=[] 87 | ) 88 | def test_ready__with_other_permissions_and_with_none(self): 89 | self._trigger_signal() 90 | self.assertEqual( 91 | self.model3._meta.permissions, 92 | ((u'copy_apptestmodel3', u'Can copy apptestmodel3'), ) 93 | ) 94 | -------------------------------------------------------------------------------- /tests/tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.test import SimpleTestCase 4 | 5 | from admin_view_permission.enums import DjangoVersion 6 | from admin_view_permission.utils import django_version 7 | 8 | try: 9 | from unittest.mock import patch 10 | except ImportError: 11 | from mock import patch 12 | 13 | 14 | class TestUtils(SimpleTestCase): 15 | 16 | @patch('django.get_version', lambda: '1.8') 17 | def test_django_version__with_django_18(self): 18 | assert django_version() == DjangoVersion.DJANGO_18 19 | 20 | @patch('django.get_version', lambda: '1.9') 21 | def test_django_version__with_django_19(self): 22 | assert django_version() == DjangoVersion.DJANGO_19 23 | 24 | @patch('django.get_version', lambda: '1.10') 25 | def test_django_version__with_django_110(self): 26 | assert django_version() == DjangoVersion.DJANGO_110 27 | 28 | @patch('django.get_version', lambda: '1.11') 29 | def test_django_version__with_django_111(self): 30 | assert django_version() == DjangoVersion.DJANGO_111 31 | 32 | @patch('django.get_version', lambda: '2.0') 33 | def test_django_version__with_django_20(self): 34 | assert django_version() == DjangoVersion.DJANGO_20 35 | 36 | # @patch('django.get_version', lambda: '1.8') 37 | # def test_get_model_name_with_django_18(self): 38 | # assert get_model_name(TestModel1) == 'test_app.TestModel1' 39 | # 40 | # @patch('django.get_version', lambda: '1.9') 41 | # def test_get_model_name_with_django_bigger_than_18(self): 42 | # assert get_model_name(TestModel1) == 'test_app.TestModel1' 43 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | url(r'^admin/', admin.site.urls), 6 | ] 7 | --------------------------------------------------------------------------------