From c4dbe6f73517395ec3c769f191c66ea133b596e7 Mon Sep 17 00:00:00 2001 From: Rivo Laks Date: Sat, 18 May 2013 15:07:34 +0200 Subject: [PATCH 1/8] Implement proper permission checkin in Admin2 Uses Django's builtin per-model permissions (add/change/delete plus view which we'll add) and also supports per-object permissions. --- djadmin2/models.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/djadmin2/models.py b/djadmin2/models.py index 6737fa8..d1b705c 100644 --- a/djadmin2/models.py +++ b/djadmin2/models.py @@ -41,24 +41,41 @@ class BaseAdmin2(object): readonly_fields = () ordering = None - def has_view_permission(self, request): - """ - Returns True if the given HttpRequest has permission to view - *at least one* page in the mongonaut site. - """ - return request.user.is_authenticated() and request.user.is_active - def has_edit_permission(self, request): + # TODO: make the model argument required after the registration code has been refactored. + # def __init__(self, model): + def __init__(self, model=None): + super(BaseAdmin2, self).__init__() + + self.model = model + + + def _user_has_permission(self, user, permission_type, obj=None): + """ Generic method for checking whether the user has permission of specified type for the model. + Type can be one of view, add, change, delete. + You can also specify instance of the model for object-specific permission check. + """ + if not user.is_authenticated() or not user.is_staff: + return False + opts = self.model._meta + full_permission_name = '%s.%s_%s' % (opts.app_label, permission_type, opts.object_name.lower()) + return user.has_perm(full_permission_name, obj) + + def has_view_permission(self, request, obj=None): + """ Can view this object """ + return self._user_has_permission(request.user, 'view', obj) + + def has_edit_permission(self, request, obj=None): """ Can edit this object """ - return request.user.is_authenticated() and request.user.is_active and request.user.is_staff + return self._user_has_permission(request.user, 'change', obj) - def has_add_permission(self, request): + def has_add_permission(self, request, obj=None): """ Can add this object """ - return request.user.is_authenticated() and request.user.is_active and request.user.is_staff + return self._user_has_permission(request.user, 'add', obj) - def has_delete_permission(self, request): + def has_delete_permission(self, request, obj=None): """ Can delete this object """ - return request.user.is_authenticated() and request.user.is_active and request.user.is_superuser + return self._user_has_permission(request.user, 'delete', obj) class Admin2(BaseAdmin2): From e29767a208fb08c0ccc51dd4fdff9f9df48e2160 Mon Sep 17 00:00:00 2001 From: Rivo Laks Date: Sat, 18 May 2013 15:09:14 +0200 Subject: [PATCH 2/8] Create view permissions for all models when syncdb is run Works exactly the same way as that of the standard add/change/delete permissions (the code is in fact copied from there). --- djadmin2/models.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/djadmin2/models.py b/djadmin2/models.py index d1b705c..b886e53 100644 --- a/djadmin2/models.py +++ b/djadmin2/models.py @@ -5,6 +5,8 @@ synonymous with the django.contrib.admin.sites model. """ +from django.contrib.auth import models as auth_app +from django.db.models import get_models, signals try: import floppyforms as forms @@ -90,3 +92,55 @@ class Admin2(BaseAdmin2): search_fields = () save_as = False save_on_top = False + + + +def create_permissions(app, created_models, verbosity, **kwargs): + """ + Creates 'view' permissions for all models. + django.contrib.auth only creates add, change and delete permissions. Since we also support read-only views, we need + to add our own extra permission. + Copied from django.contrib.auth.management.create_permissions + """ + from django.contrib.contenttypes.models import ContentType + + def _get_permission_codename(action, opts): + return u'%s_%s' % (action, opts.object_name.lower()) + + app_models = get_models(app) + + # This will hold the permissions we're looking for as + # (content_type, (codename, name)) + searched_perms = list() + # The codenames and ctypes that should exist. + ctypes = set() + for klass in app_models: + ctype = ContentType.objects.get_for_model(klass) + ctypes.add(ctype) + + opts = klass._meta + perm = (_get_permission_codename('view', opts), u'Can view %s' % opts.verbose_name_raw) + searched_perms.append((ctype, perm)) + + # Find all the Permissions that have a context_type for a model we're + # looking for. We don't need to check for codenames since we already have + # a list of the ones we're going to create. + all_perms = set(auth_app.Permission.objects.filter( + content_type__in=ctypes, + ).values_list( + "content_type", "codename" + )) + + objs = [ + auth_app.Permission(codename=codename, name=name, content_type=ctype) + for ctype, (codename, name) in searched_perms + if (ctype.pk, codename) not in all_perms + ] + auth_app.Permission.objects.bulk_create(objs) + if verbosity >= 2: + for obj in objs: + print "Adding permission '%s'" % obj + + +signals.post_syncdb.connect(create_permissions, + dispatch_uid = "django-admin2.djadmin2.models.create_permissions") From e7861323caf4a0d247392f6a9dc8b7949fff830f Mon Sep 17 00:00:00 2001 From: Rivo Laks Date: Sat, 18 May 2013 17:44:56 +0200 Subject: [PATCH 3/8] Integrate permissions into views Views now ensure that the user has corresponding permissions. Also, action buttons, e.g. Add, are shown only when the user can actually do that. --- djadmin2/models.py | 25 ++++----- .../admin2/bootstrap/model_list.html | 14 ++++- djadmin2/views.py | 53 ++++++++++++++++--- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/djadmin2/models.py b/djadmin2/models.py index cb6faf7..0131c88 100644 --- a/djadmin2/models.py +++ b/djadmin2/models.py @@ -47,9 +47,7 @@ class BaseAdmin2(object): ordering = None - # TODO: make the model argument required after the registration code has been refactored. - # def __init__(self, model): - def __init__(self, model=None): + def __init__(self, model): super(BaseAdmin2, self).__init__() self.model = model @@ -66,21 +64,24 @@ class BaseAdmin2(object): full_permission_name = '%s.%s_%s' % (opts.app_label, permission_type, opts.object_name.lower()) return user.has_perm(full_permission_name, obj) + def has_permission(self, request, permission_type, obj=None): + return self._user_has_permission(request.user, permission_type, obj) + def has_view_permission(self, request, obj=None): """ Can view this object """ - return self._user_has_permission(request.user, 'view', obj) + return self.has_permission(request, 'view', obj) def has_edit_permission(self, request, obj=None): """ Can edit this object """ - return self._user_has_permission(request.user, 'change', obj) + return self.has_permission(request, 'change', obj) def has_add_permission(self, request, obj=None): """ Can add this object """ - return self._user_has_permission(request.user, 'add', obj) + return self.has_permission(request, 'add', obj) def has_delete_permission(self, request, obj=None): """ Can delete this object """ - return self._user_has_permission(request.user, 'delete', obj) + return self.has_permission(request, 'delete', obj) class ModelAdmin2(BaseAdmin2): @@ -109,27 +110,27 @@ class ModelAdmin2(BaseAdmin2): return patterns('', url( regex=r'^$', - view=self.index_view.as_view(model=self.model), + view=self.index_view.as_view(modeladmin=self), name='index' ), url( regex=r'^create/$', - view=self.create_view.as_view(model=self.model), + view=self.create_view.as_view(modeladmin=self), name='create' ), url( regex=r'^(?P[0-9]+)/$', - view=self.detail_view.as_view(model=self.model), + view=self.detail_view.as_view(modeladmin=self), name='detail' ), url( regex=r'^(?P[0-9]+)/update/$', - view=self.update_view.as_view(model=self.model), + view=self.update_view.as_view(modeladmin=self), name='update' ), url( regex=r'^(?P[0-9]+)/delete/$', - view=self.delete_view.as_view(model=self.model), + view=self.delete_view.as_view(modeladmin=self), name='delete' ), ) diff --git a/djadmin2/templates/admin2/bootstrap/model_list.html b/djadmin2/templates/admin2/bootstrap/model_list.html index e71bd3b..36ab408 100644 --- a/djadmin2/templates/admin2/bootstrap/model_list.html +++ b/djadmin2/templates/admin2/bootstrap/model_list.html @@ -1,10 +1,20 @@ {% extends "admin2/bootstrap/base.html" %} {% block content %} - add + {% if has_add_permission %} + add + {% endif %}
{% for obj in object_list %} - {{ obj }} detail edit delete
+ {{ obj }} + detail + {% if has_edit_permission %} + edit + {% endif %} + {% if has_delete_permission %} + delete + {% endif %} +
{% endfor %} diff --git a/djadmin2/views.py b/djadmin2/views.py index 4ca2c2f..c511069 100644 --- a/djadmin2/views.py +++ b/djadmin2/views.py @@ -1,32 +1,66 @@ import os from django.conf import settings +from django.contrib.auth.views import redirect_to_login +from django.core.exceptions import PermissionDenied from django.forms.models import modelform_factory from django.views import generic from django.db import models -from braces.views import LoginRequiredMixin, StaffuserRequiredMixin +from braces.views import LoginRequiredMixin, StaffuserRequiredMixin, AccessMixin from .utils import get_admin2s ADMIN2_THEME_DIRECTORY = getattr(settings, "ADMIN2_THEME_DIRECTORY", "admin2/bootstrap") + class Admin2Mixin(object): def get_template_names(self): return [os.path.join(ADMIN2_THEME_DIRECTORY, self.default_template_name)] def get_model(self): - return self.model + return self.modeladmin.model def get_queryset(self): return self.get_model()._default_manager.all() + def get_form_class(self): if self.form_class is not None: return self.form_class return modelform_factory(self.get_model()) +class AdminModel2Mixin(Admin2Mixin, AccessMixin): + modeladmin = None + # Permission type to check for when a request is sent to this view. + permission_type = None + + def dispatch(self, request, *args, **kwargs): + # Check if user has necessary permissions. If the permission_type isn't specified then check for staff status. + print "distpatch perm check:", self.permission_type + has_permission = self.modeladmin.has_permission(request, self.permission_type) \ + if self.permission_type else request.user.is_staff + # Raise exception or redirect to login if user doesn't have permissions. + if not has_permission: + if self.raise_exception: + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), self.get_redirect_field_name()) + + return super(AdminModel2Mixin, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(AdminModel2Mixin, self).get_context_data(**kwargs) + context.update({ + 'has_add_permission': self.modeladmin.has_add_permission(self.request), + 'has_edit_permission': self.modeladmin.has_edit_permission(self.request), + 'has_delete_permission': self.modeladmin.has_delete_permission(self.request), + }) + return context + + class IndexView(Admin2Mixin, generic.ListView): default_template_name = "index.html" @@ -34,26 +68,31 @@ class IndexView(Admin2Mixin, generic.ListView): return get_admin2s() -class ModelListView(Admin2Mixin, generic.ListView): +class ModelListView(AdminModel2Mixin, generic.ListView): default_template_name = "model_list.html" + permission_type = 'view' -class ModelDetailView(Admin2Mixin, generic.DetailView): +class ModelDetailView(AdminModel2Mixin, generic.DetailView): default_template_name = "model_detail.html" + permission_type = 'view' -class ModelEditFormView(Admin2Mixin, generic.UpdateView): +class ModelEditFormView(AdminModel2Mixin, generic.UpdateView): form_class = None success_url = "../../" default_template_name = "model_edit_form.html" + permission_type = 'change' -class ModelAddFormView(Admin2Mixin, generic.CreateView): +class ModelAddFormView(AdminModel2Mixin, generic.CreateView): form_class = None success_url = "../" default_template_name = "model_add_form.html" + permission_type = 'add' -class ModelDeleteView(Admin2Mixin, generic.DeleteView): +class ModelDeleteView(AdminModel2Mixin, generic.DeleteView): success_url = "../../" default_template_name = "model_delete.html" + permission_type = 'delete' From a99952375cba712572528d29aa9698b02d9c1ec1 Mon Sep 17 00:00:00 2001 From: Audrey Roy Date: Sat, 18 May 2013 17:52:53 +0200 Subject: [PATCH 4/8] Display model name in template --- djadmin2/templates/admin2/bootstrap/model_list.html | 4 +++- djadmin2/views.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/djadmin2/templates/admin2/bootstrap/model_list.html b/djadmin2/templates/admin2/bootstrap/model_list.html index e71bd3b..ab86a8b 100644 --- a/djadmin2/templates/admin2/bootstrap/model_list.html +++ b/djadmin2/templates/admin2/bootstrap/model_list.html @@ -1,7 +1,9 @@ {% extends "admin2/bootstrap/base.html" %} {% block content %} - add + +

{{ model_pluralized|title }}

+ Add {{ model|title }}
{% for obj in object_list %} {{ obj }} detail edit delete
diff --git a/djadmin2/views.py b/djadmin2/views.py index 63fd427..d87b08a 100644 --- a/djadmin2/views.py +++ b/djadmin2/views.py @@ -42,6 +42,13 @@ class IndexView(Admin2Mixin, generic.TemplateView): class ModelListView(Admin2Mixin, generic.ListView): default_template_name = "model_list.html" + def get_context_data(self, **kwargs): + context = super(ModelListView, self).get_context_data(**kwargs) + context['model'] = self.get_model()._meta.verbose_name + context['model_pluralized'] = self.get_model()._meta.verbose_name_plural + # context['model'] = self.get_queryset().model._meta.verbose_name + return context + class ModelDetailView(Admin2Mixin, generic.DetailView): default_template_name = "model_detail.html" From aa89b351b97199752c1a07feadf2cd1d5849e1fb Mon Sep 17 00:00:00 2001 From: Daniel Greenfeld Date: Sat, 18 May 2013 18:07:02 +0200 Subject: [PATCH 5/8] Update CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index fa8f2e7..22d0df7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -14,4 +14,4 @@ Developers * Raphael Kimmig (@RaphaelKimmig) * Andrew Ingram (@AndrewIngram) * Gregor Müllegger (@gregmuellegger) - +* Rivo Laks (@rivol) From 5fb94b69becb297120aae981d5352c7f894f096a Mon Sep 17 00:00:00 2001 From: Daniel Greenfeld Date: Sat, 18 May 2013 18:09:59 +0200 Subject: [PATCH 6/8] Document fixing --- docs/TODO | 0 docs/index.rst | 23 +++++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) delete mode 100644 docs/TODO diff --git a/docs/TODO b/docs/TODO deleted file mode 100644 index e69de29..0000000 diff --git a/docs/index.rst b/docs/index.rst index 1710cc4..f9b5cd7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,17 +12,24 @@ Our goal is to make this API work: .. code-block:: python - # myapp/admin2.py + # Import your custom models + from .models import Post, Comment + from django.contrib.auth.forms import UserCreationForm, UserChangeForm + from django.contrib.auth.models import User - # Import the Admin2 base class - from djadmin2.models import Admin2 + import djadmin2 + from djadmin2.models import ModelAdmin2 - # Import your custom models - from blog.models import Post - # Instantiate the Admin2 class - # Then attach the admin2 object to your model - Post.admin2 = Admin2() + class UserAdmin2(ModelAdmin2): + create_form_class = UserCreationForm + update_form_class = UserChangeForm + + + # Register each model with the admin + djadmin2.default.register(Post) + djadmin2.default.register(Comment) + djadmin2.default.register(User, UserAdmin2) .. _GitHub: https://github.com/pydanny/django-admin2 From 74c86f9c3bd08123534a93a81aaf498b62a21deb Mon Sep 17 00:00:00 2001 From: Audrey Roy Date: Sat, 18 May 2013 18:16:25 +0200 Subject: [PATCH 7/8] Partial checkin of #61 --- .../admin2/bootstrap/model_list.html | 52 +++++++++++++------ djadmin2/views.py | 1 - 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/djadmin2/templates/admin2/bootstrap/model_list.html b/djadmin2/templates/admin2/bootstrap/model_list.html index 631a23f..8121584 100644 --- a/djadmin2/templates/admin2/bootstrap/model_list.html +++ b/djadmin2/templates/admin2/bootstrap/model_list.html @@ -1,22 +1,42 @@ {% extends "admin2/bootstrap/base.html" %} {% block content %} -

{{ model_pluralized|title }}

- {% if has_add_permission %} - add - {% endif %} -
-{% for obj in object_list %} - {{ obj }} - detail - {% if has_edit_permission %} - edit - {% endif %} - {% if has_delete_permission %} - delete - {% endif %} -
-{% endfor %} +
+
+

Select {{ model }} to change

+
+
+ {% if has_add_permission %} + Add {{ model|title }} + {% endif %} +
+
+
+ +
+
+ + + + + + + {% for obj in object_list %} + + + {% endfor %} + +
{{ model|title}}
+ {{ obj }} detail + {% if has_edit_permission %} + edit + {% endif %} + {% if has_delete_permission %} + delete + {% endif %} +
+
+
{% endblock content %} diff --git a/djadmin2/views.py b/djadmin2/views.py index 6a6f86f..d0250ad 100644 --- a/djadmin2/views.py +++ b/djadmin2/views.py @@ -78,7 +78,6 @@ class ModelListView(AdminModel2Mixin, generic.ListView): context = super(ModelListView, self).get_context_data(**kwargs) context['model'] = self.get_model()._meta.verbose_name context['model_pluralized'] = self.get_model()._meta.verbose_name_plural - # context['model'] = self.get_queryset().model._meta.verbose_name return context From f5f94c352c5f3b2b403a3ba92fff0cf717534b61 Mon Sep 17 00:00:00 2001 From: Daniel Greenfeld Date: Sat, 18 May 2013 18:24:41 +0200 Subject: [PATCH 8/8] remove confusing comments --- example/example/urls.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/example/example/urls.py b/example/example/urls.py index d5e661f..34f1f72 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -8,12 +8,6 @@ import djadmin2 djadmin2.default.autodiscover() urlpatterns = patterns('', - # Examples: url(r'^admin2/', include(djadmin2.default.urls)), - # url(r'^example/', include('example.foo.urls')), - - # Uncomment the admin/doc line below to enable admin documentation: - # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^admin/', include(admin.site.urls)), )