diff --git a/cof/settings/common.py b/cof/settings/common.py index f92dc83b..358c71cf 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -69,6 +69,7 @@ INSTALLED_APPS = [ 'autocomplete_light', 'captcha', 'django_cas_ng', + 'djangoformsetjs', 'bootstrapform', 'kfet', 'kfet.open', diff --git a/kfet/apps.py b/kfet/apps.py index 4f114c37..157f6d02 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- - -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * - from django.apps import AppConfig + class KFetConfig(AppConfig): name = 'kfet' verbose_name = "Application K-Fêt" @@ -15,5 +11,5 @@ class KFetConfig(AppConfig): def register_config(self): import djconfig - from kfet.forms import KFetConfigForm + from .config import KFetConfigForm djconfig.register(KFetConfigForm) diff --git a/kfet/auth/fields.py b/kfet/auth/fields.py index 045667a8..998749f5 100644 --- a/kfet/auth/fields.py +++ b/kfet/auth/fields.py @@ -1,23 +1,74 @@ +import re +from itertools import groupby +from operator import attrgetter + from django import forms -from django.forms import widgets +from django.utils.translation import ugettext_lazy as _ from .models import Group, Permission +DEFAULT_PERMS = ['view', 'add', 'change', 'delete'] + +DEFAULT_PERMS_LABELS = { + 'view': _("Voir"), + 'add': _("Ajouter"), + 'change': _("Modifier"), + 'delete': _("Supprimer"), +} + class GroupsField(forms.ModelMultipleChoiceField): def __init__(self, **kwargs): kwargs.setdefault('queryset', Group.objects.all()) - kwargs.setdefault('widget', widgets.CheckboxSelectMultiple) + kwargs.setdefault('widget', forms.CheckboxSelectMultiple) super().__init__(**kwargs) class BasePermissionsField(forms.ModelMultipleChoiceField): def __init__(self, **kwargs): - kwargs.setdefault('widget', widgets.CheckboxSelectMultiple) + kwargs.setdefault('widget', forms.CheckboxSelectMultiple(attrs={ + 'field_class': 'permissions-field', + })) super().__init__(**kwargs) - def label_from_instance(self, obj): - return obj.name + # Contain permissions grouped by `ContentType`. Used as choices for + # this field. + grouped_choices = [] + + for ct, perms in groupby(self.queryset, attrgetter('content_type')): + model_opts = ct.model_class()._meta + choices = [] + + # This helps for the default permissions, if they exists, to appear + # at the beginning of the permissions list and use custom labels. + reg_defaults_p = re.compile( + r'^(?P({defaults}))_{model_name}$' + .format( + defaults='|'.join(DEFAULT_PERMS), + model_name=model_opts.model_name, + ) + ) + next_default_pos = 0 + + for p in perms: + match = reg_defaults_p.match(p.codename) + if match: + # `p` is a default permission. Use shorter label and insert + # after the last default permission seen in `choices`. + p_type = match.group('p_type') + choices.insert( + next_default_pos, + (p.id, DEFAULT_PERMS_LABELS[p_type]) + ) + next_default_pos += 1 + else: + # Non-default permissions. Use the permission description, + # instead of its `__str__`. + choices.append((p.id, p.name)) + + grouped_choices.append((model_opts.verbose_name_plural, choices)) + + self.choices = grouped_choices class CorePermissionsField(BasePermissionsField): diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py index 0de47664..5ca5da70 100644 --- a/kfet/auth/forms.py +++ b/kfet/auth/forms.py @@ -4,13 +4,12 @@ from django.utils.translation import ugettext_lazy as _ from utils.forms import KeepUnselectableModelFormMixin - from .fields import GroupsField, CorePermissionsField from .models import Group class GroupForm(KeepUnselectableModelFormMixin, forms.ModelForm): - permissions = CorePermissionsField(label=_("Permissions"), required=False) + permissions = CorePermissionsField(label='', required=False) keep_unselectable_fields = ['permissions'] diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py index 036a30c6..061570a8 100644 --- a/kfet/auth/migrations/0001_initial.py +++ b/kfet/auth/migrations/0001_initial.py @@ -21,26 +21,4 @@ class Migration(migrations.Migration): ('token', models.CharField(unique=True, max_length=50)), ], ), - migrations.CreateModel( - name='Group', - fields=[ - ('group_ptr', models.OneToOneField(parent_link=True, serialize=False, primary_key=True, auto_created=True, to='auth.Group')), - ], - options={ - 'verbose_name': 'Groupe', - 'verbose_name_plural': 'Groupes', - }, - bases=('auth.group',), - ), - migrations.CreateModel( - name='Permission', - fields=[ - ], - options={ - 'verbose_name': 'Permission', - 'verbose_name_plural': 'Permissions', - 'proxy': True, - }, - bases=('auth.permission',), - ), ] diff --git a/kfet/auth/migrations/0002_create_group_permission_models.py b/kfet/auth/migrations/0002_create_group_permission_models.py new file mode 100644 index 00000000..bd79fdb0 --- /dev/null +++ b/kfet/auth/migrations/0002_create_group_permission_models.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfetauth', '0001_initial'), + ] + + operations = [ + # See also `kfetauth.0004` migration which deletes already created + # permissions, if applicable. + migrations.AlterModelOptions( + name='genericteamtoken', + options={ + 'default_permissions': (), + }, + ), + # See also `kfetauth.0003` migration which imports existing K-Fêt + # groups. + # See also `kfetauth.0004` migration which gives the default + # permissions to `Group` objects which have the deleted + # `kfet.manage_perms` permission. + migrations.CreateModel( + name='Group', + fields=[ + ('group_ptr', models.OneToOneField( + parent_link=True, + serialize=False, + primary_key=True, + auto_created=True, + to='auth.Group', + )), + ], + options={ + 'default_permissions': ('view', 'add', 'change'), + 'verbose_name': 'Groupe', + 'verbose_name_plural': 'Groupes', + }, + bases=('auth.group',), + ), + migrations.CreateModel( + name='Permission', + fields=[ + ], + options={ + 'verbose_name': 'Permission', + 'verbose_name_plural': 'Permissions', + 'proxy': True, + }, + bases=('auth.permission',), + ), + ] diff --git a/kfet/auth/migrations/0002_existing_groups.py b/kfet/auth/migrations/0003_existing_groups.py similarity index 85% rename from kfet/auth/migrations/0002_existing_groups.py rename to kfet/auth/migrations/0003_existing_groups.py index f816429e..7ab2dd94 100644 --- a/kfet/auth/migrations/0002_existing_groups.py +++ b/kfet/auth/migrations/0003_existing_groups.py @@ -22,7 +22,8 @@ class Migration(migrations.Migration): """ dependencies = [ - ('kfetauth', '0001_initial'), + ('kfetauth', '0002_create_group_permission_models'), + ('auth', '0006_require_contenttypes_0002'), ] operations = [ diff --git a/kfet/auth/migrations/0004_update_permissions.py b/kfet/auth/migrations/0004_update_permissions.py new file mode 100644 index 00000000..7044fd28 --- /dev/null +++ b/kfet/auth/migrations/0004_update_permissions.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from django.db import migrations +from django.db.models import Q + + +def convert_manage_perms(apps, schema_editor): + """ + Use the permissions of `kfet.auth.models.Group` model, instead of using the + `manage_perms` permission, which is deleted, of `kfet.models.Account`. + + `Group` which have this last permission will get the permissions: + `kfetauth.view_group`, `kfetauth.add_group` and `kfetauth.change_group`. + """ + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + + try: + old_p = Permission.objects.get( + content_type__app_label='kfet', + content_type__model='account', + codename='manage_perms', + ) + except Permission.DoesNotExist: + return + + groups = old_p.group_set.all() + + ct_group, _ = ContentType.objects.get_or_create( + app_label='kfetauth', + model='group', + ) + + view_p, _ = Permission.objects.get_or_create( + content_type=ct_group, codename='view_group', + defaults={'name': 'Can view Groupe'}) + add_p, _ = Permission.objects.get_or_create( + content_type=ct_group, codename='add_group', + defaults={'name': 'Can add Groupe'}) + change_p, _ = Permission.objects.get_or_create( + content_type=ct_group, codename='change_group', + defaults={'name': 'Can change Group'}) + + GroupPermission = Group.permissions.through + + GroupPermission.objects.bulk_create([ + GroupPermission(permission=p, group=g) + for g in groups + for p in (view_p, add_p, change_p) + ]) + + old_p.delete() + + +def delete_unused_permissions(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + + to_delete_q = Q(content_type__model='genericteamtoken') | Q( + content_type__model='group', + codename='delete_group', + ) + + to_delete_q &= Q(content_type__app_label='kfetauth') + + Permission.objects.filter(to_delete_q).delete() + + +class Migration(migrations.Migration): + """ + Data migration about permissions. + """ + dependencies = [ + ('kfetauth', '0003_existing_groups'), + ('auth', '0006_require_contenttypes_0002'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.RunPython(convert_manage_perms), + migrations.RunPython(delete_unused_permissions), + ] diff --git a/kfet/auth/models.py b/kfet/auth/models.py index 0dfd26fd..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): @@ -20,6 +22,9 @@ class GenericTeamToken(models.Model): objects = GenericTeamTokenManager() + class Meta: + default_permissions = () + class Group(DjangoGroup): @@ -35,6 +40,7 @@ class Group(DjangoGroup): class Meta: verbose_name = _("Groupe") verbose_name_plural = _("Groupes") + default_permissions = ('view', 'add', 'change') KFET_CORE_APP_LABELS = ['kfet', 'kfetauth'] @@ -48,6 +54,7 @@ class CorePermissionManager(models.Manager): class Permission(DjangoPermission): kfetcore = CorePermissionManager() + kfetcms = CmsPermissionManager() class Meta: proxy = True diff --git a/kfet/auth/templates/kfet/group_form.html b/kfet/auth/templates/kfet/group_form.html new file mode 100644 index 00000000..44ecfb38 --- /dev/null +++ b/kfet/auth/templates/kfet/group_form.html @@ -0,0 +1,58 @@ +{% extends "kfet/base_form.html" %} +{% load i18n %} + +{% block title %} + {% if not form.instance.pk %} + {% trans "Création d'un groupe" %} + {% else %} + {% blocktrans with name=form.instance.name %} + Modification du groupe "{{ name }}" + {% endblocktrans %} + {% endif %} +{% endblock %} + +{% block header-title %} + {% if not form.instance.pk %} + {% trans "Création d'un groupe" %} + {% else %} + {% blocktrans with name=form.instance.name %} + {{ name }} + Modification du groupe + {% endblocktrans %} + {% endif %} +{% endblock %} + +{% block main %} + +
+ {% csrf_token %} + + {# Base form #} +
+ {% include "kfet/form_snippet.html" with form=form %} +
+ + {# Extra forms #} + {% for extra in extras %} +

+ {{ extra.title }}
+ {{ extra.description }} +

+ + {% for extra_form in extra.forms %} +
+ {% with as_panel=extra_form.as_admin_panel %} + {% if as_panel %} + {{ as_panel }} + {% else %} + {% include "kfet/form_snippet.html" with form=extra_form %} + {% endif %} + {% endwith %} +
+ {% endfor %} + {% endfor %} + + {% include "kfet/form_submit_snippet.html" %} +
+ +{% endblock %} diff --git a/kfet/auth/templates/kfet/group_list.html b/kfet/auth/templates/kfet/group_list.html new file mode 100644 index 00000000..768fa30d --- /dev/null +++ b/kfet/auth/templates/kfet/group_list.html @@ -0,0 +1,58 @@ +{% extends "kfet/base_col_2.html" %} +{% load i18n %} + +{% block title %}{% trans "Groupes de comptes" %}{% endblock %} +{% block header-title %}{% trans "Groupes de comptes" %}{% endblock %} + +{% block fixed %} + +
+ + {% trans "Créer un groupe" %} + +
+ +{% endblock %} + +{% block main %} + +{% for group in groups %} +
+
+ {{ group.name }} + +
+
+

Comptes

+ {% with users=group.user_set.all %} + {% if users %} +
+
    + {% for user in group.user_set.all %} + {% with kfet_user=user.profile.account_kfet %} +
  • + {{ kfet_user }} +
  • + {% endwith %} + {% endfor %} +
+ {% else %} +
+

+ {% blocktrans %} + Aucun compte n'est associé à ce groupe. Rendez-vous sur la page + d'édition d'un compte pour l'y ajouter. + {% endblocktrans %} +

+
+ {% endif %} + {% endwith %} +
+
+{% endfor %} + +{% endblock %} diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index d207ced6..bdb61cd8 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -10,15 +10,15 @@ from django.core.urlresolvers import reverse from django.test import RequestFactory, TestCase from kfet.models import Account +from kfet.tests.testcases import ViewTestCaseMixin +from kfet.tests.utils import create_team, get_perms from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME from .backends import AccountBackend, GenericBackend from .fields import GroupsField, CorePermissionsField -from .forms import GroupForm, UserGroupForm from .middleware import TemporaryAuthMiddleware from .models import GenericTeamToken, Group, Permission from .utils import get_kfet_generic_user -from .views import GenericLoginView ## @@ -85,6 +85,7 @@ class GroupFormTests(TestCase): content_type=ot_ct, codename='cool') def test_creation(self): + from .forms import GroupForm data = { 'name': 'Another Group', 'permissions': [self.kf_perm1.pk], @@ -101,6 +102,7 @@ class GroupFormTests(TestCase): Non-kfet permissions of Group are kept when the form is submitted. Regression test for #168. """ + from .forms import GroupForm self.kf_group.permissions.add(self.ot_perm) selected = [self.kf_perm1, self.kf_perm2] @@ -139,6 +141,7 @@ class UserGroupFormTests(TestCase): User stays in its non-K-Fêt groups. Regression test for #161. """ + from .forms import UserGroupForm # add user to a non-K-Fêt group self.user.groups.add(self.ot_group) @@ -257,6 +260,10 @@ class GenericLoginViewTests(TestCase): patcher_messages.start() self.addCleanup(patcher_messages.stop) + # Prevent querying the database too soon. + from .views import GenericLoginView + self.view_cls = GenericLoginView + user_acc = Account(trigramme='000') user_acc.save({'username': 'user'}) self.user = user_acc.user @@ -348,14 +355,14 @@ class GenericLoginViewTests(TestCase): """ token = GenericTeamToken.objects.create(token='valid') self._set_signed_cookie( - self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'valid') + self.client, self.view_cls.TOKEN_COOKIE_NAME, 'valid') r = self.client.get(self.url) self.assertRedirects(r, reverse('kfet.kpsul')) self.assertEqual(r.wsgi_request.user, self.generic_user) self._is_cookie_deleted( - self.client, GenericLoginView.TOKEN_COOKIE_NAME) + self.client, self.view_cls.TOKEN_COOKIE_NAME) with self.assertRaises(GenericTeamToken.DoesNotExist): token.refresh_from_db() @@ -364,14 +371,14 @@ class GenericLoginViewTests(TestCase): If token is invalid, delete it and try again. """ self._set_signed_cookie( - self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'invalid') + self.client, self.view_cls.TOKEN_COOKIE_NAME, 'invalid') r = self.client.get(self.url) self.assertRedirects(r, self.url, fetch_redirect_response=False) self.assertEqual(r.wsgi_request.user, AnonymousUser()) self._is_cookie_deleted( - self.client, GenericLoginView.TOKEN_COOKIE_NAME) + self.client, self.view_cls.TOKEN_COOKIE_NAME) def test_flow_ok(self): """ @@ -387,6 +394,154 @@ class GenericLoginViewTests(TestCase): self.assertEqual(r.wsgi_request.path, '/k-fet/') +class GroupListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.group' + url_expected = '/k-fet/groupes/' + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + def get_users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfetauth.view_group']), + } + + def setUp(self): + super().setUp() + self.group1 = Group.objects.create(name='K-Fêt - Group1') + self.group2 = Group.objects.create(name='K-Fêt - Group2') + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + self.assertQuerysetEqual( + r.context['groups'], + map(repr, [self.group1, self.group2]), + ordered=False, + ) + + +class GroupCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.group.create' + url_expected = '/k-fet/groupes/nouveau/' + + http_methods = ['GET', 'POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + def get_users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfetauth.add_group']), + } + + @property + def post_data(self): + return { + 'name': 'The Group', + 'permissions': [ + str(self.perms['kfet.is_team'].pk), + str(self.perms['kfetauth.add_group'].pk), + ], + } + + def setUp(self): + super().setUp() + self.perms = get_perms( + 'kfet.is_team', + 'kfetauth.add_group', + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + r = self.client.post(self.url, self.post_data) + self.assertRedirects( + r, reverse('kfet.group'), + fetch_redirect_response=False, # Requires `kfetauth.view_group`. + ) + + group = Group.objects.get(name='The Group') + + self.assertQuerysetEqual( + group.permissions.all(), + map(repr, [ + self.perms['kfet.is_team'], + self.perms['kfetauth.add_group'], + ]), + ordered=False, + ) + + +class GroupUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.group.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def url_kwargs(self): + return {'pk': self.group.pk} + + @property + def url_expected(self): + return '/k-fet/groupes/{}/edition/'.format(self.group.pk) + + def get_users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfetauth.change_group']), + } + + @property + def post_data(self): + return { + 'name': 'The Group', + 'permissions': [ + str(self.perms['kfet.is_team'].pk), + str(self.perms['kfetauth.change_group'].pk), + ], + } + + def setUp(self): + super().setUp() + self.perms = get_perms( + 'kfet.is_team', + 'kfetauth.change_group', + ) + self.group = Group.objects.create(name='K-Fêt - Group') + self.group.permissions = self.perms.values() + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + r = self.client.post(self.url, self.post_data) + self.assertRedirects( + r, reverse('kfet.group'), + fetch_redirect_response=False, # Requires `kfetauth.view_group`. + ) + + self.group.refresh_from_db() + + self.assertEqual(self.group.name, 'The Group') + self.assertQuerysetEqual( + self.group.permissions.all(), + map(repr, [ + self.perms['kfet.is_team'], + self.perms['kfetauth.change_group'], + ]), + ordered=False, + ) + + ## # Temporary authentication # diff --git a/kfet/auth/urls.py b/kfet/auth/urls.py new file mode 100644 index 00000000..5937663e --- /dev/null +++ b/kfet/auth/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls import include, url + +from . import views + +group_patterns = [ + url(r'^$', views.group_index, + name='kfet.group'), + url(r'^nouveau/$', views.group_create, + name='kfet.group.create'), + url('^(?P\d+)/edition/$', views.group_update, + name='kfet.group.update'), +] + +urlpatterns = [ + url(r'^groupes/', include(group_patterns)), + url(r'^login/generic', views.login_generic, + name='kfet.login.generic'), +] diff --git a/kfet/auth/views.py b/kfet/auth/views.py index 073c558f..1386b5d6 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -1,22 +1,23 @@ from django.contrib import messages -from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth import authenticate, login +from django.contrib.auth import authenticate, get_user_model, login from django.contrib.auth.decorators import permission_required -from django.contrib.auth.models import User from django.contrib.auth.views import redirect_to_login -from django.core.urlresolvers import reverse, reverse_lazy +from django.core.urlresolvers import reverse from django.db.models import Prefetch from django.http import QueryDict -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views.generic import View from django.views.decorators.http import require_http_methods -from django.views.generic.edit import CreateView, UpdateView + +from kfet.cms.views import get_kfetcms_group_formview_extra from .forms import GroupForm from .models import GenericTeamToken, Group +User = get_user_model() + class GenericLoginView(View): """ @@ -104,38 +105,105 @@ class GenericLoginView(View): login_generic = GenericLoginView.as_view() -@permission_required('kfet.manage_perms') -def account_group(request): +@permission_required('kfetauth.view_group') +def group_index(request): user_pre = Prefetch( 'user_set', queryset=User.objects.select_related('profile__account_kfet'), ) - groups = ( - Group.objects - .prefetch_related('permissions', user_pre) - ) - return render(request, 'kfet/account_group.html', { + groups = Group.objects.prefetch_related(user_pre) + return render(request, 'kfet/group_list.html', { 'groups': groups, }) -class BaseAccountGroupFormViewMixin: - model = Group - form_class = GroupForm - template_name = 'kfet/account_group_form.html' - success_url = reverse_lazy('kfet.account.group') +_group_formview_extras = None -class AccountGroupFormViewMixin( - SuccessMessageMixin, - BaseAccountGroupFormViewMixin, -): - pass +def get_group_formview_extras(): + global _group_formview_extras + + if _group_formview_extras is None: + _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] -class AccountGroupCreate(AccountGroupFormViewMixin, CreateView): - success_message = 'Nouveau groupe : %(name)s' +@permission_required('kfetauth.add_group') +def group_create(request): + group = Group() + + extras = get_group_formview_extras() + + if request.method == 'POST': + form = GroupForm(request.POST, instance=group) + for extra in extras: + extra['forms'] = [ + form_cls( + request.POST, request.FILES, instance=group, **form_kwargs) + for form_cls, form_kwargs in extra['form_classes'] + ] + extra_forms = sum((extra['forms'] for extra in extras), []) + + if form.is_valid() and all(form.is_valid() for form in extra_forms): + group = form.save() + for extra_form in extra_forms: + extra_form.save() + + messages.success(request, _("Nouveau groupe : {}").format(group)) + + return redirect('kfet.group') + else: + form = GroupForm(instance=group) + for extra in extras: + extra['forms'] = [ + form_cls(instance=group, **form_kwargs) + for form_cls, form_kwargs in extra['form_classes'] + ] + + return render(request, 'kfet/group_form.html', { + 'form': form, + 'extras': extras, + }) -class AccountGroupUpdate(AccountGroupFormViewMixin, UpdateView): - success_message = 'Groupe modifié : %(name)s' +@permission_required('kfetauth.change_group') +def group_update(request, pk): + group = get_object_or_404(Group, pk=pk) + + extras = get_group_formview_extras() + + if request.method == 'POST': + form = GroupForm(request.POST, instance=group) + for extra in extras: + extra['forms'] = [ + form_cls( + request.POST, request.FILES, instance=group, **form_kwargs) + for form_cls, form_kwargs in extra['form_classes'] + ] + extra_forms = sum((extra['forms'] for extra in extras), []) + + if form.is_valid() and all(form.is_valid() for form in extra_forms): + group = form.save() + for extra_form in extra_forms: + extra_form.save() + + messages.success(request, _("Groupe modifié : {}").format(group)) + + return redirect('kfet.group') + else: + form = GroupForm(instance=group) + for extra in extras: + extra['forms'] = [ + form_cls(instance=group, **form_kwargs) + for form_cls, form_kwargs in extra['form_classes'] + ] + + return render(request, 'kfet/group_form.html', { + 'form': form, + 'extras': extras, + }) diff --git a/kfet/cms/forms.py b/kfet/cms/forms.py new file mode 100644 index 00000000..727649b0 --- /dev/null +++ b/kfet/cms/forms.py @@ -0,0 +1,196 @@ +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='', required=False, + queryset=Permission.kfetcms.all(), + ) + + keep_unselectable_fields = ['permissions'] + + 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 %} + +
+ {{ form.non_field_errors }} + {% if form.DELETE %}{% include "kfet/formset_form_actions.html" %}{% endif %} +
+ {{ form.collection|add_class:"form-control input-sm" }} +
+ {% for option in form.permissions %} +
+ {% with p_type=formset.permission_types|get:forloop.counter0 %} + {# p_type format: (identifier, short_label, long_label) #} + + {% endwith %} +
+ {% endfor %} +
diff --git a/kfet/cms/templates/kfet/permissions/collection_member_permissions_formset.html b/kfet/cms/templates/kfet/permissions/collection_member_permissions_formset.html new file mode 100644 index 00000000..c5faf2af --- /dev/null +++ b/kfet/cms/templates/kfet/permissions/collection_member_permissions_formset.html @@ -0,0 +1,42 @@ +{% load i18n %} +{% load formset_tags %} + +
+ +
+ + {{ model_name|title }} +
+ +{{ formset.management_form }} + +{% if formset.non_form_errors %} + {{ formset.non_form_errors }} +{% endif %} + +{% with form_tpl="kfet/permissions/collection_member_permissions_form.html" %} + +
+ {% for form in formset %} + {% include form_tpl with form=form formset=formset only %} + {% endfor %} +
+ + + +{% endwith %} + +
+ + diff --git a/kfet/cms/templates/kfet/permissions/page_permissions_form.html b/kfet/cms/templates/kfet/permissions/page_permissions_form.html new file mode 100644 index 00000000..6b778861 --- /dev/null +++ b/kfet/cms/templates/kfet/permissions/page_permissions_form.html @@ -0,0 +1,20 @@ +{% load widget_tweaks %} +{% load kfet_extras %} + +
+ {{ form.non_field_errors }} + {% if form.DELETE %}{% include "kfet/formset_form_actions.html" %}{% endif %} +
+ {{ form.page|add_class:"form-control input-sm" }} +
+ {% for option in form.permission_types %} +
+ {% with p_type=formset.permission_types|get:forloop.counter0 %} + {# p_type format: (identifier, short_label, long_label) #} + + {% endwith %} +
+ {% endfor %} +
diff --git a/kfet/cms/templates/kfet/permissions/page_permissions_formset.html b/kfet/cms/templates/kfet/permissions/page_permissions_formset.html new file mode 100644 index 00000000..03770491 --- /dev/null +++ b/kfet/cms/templates/kfet/permissions/page_permissions_formset.html @@ -0,0 +1,42 @@ +{% load i18n %} +{% load formset_tags %} + +
+ +
+ + {% trans "Pages" %} +
+ +{{ formset.management_form }} + +{% if formset.non_form_errors %} + {{ formset.non_form_errors }} +{% endif %} + +{% with form_tpl="kfet/permissions/page_permissions_form.html" %} + +
+ {% for form in formset %} + {% include form_tpl with form=form formset=formset only %} + {% endfor %} +
+ + + +{% endwith %} + +
+ + diff --git a/kfet/cms/utils.py b/kfet/cms/utils.py new file mode 100644 index 00000000..21c44547 --- /dev/null +++ b/kfet/cms/utils.py @@ -0,0 +1,35 @@ +from django.apps import apps + +from wagtail.wagtailcore.models import Collection, Page + + +def get_kfet_root_page(): + """ + Returns the K-Fêt root page, or 'None' if it does not exist. + """ + from .models import KFetPage + return KFetPage.objects.first() + + +def get_kfet_root_collection(): + """ + Returns the K-Fêt root collection, or 'None' if it does not exist. + """ + return Collection.objects.filter(name='K-Fêt').first() + + +def get_page_model_names(): + """ + Returns all model names of `Page` subclasses. + + This uses the django apps registry (instead of `ContentType.objects.all()`) + in order to be usuable even before migrations are applied. E.g. this can be + used in `Field.__init__`. + + Note these model names are the same in `model` attribute of `ContentType` + objects. + """ + return [ + model._meta.model_name + for model in apps.get_models() if issubclass(model, Page) + ] diff --git a/kfet/cms/views.py b/kfet/cms/views.py new file mode 100644 index 00000000..4cb719a8 --- /dev/null +++ b/kfet/cms/views.py @@ -0,0 +1,74 @@ +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailadmin.forms import ( + BaseGroupCollectionMemberPermissionFormSet, +) +from wagtail.wagtailcore import hooks +from wagtail.wagtailcore.models import Collection, Page + +from .utils import get_kfet_root_collection, get_kfet_root_page + + +def get_kfetcms_group_formview_extra(): + # Prevents querying the database too soon. + from .forms import ( + CmsGroupForm, SnippetsCmsGroupForm, prepare_page_permissions_formset, + prepare_collection_member_permissions_formset, + ) + forms = [] + + # Misc cms-related permissions. + forms.append((CmsGroupForm, {'prefix': 'kfetcms'})) + + # Snippets permissions. + forms.append((SnippetsCmsGroupForm, {'prefix': 'kfetcms-snippets'})) + + # Setup PagePermissionsFormSet for kfet root page descendants only. + + root_page = get_kfet_root_page() + + if root_page: + pages = Page.objects.descendant_of(root_page, inclusive=True) + forms.append( + (prepare_page_permissions_formset(pages=pages), {}) + ) + + # Setup CollectionPermissions for kfet root collection descendants only. + + root_collection = get_kfet_root_collection() + + if root_collection: + collections = Collection.objects.descendant_of( + root_collection, inclusive=True) + + # Retrieve forms based on CollectionMemberPermissionFormSet displayed + # by Wagtail admin site. + # http://docs.wagtail.io/en/stable/reference/hooks.html#register-group-permission-panel + collectionmember_form_classes = [] + for fn in hooks.get_hooks('register_group_permission_panel'): + form_cls = fn() + if issubclass( + form_cls, + BaseGroupCollectionMemberPermissionFormSet + ): + collectionmember_form_classes.append(form_cls) + + # Apply choices limit. + for form_cls in collectionmember_form_classes: + forms.append(( + prepare_collection_member_permissions_formset( + form_cls, collections=collections), + {}, + )) + + # The 'extra' definition of kfetcms. + + extra = { + 'title': _("Site"), + 'description': _( + "Permissions liées aux pages à contenu libre." + ), + 'form_classes': forms, + } + + return extra diff --git a/kfet/config.py b/kfet/config.py index 76da5a79..c8470093 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -1,9 +1,15 @@ # -*- coding: utf-8 -*- +from datetime import timedelta +from decimal import Decimal +from django import forms from django.core.exceptions import ValidationError from django.db import models from djconfig import config +from djconfig.forms import ConfigForm + +from .models import Account class KFetConfig(object): @@ -19,8 +25,8 @@ class KFetConfig(object): if key == 'subvention_cof': # Allows accessing to the reduction as a subvention # Other reason: backward compatibility - reduction_mult = 1 - self.reduction_cof/100 - return (1/reduction_mult - 1) * 100 + reduction_mult = 1 - self.reduction_cof / 100 + return (1 / reduction_mult - 1) * 100 return getattr(config, self._get_dj_key(key)) def list(self): @@ -30,8 +36,6 @@ class KFetConfig(object): (key, value) for each configuration entry as list. """ - # prevent circular imports - from kfet.forms import KFetConfigForm return [(field.label, getattr(config, name), ) for name, field in KFetConfigForm.base_fields.items()] @@ -46,9 +50,6 @@ class KFetConfig(object): Config entries are updated to given values. """ - # prevent circular imports - from kfet.forms import KFetConfigForm - # get old config new_cfg = KFetConfigForm().initial # update to new config @@ -69,3 +70,38 @@ class KFetConfig(object): kfet_config = KFetConfig() + + +class KFetConfigForm(ConfigForm): + + kfet_reduction_cof = forms.DecimalField( + label='Réduction COF', initial=Decimal('20'), + max_digits=6, decimal_places=2, + help_text="Réduction, à donner en pourcentage, appliquée lors d'un " + "achat par un-e membre du COF sur le montant en euros.", + ) + kfet_addcost_amount = forms.DecimalField( + label='Montant de la majoration (en €)', initial=Decimal('0'), + required=False, + max_digits=6, decimal_places=2, + ) + kfet_addcost_for = forms.ModelChoiceField( + label='Destinataire de la majoration', initial=None, required=False, + help_text='Laissez vide pour désactiver la majoration.', + queryset=(Account.objects + .select_related('cofprofile', 'cofprofile__user') + .all()), + ) + kfet_overdraft_duration = forms.DurationField( + label='Durée du découvert autorisé par défaut', + initial=timedelta(days=1), + ) + kfet_overdraft_amount = forms.DecimalField( + label='Montant du découvert autorisé par défaut (en €)', + initial=Decimal('20'), + max_digits=6, decimal_places=2, + ) + kfet_cancel_duration = forms.DurationField( + label='Durée pour annuler une commande sans mot de passe', + initial=timedelta(minutes=5), + ) diff --git a/kfet/forms.py b/kfet/forms.py index 963e4254..b9d7970c 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- - -from datetime import timedelta from decimal import Decimal from django import forms @@ -9,16 +7,12 @@ from django.contrib.auth.models import User from django.forms import modelformset_factory from django.utils import timezone -from djconfig.forms import ConfigForm - from kfet.models import ( Account, Checkout, Article, OperationGroup, Operation, CheckoutStatement, ArticleCategory, AccountNegative, Transfer, TransferGroup, Supplier) from gestioncof.models import CofProfile -from .auth.forms import UserGroupForm # noqa - # ----- # Widgets @@ -369,46 +363,6 @@ class AddcostForm(forms.Form): super(AddcostForm, self).clean() -# ----- -# Settings forms -# ----- - - -class KFetConfigForm(ConfigForm): - - kfet_reduction_cof = forms.DecimalField( - label='Réduction COF', initial=Decimal('20'), - max_digits=6, decimal_places=2, - help_text="Réduction, à donner en pourcentage, appliquée lors d'un " - "achat par un-e membre du COF sur le montant en euros.", - ) - kfet_addcost_amount = forms.DecimalField( - label='Montant de la majoration (en €)', initial=Decimal('0'), - required=False, - max_digits=6, decimal_places=2, - ) - kfet_addcost_for = forms.ModelChoiceField( - label='Destinataire de la majoration', initial=None, required=False, - help_text='Laissez vide pour désactiver la majoration.', - queryset=(Account.objects - .select_related('cofprofile', 'cofprofile__user') - .all()), - ) - kfet_overdraft_duration = forms.DurationField( - label='Durée du découvert autorisé par défaut', - initial=timedelta(days=1), - ) - kfet_overdraft_amount = forms.DecimalField( - label='Montant du découvert autorisé par défaut (en €)', - initial=Decimal('20'), - max_digits=6, decimal_places=2, - ) - kfet_cancel_duration = forms.DurationField( - label='Durée pour annuler une commande sans mot de passe', - initial=timedelta(minutes=5), - ) - - class FilterHistoryForm(forms.Form): checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all()) accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all()) diff --git a/kfet/migrations/0054_delete_settings.py b/kfet/migrations/0054_delete_settings.py index 80ee1d24..aa826637 100644 --- a/kfet/migrations/0054_delete_settings.py +++ b/kfet/migrations/0054_delete_settings.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion -from kfet.forms import KFetConfigForm +from kfet.config import KFetConfigForm def adapt_settings(apps, schema_editor): diff --git a/kfet/migrations/0062_change_models_opts.py b/kfet/migrations/0062_change_models_opts.py new file mode 100644 index 00000000..a8b974cb --- /dev/null +++ b/kfet/migrations/0062_change_models_opts.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0061_add_perms_config'), + ] + + operations = [ + migrations.AlterModelOptions( + name='account', + options={ + 'permissions': ( + ('is_team', "Membre de l'équipe"), + ('manage_addcosts', 'Gérer les majorations'), + ( + 'edit_balance_account', + "Modifier la balance d'un compte" + ), + ( + 'change_account_password', + "Modifier le mot de passe d'une personne de l'équipe" + ), + ( + 'special_add_account', + 'Créer un compte avec une balance initiale' + ), + ('can_force_close', 'Fermer manuellement la K-Fêt') + ), + 'default_permissions': ('add', 'change'), + 'verbose_name_plural': 'Comptes', + 'verbose_name': 'Compte', + }, + ), + migrations.AlterModelOptions( + name='accountnegative', + options={ + 'default_permissions': ('view', 'change',), + 'verbose_name_plural': 'Comptes en négatif', + 'verbose_name': 'Compte en négatif', + }, + ), + migrations.AlterModelOptions( + name='article', + options={ + 'default_permissions': ('add', 'change'), + 'verbose_name_plural': 'Articles', + 'verbose_name': 'Article', + }, + ), + migrations.AlterModelOptions( + name='articlecategory', + options={ + 'default_permissions': ('change',), + 'verbose_name_plural': "Catégories d'articles", + 'verbose_name': "Catégorie d'articles", + }, + ), + migrations.AlterModelOptions( + name='articlerule', + options={ + 'default_permissions': (), + }, + ), + migrations.AlterModelOptions( + name='checkout', + options={ + 'default_permissions': ('add', 'change'), + 'ordering': ['-valid_to'], + 'verbose_name_plural': 'Caisses', + 'verbose_name': 'Caisse', + }, + ), + migrations.AlterModelOptions( + name='checkoutstatement', + options={ + 'default_permissions': ('add', 'change'), + 'verbose_name_plural': 'Relevés de caisses', + 'verbose_name': 'Relevé de caisse', + }, + ), + migrations.AlterModelOptions( + name='checkouttransfer', + options={ + 'default_permissions': (), + }, + ), + migrations.AlterModelOptions( + name='inventory', + options={ + 'permissions': ( + ( + 'order_to_inventory', + "Générer un inventaire à partir d'une commande" + ), + ), + 'ordering': ['-at'], + 'verbose_name_plural': 'Inventaires', + 'default_permissions': ('add',), + 'verbose_name': 'Inventaire', + }, + ), + migrations.AlterModelOptions( + name='inventoryarticle', + options={ + 'default_permissions': (), + }, + ), + migrations.AlterModelOptions( + name='operation', + options={ + 'permissions': ( + ('perform_deposit', 'Effectuer une charge'), + ( + 'perform_negative_operations', + 'Enregistrer des commandes en négatif' + ), + ( + 'override_frozen_protection', + "Forcer le gel d'un compte" + ), + ( + 'cancel_old_operations', + 'Annuler des commandes non récentes' + ), + ( + 'perform_commented_operations', + 'Enregistrer des commandes avec commentaires' + ), + ), + 'default_permissions': (), + 'verbose_name_plural': 'Opérations', + 'verbose_name': 'Opération', + }, + ), + migrations.AlterModelOptions( + name='operationgroup', + options={ + 'default_permissions': (), + }, + ), + migrations.AlterModelOptions( + name='order', + options={ + 'default_permissions': ('add',), + 'ordering': ['-at'], + 'verbose_name_plural': 'Commandes', + 'verbose_name': 'Commande', + }, + ), + migrations.AlterModelOptions( + name='orderarticle', + options={ + 'default_permissions': (), + }, + ), + migrations.AlterModelOptions( + name='supplier', + options={ + 'default_permissions': ('change',), + 'verbose_name_plural': 'Fournisseurs', + 'verbose_name': 'Fournisseur', + }, + ), + migrations.AlterModelOptions( + name='supplierarticle', + options={ + 'default_permissions': (), + }, + ), + migrations.AlterModelOptions( + name='transfer', + options={ + 'default_permissions': ('add',), + 'verbose_name_plural': 'Transferts', + 'verbose_name': 'Transfert', + }, + ), + migrations.AlterModelOptions( + name='transfergroup', + options={ + 'default_permissions': (), + }, + ), + ] diff --git a/kfet/migrations/0063_update_permissions.py b/kfet/migrations/0063_update_permissions.py new file mode 100644 index 00000000..f9562370 --- /dev/null +++ b/kfet/migrations/0063_update_permissions.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations +from django.db.models import Q + + +def update_permissions(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + Account = apps.get_model('kfet', 'Account') + + # If `kfet.is_team` permission exists, rename it. + + Permission.objects.filter( + content_type__app_label='kfet', + content_type__model='account', + codename='is_team', + ).update(name="Membre de l'équipe") + + # If `kfet.view_negs` permission exists, move it as + # `kfet.view_accountnegative`. + + try: + view_negs_p = Permission.objects.get( + content_type__app_label='kfet', + content_type__model='accountnegative', + codename='view_negs', + ) + except Permission.DoesNotExist: + pass + else: + # Avoid failure due to unicity constraint if migrations were partially + # applied. + # Because `view_negs` still exists here, it should be safe to consider + # that nothing uses `view_accountnegative` so that it can be deleted. + Permission.objects.filter( + content_type__app_label='kfet', + content_type__model='accountnegative', + codename='view_accountnegative', + ).delete() + + view_negs_p.codename = 'view_accountnegative' + view_negs_p.name = 'Can view Compte en négatif' + view_negs_p.save() + + # Delete unused permissions. + + to_delete = { + 'account': ['delete_account'], + 'accountnegative': [ + 'add_accountnegative', 'delete_accountnegative', 'view_negs'], + 'article': ['delete_article'], + 'articlecategory': ['add_articlecategory', 'delete_articlecategory'], + 'articlerule': '__all__', + 'checkout': ['delete_checkout'], + 'checkoutstatement': ['delete_checkoutstatement'], + 'checkouttransfer': '__all__', + 'inventory': ['change_inventory', 'delete_inventory'], + 'inventoryarticle': '__all__', + 'operation': ['add_operation', 'change_operation', 'delete_operation'], + 'operationgroup': '__all__', + 'order': ['change_order', 'delete_order'], + 'orderarticle': '__all__', + 'supplier': ['add_supplier', 'delete_supplier'], + 'supplierarticle': '__all__', + 'transfer': ['change_transfer', 'delete_transfer'], + 'transfergroup': '__all__', + } + + to_delete_q = Q() + + for model_name, codenames in to_delete.items(): + if codenames == '__all__': + to_delete_q |= Q(content_type__model=model_name) + else: + to_delete_q |= Q( + content_type__model=model_name, + codename__in=codenames, + ) + + to_delete_q &= Q(content_type__app_label='kfet') + + Permission.objects.filter(to_delete_q).delete() + + +class Migration(migrations.Migration): + """ + Data migration which performs permissions cleaning. + """ + dependencies = [ + ('kfet', '0062_change_models_opts'), + ('auth', '0006_require_contenttypes_0002'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.RunPython(update_permissions), + ] diff --git a/kfet/models.py b/kfet/models.py index 4d377b98..bd159cba 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -17,7 +17,6 @@ import re from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken, Group, Permission # noqa -from .config import kfet_config from .utils import to_ukf def choices_length(choices): @@ -85,9 +84,11 @@ class Account(models.Model): blank = True, null = True, default = None) class Meta: + verbose_name = _("Compte") + verbose_name_plural = _("Comptes") + default_permissions = ('add', 'change') permissions = ( - ('is_team', 'Is part of the team'), - ('manage_perms', 'Gérer les permissions K-Fêt'), + ('is_team', "Membre de l'équipe"), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', @@ -102,6 +103,11 @@ class Account(models.Model): def __str__(self): return '%s (%s)' % (self.trigramme, self.name) + def get_absolute_url(self): + return reverse('kfet.account.read', kwargs={ + 'trigramme': self.trigramme, + }) + # Propriétés pour accéder aux attributs de cofprofile et user @property def user(self): @@ -168,6 +174,7 @@ class Account(models.Model): return data def perms_to_perform_operation(self, amount): + from .config import kfet_config overdraft_duration_max = kfet_config.overdraft_duration overdraft_amount_max = kfet_config.overdraft_amount perms = set() @@ -332,12 +339,13 @@ class AccountNegative(models.Model): comment = models.CharField("commentaire", max_length=255, blank=True) class Meta: - permissions = ( - ('view_negs', 'Voir la liste des négatifs'), - ) + verbose_name = _("Compte en négatif") + verbose_name_plural = _("Comptes en négatif") + default_permissions = ('view', 'change') @property def until_default(self): + from .config import kfet_config return self.start + kfet_config.overdraft_duration @@ -357,7 +365,10 @@ class Checkout(models.Model): return reverse('kfet.checkout.read', kwargs={'pk': self.pk}) class Meta: + verbose_name = _("Caisse") + verbose_name_plural = _("Caisses") ordering = ['-valid_to'] + default_permissions = ('add', 'change') def __str__(self): return self.name @@ -372,6 +383,10 @@ class CheckoutTransfer(models.Model): amount = models.DecimalField( max_digits = 6, decimal_places = 2) + class Meta: + default_permissions = () + + @python_2_unicode_compatible class CheckoutStatement(models.Model): by = models.ForeignKey( @@ -410,6 +425,11 @@ class CheckoutStatement(models.Model): "montant des chèques", default=0, max_digits=6, decimal_places=2) + class Meta: + verbose_name = _("Relevé de caisse") + verbose_name_plural = _("Relevés de caisses") + default_permissions = ('add', 'change') + def __str__(self): return '%s %s' % (self.checkout, self.at) @@ -450,6 +470,11 @@ class ArticleCategory(models.Model): "appliquée aux articles de " "cette catégorie.") + class Meta: + verbose_name = _("Catégorie d'articles") + verbose_name_plural = _("Catégories d'articles") + default_permissions = ('change',) + def __str__(self): return self.name @@ -486,6 +511,11 @@ class Article(models.Model): "capacité du contenant", blank = True, null = True, default = None) + class Meta: + verbose_name = _("Article") + verbose_name_plural = _("Articles") + default_permissions = ('add', 'change') + def __str__(self): return '%s - %s' % (self.category.name, self.name) @@ -505,6 +535,10 @@ class ArticleRule(models.Model): related_name = "rule_to") ratio = models.PositiveSmallIntegerField() + class Meta: + default_permissions = () + + class Inventory(models.Model): articles = models.ManyToManyField( Article, @@ -521,7 +555,10 @@ class Inventory(models.Model): blank = True, null = True, default = None) class Meta: + verbose_name = _("Inventaire") + verbose_name_plural = _("Inventaires") ordering = ['-at'] + default_permissions = ('add',) permissions = ( ('order_to_inventory', "Générer un inventaire à partir d'une commande"), ) @@ -536,6 +573,9 @@ class InventoryArticle(models.Model): stock_new = models.IntegerField() stock_error = models.IntegerField(default = 0) + class Meta: + default_permissions = () + def save(self, *args, **kwargs): # S'il s'agit d'un inventaire provenant d'une livraison, il n'y a pas # d'erreur @@ -557,6 +597,11 @@ class Supplier(models.Model): phone = models.CharField(_("téléphone"), max_length=20, blank=True) comment = models.TextField(_("commentaire"), blank=True) + class Meta: + verbose_name = _("Fournisseur") + verbose_name_plural = _("Fournisseurs") + default_permissions = ('change',) + def __str__(self): return self.name @@ -577,6 +622,9 @@ class SupplierArticle(models.Model): max_digits = 7, decimal_places = 4, blank = True, null = True, default = None) + class Meta: + default_permissions = () + class Order(models.Model): supplier = models.ForeignKey( Supplier, on_delete = models.PROTECT, @@ -590,7 +638,10 @@ class Order(models.Model): max_digits = 6, decimal_places = 2, default = 0) class Meta: + verbose_name = _("Commande") + verbose_name_plural = _("Commandes") ordering = ['-at'] + default_permissions = ('add',) class OrderArticle(models.Model): order = models.ForeignKey( @@ -600,6 +651,9 @@ class OrderArticle(models.Model): quantity_ordered = models.IntegerField() quantity_received = models.IntegerField(default = 0) + class Meta: + default_permissions = () + class TransferGroup(models.Model): at = models.DateTimeField(default=timezone.now) # Optional @@ -611,6 +665,9 @@ class TransferGroup(models.Model): related_name = "+", blank = True, null = True, default = None) + class Meta: + default_permissions = () + class Transfer(models.Model): group = models.ForeignKey( @@ -631,6 +688,11 @@ class Transfer(models.Model): canceled_at = models.DateTimeField( null=True, blank=True, default=None) + class Meta: + verbose_name = _("Transfert") + verbose_name_plural = _("Transferts") + default_permissions = ('add',) + def __str__(self): return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount) @@ -656,6 +718,9 @@ class OperationGroup(models.Model): related_name = "+", blank = True, null = True, default = None) + class Meta: + default_permissions = () + def __str__(self): return ', '.join(map(str, self.opes.all())) @@ -706,6 +771,9 @@ class Operation(models.Model): blank=True, null=True, default=None) class Meta: + verbose_name = _("Opération") + verbose_name_plural = _("Opérations") + default_permissions = () permissions = ( ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', diff --git a/kfet/static/kfet/css/base/forms.css b/kfet/static/kfet/css/base/forms.css new file mode 100644 index 00000000..545c9d40 --- /dev/null +++ b/kfet/static/kfet/css/base/forms.css @@ -0,0 +1,52 @@ +.extra-form { + margin-bottom: 15px; +} + + +/* Checkbox select multiple field */ + +.checkbox-select-multiple > ul, +.checkbox-select-multiple > ul > li > ul { + padding-left: 0; + list-style-type: none; +} + +.checkbox-select-multiple label { + font-weight: normal; +} + + +/* Permissions field */ + +.permissions-field > ul { + font-weight: bold; +} + +.permissions-field > ul > li { + margin-bottom: 15px; +} + +.permissions-field > ul > li > ul { + display: flex; + flex-flow: row wrap; + + margin-top: 10px; + padding: 5px 10px 0; + + border: 1px solid #CCC; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); +} + +.permissions-field > ul > li > ul > li { + float: left; + flex: 0 1 100%; +} + +.permissions-field > ul > li > ul > li:not(:last-child) { + margin-right: 15px; +} + +@media (min-width: 768px) { + .permissions-field > ul > li > ul > li { flex: 0 1 auto; } +} diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 8e28cce0..bf9bf629 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -4,10 +4,12 @@ /* Libs customizations */ @import url("libs/jconfirm-kfet.css"); @import url("libs/multiple-select-kfet.css"); +@import url("libs/formset-kfet.css"); /* Base */ @import url("base/misc.css"); @import url("base/buttons.css"); +@import url("base/forms.css"); /* Blocks */ @import url("base/main.css"); @@ -35,6 +37,11 @@ font-weight: bold; } +.header small { + color: #FFF; + opacity: 0.95; +} + .nopadding { padding: 0 !important; } @@ -296,13 +303,6 @@ thead .tooltip { } -/* Checkbox select multiple */ - -.checkbox-select-multiple label { - font-weight: normal; - margin-bottom: 0; -} - /* Statement creation */ .statement-create-summary table { diff --git a/kfet/static/kfet/css/libs/formset-kfet.css b/kfet/static/kfet/css/libs/formset-kfet.css new file mode 100644 index 00000000..d3a0463d --- /dev/null +++ b/kfet/static/kfet/css/libs/formset-kfet.css @@ -0,0 +1,36 @@ +[data-formset-form] { + margin-bottom: 5px; + padding: 10px; + + border: 1px solid #CCC; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + + transition: border 0.3s, color 0.3s; +} + +@media (min-width: 768px) { + [data-formset-form] > :not(:last-child) { + margin-right: 15px; + } + [data-formset-form] .form-actions { + float: right; + margin-left: 15px; + margin-right: 0; + } +} + +[data-formset-add] { + position: relative; + top: -5px; + right: 0; +} + +[data-formset-form-deleted] { + background: repeating-linear-gradient( + 45deg, + #e9afb9, #e9afb9 2px, transparent 2px, transparent 12px + ); + border: 1px solid #e9afb9; + color: #999; +} diff --git a/kfet/templates/kfet/account.html b/kfet/templates/kfet/account.html index c4147b14..d9abafa3 100644 --- a/kfet/templates/kfet/account.html +++ b/kfet/templates/kfet/account.html @@ -1,4 +1,5 @@ {% extends "kfet/base_col_2.html" %} +{% load i18n %} {% block title %}Comptes{% endblock %} {% block header-title %}Comptes{% endblock %} @@ -22,11 +23,11 @@ - {% if perms.kfet.manage_perms %} - Permissions + {% if perms.kfetauth.view_group %} + {% trans "Permissions" %} {% endif %} - {% if perms.kfet.view_negs %} + {% if perms.kfet.view_accountnegative %} Négatifs {% endif %} diff --git a/kfet/templates/kfet/account_group.html b/kfet/templates/kfet/account_group.html deleted file mode 100644 index 6663bc0e..00000000 --- a/kfet/templates/kfet/account_group.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "kfet/base_col_2.html" %} - -{% block title %}Groupes de comptes{% endblock %} -{% block header-title %}Groupes de comptes{% endblock %} - -{% block fixed %} - - - -{% endblock %} - -{% block main %} - -{% for group in groups %} -
-
- {{ group.name }} - -
-
-

Comptes

-
- -
-

Permissions

-
- {% regroup group.permissions.all by content_type as grouped_perms %} -
    - {% for perms_group in grouped_perms %} -
  • - {{ perms_group.grouper|title }} -
      - {% for perm in perms_group.list %} -
    • {{ perm.name }}
    • - {% endfor %} -
    -
  • - {% endfor %} -
-
-
-
-{% endfor %} - -{% endblock %} diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html deleted file mode 100644 index f0581b42..00000000 --- a/kfet/templates/kfet/account_group_form.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'kfet/base_form.html' %} - -{% block title %}Permissions - Édition{% endblock %} -{% block header-title %}Modification des permissions{% endblock %} - -{% block main %} - -{% include "kfet/form_full_snippet.html" with authz=perms.kfet.manage_perms submit_text="Enregistrer" %} - -{% endblock %} diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index 8f90f459..77733a07 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -24,6 +24,7 @@ + diff --git a/kfet/templates/kfet/form_field_base_snippet.html b/kfet/templates/kfet/form_field_base_snippet.html new file mode 100644 index 00000000..7916f4dd --- /dev/null +++ b/kfet/templates/kfet/form_field_base_snippet.html @@ -0,0 +1,19 @@ +{% load widget_tweaks %} + +{% with widget=field.field.widget %} + +{% if field|widget_type == "checkboxselectmultiple" %} +
+ {{ field }} +
+{% elif field|widget_type == "checkboxinput" %} +
+ +
+{% else %} + {{ field|add_class:'form-control' }} +{% endif %} + +{% endwith %} diff --git a/kfet/templates/kfet/form_field_snippet.html b/kfet/templates/kfet/form_field_snippet.html index 6a4efa16..8177b18a 100644 --- a/kfet/templates/kfet/form_field_snippet.html +++ b/kfet/templates/kfet/form_field_snippet.html @@ -1,26 +1,24 @@ {% load widget_tweaks %}
- -
- {% if field|widget_type == "checkboxselectmultiple" %} -
    - {% for choice in field %} -
  • - -
  • - {% endfor %} -
- {% else %} - {{ field|add_class:'form-control' }} - {% endif %} + {% if not field.label %} + {% elif field|widget_type == "checkboxinput" %} + {# label is displayed along the checkbox #} + {% else %} + + {% endif %} + +
+ + {% include "kfet/form_field_base_snippet.html" with field=field %} + {% if field.errors %} {{ field.errors }} {% endif %} {% if field.help_text %} - {{ field.help_text }} + {{ field.help_text|safe }} {% endif %}
diff --git a/kfet/templates/kfet/form_full_snippet.html b/kfet/templates/kfet/form_full_snippet.html index 79df0cf6..cf834031 100644 --- a/kfet/templates/kfet/form_full_snippet.html +++ b/kfet/templates/kfet/form_full_snippet.html @@ -1,4 +1,4 @@ -
+ {% csrf_token %} {% include "kfet/form_snippet.html" %} {% if not authz %} diff --git a/kfet/templates/kfet/form_snippet.html b/kfet/templates/kfet/form_snippet.html index 2f6d9c7c..9f48cc35 100644 --- a/kfet/templates/kfet/form_snippet.html +++ b/kfet/templates/kfet/form_snippet.html @@ -1,3 +1,5 @@ -{% for field in form %} - {% include 'kfet/form_field_snippet.html' with field=field %} -{% endfor %} +
+ {% for field in form %} + {% include 'kfet/form_field_snippet.html' with field=field %} + {% endfor %} +
diff --git a/kfet/templates/kfet/form_submit_snippet.html b/kfet/templates/kfet/form_submit_snippet.html index fba168da..918153fa 100644 --- a/kfet/templates/kfet/form_submit_snippet.html +++ b/kfet/templates/kfet/form_submit_snippet.html @@ -1,5 +1,11 @@ -
-
- +{% load i18n %} + +{% trans "Enregistrer" as default_value %} + +
+
+
+ +
diff --git a/kfet/templates/kfet/formset_form_actions.html b/kfet/templates/kfet/formset_form_actions.html new file mode 100644 index 00000000..01bafaee --- /dev/null +++ b/kfet/templates/kfet/formset_form_actions.html @@ -0,0 +1,12 @@ +{% load i18n %} +{% load widget_tweaks %} + +
+ {{ form.DELETE|add_class:"hide" }} + + +
diff --git a/kfet/templatetags/dictionary_extras.py b/kfet/templatetags/dictionary_extras.py deleted file mode 100644 index fafaad8d..00000000 --- a/kfet/templatetags/dictionary_extras.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.template.defaulttags import register - -@register.filter -def get_item(dictionary, key): - return dictionary.get(key) diff --git a/kfet/templatetags/kfet_extras.py b/kfet/templatetags/kfet_extras.py new file mode 100644 index 00000000..79c6b481 --- /dev/null +++ b/kfet/templatetags/kfet_extras.py @@ -0,0 +1,8 @@ +from django.template.defaulttags import register + + +@register.filter +def get(o, key): + if hasattr(o, 'get'): + return o.get(key) + return o[int(key)] diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py index ea132acd..9cb7ad1a 100644 --- a/kfet/tests/test_models.py +++ b/kfet/tests/test_models.py @@ -12,6 +12,16 @@ class AccountTests(TestCase): self.account = Account(trigramme='000') self.account.save({'username': 'user'}) + def test_get_absolute_url(self): + self.assertEqual( + self.account.get_absolute_url(), '/k-fet/accounts/000') + + account_space = Account(trigramme=' ') + account_space.save({'username': 'space'}) + + self.assertEqual( + account_space.get_absolute_url(), '/k-fet/accounts/%20%20%20') + def test_password(self): self.account.change_pwd('anna') self.account.save() diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 3565e79f..922312d6 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -7,8 +7,6 @@ from django.core.urlresolvers import reverse from django.test import Client, TestCase from django.utils import timezone -from kfet.auth.models import Group - from ..config import kfet_config from ..models import ( Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory, @@ -16,7 +14,7 @@ from ..models import ( SupplierArticle, Transfer, TransferGroup, ) from .testcases import ViewTestCaseMixin -from .utils import create_team, create_user, get_perms +from .utils import create_team, create_user class AccountListViewTests(ViewTestCaseMixin, TestCase): @@ -336,146 +334,6 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): self.assertForbiddenKfet(r) -class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.group' - url_expected = '/k-fet/accounts/groups' - - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] - - def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), - } - - def setUp(self): - super().setUp() - self.group1 = Group.objects.create(name='K-Fêt - Group1') - self.group2 = Group.objects.create(name='K-Fêt - Group2') - - def test_ok(self): - r = self.client.get(self.url) - self.assertEqual(r.status_code, 200) - - self.assertQuerysetEqual( - r.context['groups'], - map(repr, [self.group1, self.group2]), - ordered=False, - ) - - -class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.group.create' - url_expected = '/k-fet/accounts/groups/new' - - http_methods = ['GET', 'POST'] - - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] - - def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), - } - - @property - def post_data(self): - return { - 'name': 'The Group', - 'permissions': [ - str(self.perms['kfet.is_team'].pk), - str(self.perms['kfet.manage_perms'].pk), - ], - } - - def setUp(self): - super().setUp() - self.perms = get_perms( - 'kfet.is_team', - 'kfet.manage_perms', - ) - - def test_get_ok(self): - r = self.client.get(self.url) - self.assertEqual(r.status_code, 200) - - def test_post_ok(self): - r = self.client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.account.group')) - - group = Group.objects.get(name='The Group') - - self.assertQuerysetEqual( - group.permissions.all(), - map(repr, [ - self.perms['kfet.is_team'], - self.perms['kfet.manage_perms'], - ]), - ordered=False, - ) - - -class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.account.group.update' - - http_methods = ['GET', 'POST'] - - auth_user = 'team1' - auth_forbidden = [None, 'user', 'team'] - - @property - def url_kwargs(self): - return {'pk': self.group.pk} - - @property - def url_expected(self): - return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk) - - def get_users_extra(self): - return { - 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), - } - - @property - def post_data(self): - return { - 'name': 'The Group', - 'permissions': [ - str(self.perms['kfet.is_team'].pk), - str(self.perms['kfet.manage_perms'].pk), - ], - } - - def setUp(self): - super().setUp() - self.perms = get_perms( - 'kfet.is_team', - 'kfet.manage_perms', - ) - self.group = Group.objects.create(name='K-Fêt - Group') - self.group.permissions = self.perms.values() - - def test_get_ok(self): - r = self.client.get(self.url) - self.assertEqual(r.status_code, 200) - - def test_post_ok(self): - r = self.client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.account.group')) - - self.group.refresh_from_db() - - self.assertEqual(self.group.name, 'The Group') - self.assertQuerysetEqual( - self.group.permissions.all(), - map(repr, [ - self.perms['kfet.is_team'], - self.perms['kfet.manage_perms'], - ]), - ordered=False, - ) - - class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): url_name = 'kfet.account.negative' url_expected = '/k-fet/accounts/negatives' @@ -485,7 +343,8 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): def get_users_extra(self): return { - 'team1': create_team('team1', '101', perms=['kfet.view_negs']), + 'team1': create_team('team1', '101', perms=[ + 'kfet.view_accountnegative']), } def setUp(self): diff --git a/kfet/urls.py b/kfet/urls.py index f39299a5..46e5f70c 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -8,8 +8,6 @@ from kfet.decorators import teamkfet_required urlpatterns = [ - url(r'^login/generic$', views.login_generic, - name='kfet.login.generic'), url(r'^history$', views.history, name='kfet.history'), @@ -50,20 +48,8 @@ urlpatterns = [ url(r'^accounts/(?P.{3})/edit$', views.account_update, name='kfet.account.update'), - # Account - Groups - url(r'^accounts/groups$', views.account_group, - name='kfet.account.group'), - url(r'^accounts/groups/new$', - permission_required('kfet.manage_perms') - (views.AccountGroupCreate.as_view()), - name='kfet.account.group.create'), - url(r'^accounts/groups/(?P\d+)/edit$', - permission_required('kfet.manage_perms') - (views.AccountGroupUpdate.as_view()), - name='kfet.account.group.update'), - url(r'^accounts/negatives$', - permission_required('kfet.view_negs') + permission_required('kfet.view_accountnegative') (views.AccountNegativeList.as_view()), name='kfet.account.negative'), @@ -240,6 +226,6 @@ urlpatterns = [ ] urlpatterns += [ - # K-Fêt Open urls - url('^open/', include('kfet.open.urls')), + url(r'^', include('kfet.auth.urls')), + url(r'^open/', include('kfet.open.urls')), ] diff --git a/kfet/utils.py b/kfet/utils.py index 3d06bb0b..9ee0ad46 100644 --- a/kfet/utils.py +++ b/kfet/utils.py @@ -7,11 +7,10 @@ from django.core.serializers.json import DjangoJSONEncoder from channels.channel import Group from channels.generic.websockets import JsonWebsocketConsumer -from .config import kfet_config - def to_ukf(balance, is_cof=False): """Convert euro to UKF.""" + from .config import kfet_config subvention = kfet_config.subvention_cof grant = (1 + subvention / 100) if is_cof else 1 return math.floor(balance * 10 * grant) diff --git a/kfet/views.py b/kfet/views.py index f1dd6834..a0440b3b 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -24,7 +24,9 @@ from django.utils.decorators import method_decorator from gestioncof.models import CofProfile -from kfet.config import kfet_config +from .auth.forms import UserGroupForm + +from kfet.config import KFetConfigForm, kfet_config from kfet.decorators import teamkfet_required from kfet.models import ( Account, Checkout, Article, AccountNegative, @@ -33,14 +35,14 @@ from kfet.models import ( TransferGroup, Transfer, ArticleCategory) from kfet.forms import ( AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm, - UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm, + UserRestrictTeamForm, AccountForm, CofRestrictForm, AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm, KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm, KPsulOperationFormSet, AddcostForm, FilterHistoryForm, TransferFormSet, InventoryArticleForm, OrderArticleForm, - OrderArticleToInventoryForm, CategoryForm, KFetConfigForm + OrderArticleToInventoryForm, CategoryForm, ) from collections import defaultdict from kfet import consumers @@ -50,10 +52,6 @@ import heapq import statistics from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale -from .auth.views import ( # noqa - account_group, login_generic, AccountGroupCreate, AccountGroupUpdate, -) - def put_cleaned_data_in_dict(dict, form): for field in form.cleaned_data: diff --git a/requirements.txt b/requirements.txt index 1591656d..3f885de4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,9 @@ wagtailmenus==2.2.* # Remove this when we switch to Django 1.11 djangorestframework==3.6.4 +# This fork enables restore of forms. +# Original project: https://bitbucket.org/tim_heap/django-formset-js +git+https://bitbucket.org/georgema1982/django-formset-js.git#egg=django-formset-js + # Production tools wheel