diff --git a/cof/settings/common.py b/cof/settings/common.py index 0437f5db..09d8422a 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -68,6 +68,7 @@ INSTALLED_APPS = [ 'autocomplete_light', 'captcha', 'django_cas_ng', + 'djangoformsetjs', 'bootstrapform', 'kfet', 'kfet.open', diff --git a/kfet/auth/models.py b/kfet/auth/models.py index c8648eda..3bceec2d 100644 --- a/kfet/auth/models.py +++ b/kfet/auth/models.py @@ -5,6 +5,8 @@ from django.db import models from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ +from kfet.cms.models import CmsPermissionManager + class GenericTeamTokenManager(models.Manager): @@ -52,6 +54,7 @@ class CorePermissionManager(models.Manager): class Permission(DjangoPermission): kfetcore = CorePermissionManager() + kfetcms = CmsPermissionManager() class Meta: proxy = True diff --git a/kfet/auth/views.py b/kfet/auth/views.py index 018e33db..1386b5d6 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -11,6 +11,8 @@ from django.utils.translation import ugettext_lazy as _ from django.views.generic import View from django.views.decorators.http import require_http_methods +from kfet.cms.views import get_kfetcms_group_formview_extra + from .forms import GroupForm from .models import GenericTeamToken, Group @@ -125,6 +127,8 @@ def get_group_formview_extras(): _group_formview_extras = [] # Register additional group forms below. + _group_formview_extras.append( + get_kfetcms_group_formview_extra()) return [extra.copy() for extra in _group_formview_extras] diff --git a/kfet/cms/forms.py b/kfet/cms/forms.py new file mode 100644 index 00000000..7b38db17 --- /dev/null +++ b/kfet/cms/forms.py @@ -0,0 +1,194 @@ +from django import forms +from django.contrib.auth.models import Group +from django.template.loader import render_to_string +from django.utils.functional import cached_property +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailusers.forms import GroupPagePermissionFormSet + +from utils.forms import KeepUnselectableModelFormMixin + +from kfet.auth.fields import BasePermissionsField +from kfet.auth.models import Permission + + +class CmsGroupForm(KeepUnselectableModelFormMixin, forms.ModelForm): + """ + Allow to change permissions related to the cms of a `Group` object. + """ + access_admin = forms.BooleanField( + label=_("Accès à l'administration"), + required=False, + help_text=_( + "Attention : Aucun paramétrage effectué dans cette section " + "n'aura d'effet si cette case est décochée." + ), + ) + + class Meta: + model = Group + fields = () + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + group = self.instance + + if group.pk is not None: + self.fields['access_admin'].initial = ( + group.permissions.filter(pk=self.access_admin_perm.pk).exists() + ) + + def save(self): + group = super().save() + + if self.cleaned_data['access_admin']: + group.permissions.add(self.access_admin_perm) + else: + group.permissions.remove(self.access_admin_perm) + + return group + + @cached_property + def access_admin_perm(self): + return Permission.objects.get( + content_type__app_label='wagtailadmin', + codename='access_admin', + ) + + +class SnippetsCmsGroupForm(KeepUnselectableModelFormMixin, forms.ModelForm): + permissions = BasePermissionsField( + label='', + queryset=Permission.kfetcms.all(), + ) + + class Meta: + model = Group + fields = ('permissions',) + + +def prepare_page_permissions_formset(pages): + """ + Create a new formset from base `GroupPagePermissionFormSet` Wagtail + formset. + + - The choices of `pages` field of forms are limited to `pages`. + - If there is no initial data, add an initial with the first collection + selected. + - Make 'as_admin_panel' use a custom template. + + Arguments + pages (iterable of `Page`) + + Returns + A formset to select group permissions of pages. + + """ + class RestrictedFormSet(GroupPagePermissionFormSet): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Remove forms cache, as theirs options are changed. + del self.forms + + # 'initial' must be setup before accessing 'forms'. + + self.initial = list( + filter(lambda i: i['page'] in pages, self.initial)) + + # This is just little kindness. + if not self.initial and pages: + self.initial = [{'page': pages[0]}] + + for form in self.forms: + self.customize_form(form) + + @property + def empty_form(self): + form = super().empty_form + self.customize_form(form) + return form + + def customize_form(self, form): + # Widget must be setup before queryset. + form.fields['page'].widget = forms.Select() + form.fields['page'].queryset = pages + # Force use of `CheckboxInput` for `DELETE` field, as `HiddenInput` + # is not compatible with `django-formset-js`. + form.fields['DELETE'].widget = forms.CheckboxInput() + + def as_admin_panel(self): + # http://docs.wagtail.io/en/latest/reference/hooks.html#register-group-permission-panel + template = 'kfet/permissions/page_permissions_formset.html' + return render_to_string(template, {'formset': self}) + + return RestrictedFormSet + + +def prepare_collection_member_permissions_formset(formset_cls, collections): + """ + Create a new formset from base `formset_cls`. + + - The choices of `collections` field of forms produced by `formset_cls` are + limited to `collections`. + - If there is no initial data, add an initial with the first collection + selected. + - Make 'as_admin_panel' use a custom template. + + Arguments + formset_cls (subclass of + `wagtail.wagtailcore.forms` + `.BaseGroupCollectionMemberPermissionFormSet` + ): + Formset to select group permissions of a collection member model. + It should be rerieved from the `group_permission_panel` Wagtail + hook. This includes the `Document` and `Image` models of Wagtail. + collections (iterable of `wagtail.wagtailcore.models.Collection`) + + Returns + A formset to select group permissions of collection-related models. + + """ + class RestrictedFormSet(formset_cls): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Remove forms cache, as theirs options are changed. + del self.forms + + # 'initial' must be setup before accessing 'forms'. + + self.initial = list( + filter(lambda i: i['collection'] in collections, self.initial)) + + # This is just little kindness. + if not self.initial and collections: + self.initial = [{'collection': collections[0]}] + + for form in self.forms: + self.customize_form(form) + + @property + def empty_form(self): + form = super().empty_form + self.customize_form(form) + return form + + def customize_form(self, form): + form.fields['collection'].queryset = collections + # Force use of `CheckboxInput` for `DELETE` field, as `HiddenInput` + # is not compatible with `django-formset-js`. + form.fields['DELETE'].widget = forms.CheckboxInput() + + def as_admin_panel(self): + template = ( + 'kfet/permissions/collection_member_permissions_formset.html') + model = self.permission_queryset[0].content_type.model_class() + return render_to_string(template, { + 'formset': self, + 'model_name': model._meta.verbose_name_plural, + }) + + return RestrictedFormSet diff --git a/kfet/cms/management/commands/kfet_loadwagtail.py b/kfet/cms/management/commands/kfet_loadwagtail.py index 86b94d3e..169c278b 100644 --- a/kfet/cms/management/commands/kfet_loadwagtail.py +++ b/kfet/cms/management/commands/kfet_loadwagtail.py @@ -1,8 +1,14 @@ -from django.contrib.auth.models import Group from django.core.management import call_command from django.core.management.base import BaseCommand +from django.db.models import Q -from wagtail.wagtailcore.models import Page, Site +from wagtail.wagtailcore.models import ( + GroupCollectionPermission, GroupPagePermission, Page, Site, +) + +from kfet.models import Group, Permission + +from ...utils import get_kfet_root_collection, get_kfet_root_page class Command(BaseCommand): @@ -33,3 +39,169 @@ class Command(BaseCommand): # Par défaut, il s'agit d'une copie du site K-Fêt (17-05) call_command('loaddata', options['file']) + + # Si les groupes K-Fêt existent, certaines permissions du CMS leur sont + # données. + + try: + group_chef = Group.objects.get(name='K-Fêt César') + except Group.DoesNotExist: + pass + else: + self.add_admin_access(group_chef) + + try: + group_boy = Group.objects.get(name='K-Fêt Légionnaire') + except Group.DoesNotExist: + pass + else: + self.add_staff_access(group_boy) + + def add_admin_access(self, group): + """ + Add all cms-related permissions to `group`. + + Explicitly, permissions added are: + - access admin of Wagtail, + - all permissions for the kfet root page (by inheritance, this applies + to all its descendants), + - all permissions on the MemberTeam snippet, + - add/change documents, + - add/change images. + + To avoid bugs related to permissions not added by this method, it is + guaranteed the group has more or the same permissions than at the + beginning. + """ + group.permissions.add( + Permission.objects.get( + content_type__app_label='wagtailadmin', + codename='access_admin', + ), + # Snippets permissions + *Permission.kfetcms.all(), + ) + + # Page permissions: set all for the kfet root page. + + root_page = get_kfet_root_page() + + p_types = ('add', 'edit', 'publish', 'bulk_delete', 'lock') + + GroupPagePermission.objects.filter( + group=group, page=root_page, + permission_type__in=p_types, + ).delete() + + GroupPagePermission.objects.bulk_create([ + GroupPagePermission( + group=group, page=root_page, + permission_type=p_type, + ) + for p_type in p_types + ]) + + # Collection-based permissions: set all for the kfet root collection + # for each known collection-based model (docs, images). + + root_collection = get_kfet_root_collection() + + collection_perms = Permission.objects.filter( + Q( + content_type__app_label='wagtaildocs', + codename__in=['add_document', 'change_document'], + ) | + Q( + content_type__app_label='wagtailimages', + codename__in=['add_image', 'change_image'], + ) + ) + + GroupCollectionPermission.objects.filter( + group=group, collection=root_collection, + permission__in=collection_perms, + ).delete() + + GroupCollectionPermission.objects.bulk_create([ + GroupCollectionPermission( + group=group, collection=root_collection, + permission=perm, + ) + for perm in collection_perms + ]) + + def add_staff_access(self, group): + """ + Add a subset of cms-related permissions to `group`. + + Permissions added are: + - access admin of Wagtail, + - add/edit permissions for the kfet root page (by inheritance, this + applies to all its descendants), + - all permissions on the MemberTeam snippet, + - add/change own documents, + - add/change own images. + + Because 'publish' page permission type is not given, group members can + only create or change pages as drafts. + + To avoid bugs related to permissions not added by this method, it is + guaranteed the group has more or the same permissions than at the + beginning. + """ + group.permissions.add( + Permission.objects.get( + content_type__app_label='wagtailadmin', + codename='access_admin', + ), + *Permission.kfetcms.filter(codename__in=[ + 'add_memberteam', + ]), + ) + + # Give 'safe' operations permissions for the kfet root page. + + root_page = get_kfet_root_page() + + p_types = ('add', 'edit') + + GroupPagePermission.objects.filter( + group=group, page=root_page, + permission_type__in=p_types, + ).delete() + + GroupPagePermission.objects.bulk_create([ + GroupPagePermission( + group=group, page=root_page, + permission_type=p_type, + ) + for p_type in p_types + ]) + + # Give 'safe' operations permissions for the collection-based models. + + root_collection = get_kfet_root_collection() + + collection_perms = Permission.objects.filter( + Q( + content_type__app_label='wagtaildocs', + codename__in=['add_document'], + ) | + Q( + content_type__app_label='wagtailimages', + codename__in=['add_image'], + ) + ) + + GroupCollectionPermission.objects.filter( + group=group, collection=root_collection, + permission__in=collection_perms, + ).delete() + + GroupCollectionPermission.objects.bulk_create([ + GroupCollectionPermission( + group=group, collection=root_collection, + permission=perm, + ) + for perm in collection_perms + ]) diff --git a/kfet/cms/models.py b/kfet/cms/models.py index 0dff183f..5ac956a2 100644 --- a/kfet/cms/models.py +++ b/kfet/cms/models.py @@ -11,7 +11,7 @@ from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtailsnippets.blocks import SnippetChooserBlock from wagtail.wagtailsnippets.models import register_snippet -from kfet.cms.context_processors import get_articles +from .utils import get_page_model_names @register_snippet @@ -60,6 +60,7 @@ class MenuBlock(blocks.StaticBlock): template = 'kfetcms/block_menu.html' def get_context(self, *args, **kwargs): + from .context_processors import get_articles context = super().get_context(*args, **kwargs) context.update(get_articles()) return context @@ -172,3 +173,18 @@ class KFetPage(Page): page.seo_title = page.title return context + + +## +# Helpers for kfetauth app +## + + +class CmsPermissionManager(models.Manager): + def get_queryset(self): + return ( + super().get_queryset() + .filter(content_type__app_label='kfetcms') + # Permissions of Page-based models are unused. + .exclude(content_type__model__in=get_page_model_names()) + ) diff --git a/kfet/cms/templates/kfet/permissions/collection_member_permissions_form.html b/kfet/cms/templates/kfet/permissions/collection_member_permissions_form.html new file mode 100644 index 00000000..1286e780 --- /dev/null +++ b/kfet/cms/templates/kfet/permissions/collection_member_permissions_form.html @@ -0,0 +1,20 @@ +{% load widget_tweaks %} +{% load kfet_extras %} + +