Compare commits

...

26 commits

Author SHA1 Message Date
Aurélien Delobelle e3e8608563 Merge branch 'aureplop/kfet-auth' into aureplop/kfet-auth_cms 2019-01-14 22:07:52 +01:00
Martin Pépin 02f1e83420 typos 2017-10-25 20:57:17 +02:00
Aurélien Delobelle 6d90311ae1 Fix tests, force reevaluate field queryset 2017-10-25 20:57:17 +02:00
Aurélien Delobelle a7dbc64e2b Fix SnippetsCmsForm
+ Prevent querying the database from tests too soon.

=> tests pass.
2017-10-25 20:57:17 +02:00
Aurélien Delobelle 94ab754f82 Move group-related views tests to kfetauth tests 2017-10-25 20:26:35 +02:00
Martin Pepin 13f01020f7 Merge branch 'aureplop/fix-test-keepunselecform' into 'aureplop/kfet-auth'
Fix KeepUnselectableForm tests

See merge request !264
2017-10-25 15:48:22 +02:00
Martin Pépin 2b62c3a785 typos 2017-10-25 15:43:35 +02:00
Aurélien Delobelle d36a813e15 Fix tests, force reevaluate field queryset 2017-10-25 14:28:24 +02:00
Aurélien Delobelle 94f21c062a SnippetCmsGroupForm should not clean previous permissions 2017-10-25 03:12:22 +02:00
Aurélien Delobelle e39f88991e Fix migration history 2017-10-24 19:49:52 +02:00
Aurélien Delobelle 03a0c58869 Merge branch 'aureplop/kfet-auth' into aureplop/kfet-auth_cms
+ Move migrations.
2017-10-24 19:48:47 +02:00
Aurélien Delobelle aa405d212a Fix tests…
…due to merge of aureplop/kfet-auth
2017-10-24 19:44:02 +02:00
Aurélien Delobelle c524da22fe Merge branch 'aureplop/kfet-auth' into aureplop/kfet-auth_cms
+ Move migrations.
+ Update tests to use new url names and new permissions.
2017-10-24 19:41:45 +02:00
Aurélien Delobelle 09290131d5 Merge branch 'master' into aureplop/kfet-auth 2017-10-24 19:31:36 +02:00
Aurélien Delobelle 40ceaf411a Merge branch 'master' into aureplop/kfet-auth
- Modify tests of group form-views: using Group model of kfetauth
doesn't add 'K-Fêt' to the group name.
2017-10-24 18:33:38 +02:00
Aurélien Delobelle 097ee44131 Organize migrations to avoid issues with…
…migrations already applied from master.
2017-10-17 16:50:39 +02:00
Aurélien Delobelle 2c76bea1e6 Better display of objects of BasePermissionsField
- Permissions are grouped by content type and displayed under its
verbose_name_plural.
- Default permissions appear before custom ones.
- Use `permissions-field` class to enhance display.
2017-10-17 16:50:39 +02:00
Aurélien Delobelle 07f1a53532 CMS permissions can be managed from group views.
These permissions concern pages, images, documents and access to the
wagtail admin site. Only appropriate elements can be selected: only the
kfet root page and its descendants, same for the kfet root collection
(for images and documents), and kfet snippets (MemberTeam).

Add django-formset-js as dependency to help manipulate formsets.

K-Fêt groups created from "devdata" commands get suitable permissions
for the CMS.
2017-10-17 16:50:39 +02:00
Aurélien Delobelle 82582866b4 Clean forms/views/urls related to kfetauth.Group…
…and it becomes possible to add extra forms/formsets to the create and
update group views.
2017-10-17 16:50:39 +02:00
Aurélien Delobelle 5502c6876a Clean permissions objects
- Define default permissions of kfet models.
- Unused default permissions are deleted.
- `kfet.manage_perms` is now splitted as `kfetauth.(view|add|change)_group` permissions.
2017-10-17 16:49:45 +02:00
Aurélien Delobelle df7594a105 Move KFetConfigForm to kfet.config
Import in `ready` method of kfet app config of `kfet.forms` may be
annoying because it starts executing `__init__` methods of fields.
Causing failures if these methods does DB calls, as `ready` may be
called before applying migrations.
2017-10-12 13:53:48 +02:00
Aurélien Delobelle e6fab703ee Use convenience imports 2017-10-12 13:42:06 +02:00
Martin Pepin c17ed416c4 Merge branch 'aureplop/kfet-auth_perms' into 'aureplop/kfet-auth'
Cleaner use of Group in kfet app

See merge request !257
2017-10-12 11:30:44 +02:00
Aurélien Delobelle 085a068020 Merge branch 'aureplop/kfet-auth' into aureplop/kfet-auth_perms 2017-10-12 11:07:16 +02:00
Aurélien Delobelle 8ea5775d61 Add test for callable queryset with Unselectable… 2017-09-30 02:14:01 +02:00
Aurélien Delobelle ded824bddd Cleaner use of Group in kfet app
KFetGroup model
- Provides a distinction from non-kfet Groups.
- Convert code appropriately.
- Initially filled from Groups containing K-Fêt (this was the previous
distinction) in the kfetauth.0002 migration.

Permission proxy model (kfetauth app)
- Proxy of the django.contrib.auth Permission model.
- Adds the 'kfet' manager which returns only kfet-related permissions.

KeepUnselectableModelFormMixin
- Helps to keep the unselectable items of many-to-many field for
ModelForm.
- 'kfetauth' forms (related to KFetGroup) use this mixin.

Using KFetGroup allows to simplify the 'kfet/account_group_form.html' template.

A bug is also fixed in 'kfet/form_field_snippet.html', which could lead to
prevent field displays if they used CheckboxSelectMultiple widget.
2017-09-29 22:37:30 +02:00
55 changed files with 2283 additions and 520 deletions

View file

@ -69,6 +69,7 @@ INSTALLED_APPS = [
'autocomplete_light',
'captcha',
'django_cas_ng',
'djangoformsetjs',
'bootstrapform',
'kfet',
'kfet.open',

View file

@ -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)

View file

@ -1,20 +1,77 @@
import re
from itertools import groupby
from operator import attrgetter
from django import forms
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
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 KFetPermissionsField(forms.ModelMultipleChoiceField):
class GroupsField(forms.ModelMultipleChoiceField):
def __init__(self, **kwargs):
kwargs.setdefault('queryset', Group.objects.all())
kwargs.setdefault('widget', forms.CheckboxSelectMultiple)
super().__init__(**kwargs)
def __init__(self, *args, **kwargs):
queryset = Permission.objects.filter(
content_type__in=ContentType.objects.filter(app_label="kfet"),
)
super().__init__(
queryset=queryset,
widget=widgets.CheckboxSelectMultiple,
*args, **kwargs
)
def label_from_instance(self, obj):
return obj.name
class BasePermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, **kwargs):
kwargs.setdefault('widget', forms.CheckboxSelectMultiple(attrs={
'field_class': 'permissions-field',
}))
super().__init__(**kwargs)
# 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<p_type>({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):
def __init__(self, **kwargs):
kwargs.setdefault('queryset', Permission.kfetcore.all())
super().__init__(**kwargs)

View file

@ -1,47 +1,27 @@
from django import forms
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from .fields import KFetPermissionsField
from utils.forms import KeepUnselectableModelFormMixin
from .fields import GroupsField, CorePermissionsField
from .models import Group
class GroupForm(forms.ModelForm):
permissions = KFetPermissionsField()
class GroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
permissions = CorePermissionsField(label='', required=False)
def clean_name(self):
name = self.cleaned_data['name']
return 'K-Fêt %s' % name
def clean_permissions(self):
kfet_perms = self.cleaned_data['permissions']
# TODO: With Django >=1.11, the QuerySet method 'difference' can be
# used.
# other_groups = self.instance.permissions.difference(
# self.fields['permissions'].queryset
# )
if self.instance.pk is None:
return kfet_perms
other_perms = self.instance.permissions.exclude(
pk__in=[p.pk for p in self.fields['permissions'].queryset],
)
return list(kfet_perms) + list(other_perms)
keep_unselectable_fields = ['permissions']
class Meta:
model = Group
fields = ['name', 'permissions']
class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'),
label='Statut équipe',
required=False)
class UserGroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
groups = GroupsField(label=_("Statut équipe"), required=False)
def clean_groups(self):
kfet_groups = self.cleaned_data.get('groups')
if self.instance.pk is None:
return kfet_groups
other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return list(kfet_groups) + list(other_groups)
keep_unselectable_fields = ['groups']
class Meta:
model = User

View file

@ -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',),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def existing_groups(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
KFetGroup = apps.get_model('kfetauth', 'Group')
for group in Group.objects.filter(name__icontains='K-Fêt'):
kf_group = KFetGroup(group_ptr=group, pk=group.pk, name=group.name)
kf_group.save()
class Migration(migrations.Migration):
"""
Data migration.
Previously, K-Fêt groups were identified with the presence of 'K-Fêt' in
their name.
"""
dependencies = [
('kfetauth', '0002_create_group_permission_models'),
('auth', '0006_require_contenttypes_0002'),
]
operations = [
migrations.RunPython(existing_groups),
]

View file

@ -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),
]

View file

@ -1,5 +1,11 @@
from django.contrib.auth.models import (
Group as DjangoGroup, Permission as DjangoPermission,
)
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):
@ -15,3 +21,42 @@ class GenericTeamToken(models.Model):
token = models.CharField(max_length=50, unique=True)
objects = GenericTeamTokenManager()
class Meta:
default_permissions = ()
class Group(DjangoGroup):
def give_admin_access(self):
perms = Permission.kfetcore.all()
self.permissions.add(*perms)
def give_staff_access(self):
perms = Permission.kfetcore.filter(
codename__in=['is_team', 'perform_deposit'])
self.permissions.add(*perms)
class Meta:
verbose_name = _("Groupe")
verbose_name_plural = _("Groupes")
default_permissions = ('view', 'add', 'change')
KFET_CORE_APP_LABELS = ['kfet', 'kfetauth']
class CorePermissionManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(
content_type__app_label__in=KFET_CORE_APP_LABELS)
class Permission(DjangoPermission):
kfetcore = CorePermissionManager()
kfetcms = CmsPermissionManager()
class Meta:
proxy = True
verbose_name = _("Permission")
verbose_name_plural = _("Permissions")

View file

@ -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 }}
<small>Modification du groupe</small>
{% endblocktrans %}
{% endif %}
{% endblock %}
{% block main %}
<form action="" method="post" class="group-form">
{% csrf_token %}
{# Base form #}
<div class="form-horizontal">
{% include "kfet/form_snippet.html" with form=form %}
</div>
{# Extra forms #}
{% for extra in extras %}
<h3>
{{ extra.title }}<br>
<small>{{ extra.description }}</small>
</h3>
{% for extra_form in extra.forms %}
<div class="extra-form">
{% 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 %}
</div>
{% endfor %}
{% endfor %}
{% include "kfet/form_submit_snippet.html" %}
</form>
{% endblock %}

View file

@ -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 %}
<div class="buttons">
<a class="btn btn-primary" href="{% url 'kfet.group.create' %}">
<span class="glyphicon glyphicon-plus"></span><span>{% trans "Créer un groupe" %}</span>
</a>
</div>
{% endblock %}
{% block main %}
{% for group in groups %}
<section>
<div class="heading">
{{ group.name }}
<div class="buttons">
<a class="btn btn-default" href="{% url 'kfet.group.update' group.pk %}">
<span class="glyphicon glyphicon-cog"></span><span class="hidden-xs">{% trans "Éditer" %}</span>
</a>
</div>
</div>
<div>
<h3>Comptes</h3>
{% with users=group.user_set.all %}
{% if users %}
<div class="sub-block column-sm-2 column-md-3">
<ul>
{% for user in group.user_set.all %}
{% with kfet_user=user.profile.account_kfet %}
<li>
<a href="{{ kfet_user.get_absolute_url }}">{{ kfet_user }}</a>
</li>
{% endwith %}
{% endfor %}
</ul>
{% else %}
<div class="sub-block">
<p>
{% blocktrans %}
Aucun compte n'est associé à ce groupe. Rendez-vous sur la page
d'édition d'un compte pour l'y ajouter.
{% endblocktrans %}
</p>
</div>
{% endif %}
{% endwith %}
</div>
</section>
{% endfor %}
{% endblock %}

View file

@ -1,76 +1,197 @@
# -*- coding: utf-8 -*-
from unittest import mock
from django.contrib.auth.models import (
AnonymousUser, Group as DjangoGroup, User,
)
from django.contrib.contenttypes.models import ContentType
from django.core import signing
from django.core.urlresolvers import reverse
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
from django.test import RequestFactory, TestCase
from kfet.forms import UserGroupForm
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 .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken
from .models import GenericTeamToken, Group, Permission
from .utils import get_kfet_generic_user
from .views import GenericLoginView
##
# Forms
# Forms and form fields
##
class CorePermissionsFieldTests(TestCase):
def setUp(self):
kf_ct = ContentType.objects.get_for_model(GenericTeamToken)
self.kf_perm1 = Permission.objects.create(
content_type=kf_ct, codename='code1')
self.kf_perm2 = Permission.objects.create(
content_type=kf_ct, codename='code2')
self.kf_perm3 = Permission.objects.create(
content_type=kf_ct, codename='code3')
ot_ct = ContentType.objects.get_for_model(Permission)
self.ot_perm = Permission.objects.create(
content_type=ot_ct, codename='code')
def test_choices(self):
"""
Only K-Fêt permissions are selectable.
"""
field = CorePermissionsField()
for p in [self.kf_perm1, self.kf_perm2, self.kf_perm3]:
self.assertIn(p, field.queryset)
self.assertNotIn(self.ot_perm, field.queryset)
class GroupsFieldTests(TestCase):
def setUp(self):
self.kf_group1 = Group.objects.create(name='KF Group1')
self.kf_group2 = Group.objects.create(name='KF Group2')
self.kf_group3 = Group.objects.create(name='KF Group3')
self.ot_group = DjangoGroup.objects.create(name='OT Group')
def test_choices(self):
"""
Only K-Fêt groups are selectable.
"""
field = GroupsField()
self.assertQuerysetEqual(
field.queryset,
map(repr, [self.kf_group1, self.kf_group2, self.kf_group3]),
ordered=False,
)
class GroupFormTests(TestCase):
def setUp(self):
self.user = User.objects.create(username='user')
self.kf_group = Group.objects.create(name='A Group')
self.kf_perm1 = Permission.kfetcore.get(codename='is_team')
self.kf_perm2 = Permission.kfetcore.get(codename='add_checkout')
ot_ct = ContentType.objects.get_for_model(Permission)
self.ot_perm = Permission.objects.create(
content_type=ot_ct, codename='cool')
def test_creation(self):
from .forms import GroupForm
data = {
'name': 'Another Group',
'permissions': [self.kf_perm1.pk],
}
form = GroupForm(data)
instance = form.save()
self.assertEqual(instance.name, 'Another Group')
self.assertQuerysetEqual(
instance.permissions.all(), map(repr, [self.kf_perm1]))
def test_keep_others(self):
"""
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]
data = {
'name': 'A Group',
'permissions': [str(p.pk) for p in selected],
}
form = GroupForm(data, instance=self.kf_group)
form.save()
self.assertQuerysetEqual(
self.kf_group.permissions.all(),
map(repr, [self.ot_perm] + selected),
ordered=False,
)
class UserGroupFormTests(TestCase):
"""Test suite for UserGroupForm."""
def setUp(self):
# create user
self.user = User.objects.create(username="foo", password="foo")
# create some K-Fêt groups
prefix_name = "K-Fêt "
names = ["Group 1", "Group 2", "Group 3"]
self.kfet_groups = [
Group.objects.create(name=prefix_name+name)
for name in names
]
self.kf_group1 = Group.objects.create(name='KF Group1')
self.kf_group2 = Group.objects.create(name='KF Group2')
self.kf_group3 = Group.objects.create(name='KF Group3')
# create a non-K-Fêt group
self.other_group = Group.objects.create(name="Other group")
def test_choices(self):
"""Only K-Fêt groups are selectable."""
form = UserGroupForm(instance=self.user)
groups_field = form.fields['groups']
self.assertQuerysetEqual(
groups_field.queryset,
[repr(g) for g in self.kfet_groups],
ordered=False,
)
self.ot_group = DjangoGroup.objects.create(name="OT Group")
def test_keep_others(self):
"""User stays in its non-K-Fêt groups."""
user = self.user
"""
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
user.groups.add(self.other_group)
self.user.groups.add(self.ot_group)
# add user to some K-Fêt groups through UserGroupForm
data = {
'groups': [group.pk for group in self.kfet_groups],
}
form = UserGroupForm(data, instance=user)
form.is_valid()
selected = [self.kf_group1, self.kf_group2]
data = {'groups': [str(g.pk) for g in selected]}
form = UserGroupForm(data, instance=self.user)
form.save()
transform = lambda g: g.pk
self.assertQuerysetEqual(
user.groups.all(),
[repr(g) for g in [self.other_group] + self.kfet_groups],
ordered=False,
self.user.groups.all(),
map(transform, [self.ot_group] + selected),
ordered=False, transform=transform,
)
##
# Models
##
class PermissionTests(TestCase):
def test_manager_kfet(self):
"""
'kfet' manager only returns K-Fêt permissions.
"""
kf_ct = ContentType.objects.get_for_model(GenericTeamToken)
kf_perm1 = Permission.objects.create(
content_type=kf_ct, codename='code1')
kf_perm2 = Permission.objects.create(
content_type=kf_ct, codename='code2')
kf_perm3 = Permission.objects.create(
content_type=kf_ct, codename='code3')
self.assertEqual(Permission.kfetcore.get(codename='code1'), kf_perm1)
self.assertEqual(Permission.kfetcore.get(codename='code2'), kf_perm2)
self.assertEqual(Permission.kfetcore.get(codename='code3'), kf_perm3)
ot_ct = ContentType.objects.get_for_model(Permission)
Permission.objects.create(content_type=ot_ct, codename='code')
with self.assertRaises(Permission.DoesNotExist):
Permission.kfetcore.get(codename='code')
##
# K-Fêt generic user object
##
class KFetGenericUserTests(TestCase):
def test_exists(self):
@ -139,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
@ -230,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()
@ -246,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):
"""
@ -269,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
#

18
kfet/auth/urls.py Normal file
View file

@ -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<pk>\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'),
]

View file

@ -1,21 +1,22 @@
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 Group, 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
from .models import GenericTeamToken, Group
User = get_user_model()
class GenericLoginView(View):
@ -104,33 +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
.filter(name__icontains='K-Fêt')
.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 AccountGroupCreate(SuccessMessageMixin, CreateView):
model = Group
template_name = 'kfet/account_group_form.html'
form_class = GroupForm
success_message = 'Nouveau groupe : %(name)s'
success_url = reverse_lazy('kfet.account.group')
_group_formview_extras = None
class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
queryset = Group.objects.filter(name__icontains='K-Fêt')
template_name = 'kfet/account_group_form.html'
form_class = GroupForm
success_message = 'Groupe modifié : %(name)s'
success_url = reverse_lazy('kfet.account.group')
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]
@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,
})
@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,
})

196
kfet/cms/forms.py Normal file
View file

@ -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=_(
"<b>Attention :</b> 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

View file

@ -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
])

View file

@ -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())
)

View file

@ -0,0 +1,20 @@
{% load widget_tweaks %}
{% load kfet_extras %}
<div class="form-inline" data-formset-form>
{{ form.non_field_errors }}
{% if form.DELETE %}{% include "kfet/formset_form_actions.html" %}{% endif %}
<div class="form-group">
{{ form.collection|add_class:"form-control input-sm" }}
</div>
{% for option in form.permissions %}
<div class="checkbox">
{% with p_type=formset.permission_types|get:forloop.counter0 %}
{# p_type format: (identifier, short_label, long_label) #}
<label title="{{ p_type.2 }}">
{{ option.tag }} {{ p_type.1}}
</label>
{% endwith %}
</div>
{% endfor %}
</div>

View file

@ -0,0 +1,42 @@
{% load i18n %}
{% load formset_tags %}
<div data-formset-prefix="{{ formset.prefix }}">
<div class="h5">
<button type="button" class="pull-right btn btn-default btn-sm" data-formset-add>
{% trans "Ajouter pour une autre collection" %}
</button>
<b>{{ model_name|title }}</b>
</div>
{{ formset.management_form }}
{% if formset.non_form_errors %}
<span class="help-block">{{ formset.non_form_errors }}</span>
{% endif %}
{% with form_tpl="kfet/permissions/collection_member_permissions_form.html" %}
<div data-formset-body>
{% for form in formset %}
{% include form_tpl with form=form formset=formset only %}
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
{% include form_tpl with form=formset.empty_form formset=formset only %}
{% endescapescript %}
</script>
{% endwith %}
</div>
<script type="text/javascript">
$( function() {
let $formset_container = $('[data-formset-prefix={{ formset.prefix }}]');
$formset_container.formset();
});
</script>

View file

@ -0,0 +1,20 @@
{% load widget_tweaks %}
{% load kfet_extras %}
<div class="form-inline" data-formset-form>
{{ form.non_field_errors }}
{% if form.DELETE %}{% include "kfet/formset_form_actions.html" %}{% endif %}
<div class="form-group">
{{ form.page|add_class:"form-control input-sm" }}
</div>
{% for option in form.permission_types %}
<div class="checkbox">
{% with p_type=formset.permission_types|get:forloop.counter0 %}
{# p_type format: (identifier, short_label, long_label) #}
<label title="{{ p_type.2 }}">
{{ option.tag }} {{ p_type.1 }}
</label>
{% endwith %}
</div>
{% endfor %}
</div>

View file

@ -0,0 +1,42 @@
{% load i18n %}
{% load formset_tags %}
<div data-formset-prefix="{{ formset.prefix }}">
<div class="h5">
<button type="button" class="pull-right btn btn-default btn-sm" data-formset-add>
{% trans "Ajouter pour une autre page" %}
</button>
<b>{% trans "Pages" %}</b>
</div>
{{ formset.management_form }}
{% if formset.non_form_errors %}
<span class="help-block">{{ formset.non_form_errors }}</span>
{% endif %}
{% with form_tpl="kfet/permissions/page_permissions_form.html" %}
<div data-formset-body>
{% for form in formset %}
{% include form_tpl with form=form formset=formset only %}
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
{% include form_tpl with form=formset.empty_form formset=formset only %}
{% endescapescript %}
</script>
{% endwith %}
</div>
<script type="text/javascript">
$( function() {
let $formset_container = $('[data-formset-prefix={{ formset.prefix }}]');
$formset_container.formset();
});
</script>

35
kfet/cms/utils.py Normal file
View file

@ -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)
]

74
kfet/cms/views.py Normal file
View file

@ -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

View file

@ -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),
)

View file

@ -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())

View file

@ -7,13 +7,15 @@ import random
from datetime import timedelta
from django.utils import timezone
from django.contrib.auth.models import User, Group, Permission, ContentType
from django.contrib.auth.models import User
from django.core.management import call_command
from gestioncof.management.base import MyBaseCommand
from gestioncof.models import CofProfile
from kfet.models import (Account, Checkout, CheckoutStatement, Supplier,
SupplierArticle, Article)
from kfet.models import (
Account, Article, Checkout, CheckoutStatement, Group, Supplier,
SupplierArticle, Article,
)
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
@ -28,22 +30,13 @@ class Command(MyBaseCommand):
# Groupes
# ---
Group.objects.filter(name__icontains='K-Fêt').delete()
group_chef, _ = Group.objects.get_or_create(
name="K-Fêt César")
group_boy, _ = Group.objects.get_or_create(
name="K-Fêt Légionnaire")
group_chef = Group(name="K-Fêt César")
group_boy = Group(name="K-Fêt Légionnaire")
group_chef.save()
group_boy.save()
permissions_chef = Permission.objects.filter(
content_type__in=ContentType.objects.filter(
app_label='kfet'))
permissions_boy = Permission.objects.filter(
codename__in=['is_team', 'perform_deposit'])
group_chef.permissions.add(*permissions_chef)
group_boy.permissions.add(*permissions_boy)
group_chef.give_admin_access()
group_boy.give_staff_access()
# ---
# Comptes

View file

@ -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):

View file

@ -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': (),
},
),
]

View file

@ -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),
]

View file

@ -15,9 +15,8 @@ from datetime import date
import re
from .auth import KFET_GENERIC_TRIGRAMME
from .auth.models import GenericTeamToken # noqa
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',

View file

@ -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; }
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 @@
</a>
</div>
{% if perms.kfet.manage_perms %}
<a class="btn btn-primary" href="{% url 'kfet.account.group' %}">Permissions</a>
{% if perms.kfetauth.view_group %}
<a class="btn btn-primary" href="{% url 'kfet.group' %}">{% trans "Permissions" %}</a>
{% endif %}
{% if perms.kfet.view_negs %}
{% if perms.kfet.view_accountnegative %}
<a class="btn btn-primary" href="{% url 'kfet.account.negative' %}">Négatifs</a>
{% endif %}
</div>

View file

@ -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 %}
<div class="buttons">
<a class="btn btn-primary" href="{% url 'kfet.account.group.create' %}">
<span class="glyphicon glyphicon-plus"></span><span>Créer un groupe</span>
</a>
</div>
{% endblock %}
{% block main %}
{% for group in groups %}
<section>
<div class="heading">
{{ group.name }}
<div class="buttons">
<a class="btn btn-default" href="{% url 'kfet.account.group.update' group.pk %}">
<span class="glyphicon glyphicon-cog"></span><span class="hidden-xs">Éditer</span>
</a>
</div>
</div>
<div>
<h3>Comptes</h3>
<div class="sub-block column-sm-2 column-md-3">
<ul>
{% for user in group.user_set.all %}
<li>
<a href="{% url "kfet.account.update" user.profile.account_kfet.trigramme %}">
{{ user.profile.account_kfet }}
</a>
</li>
{% endfor %}
</ul>
</div>
<h3>Permissions</h3>
<div class="column-sm-2 column-lg-3">
{% regroup group.permissions.all by content_type as grouped_perms %}
<ul class="list-unstyled">
{% for perms_group in grouped_perms %}
<li class="unbreakable">
<b>{{ perms_group.grouper|title }}</b>
<ul>
{% for perm in perms_group.list %}
<li>{{ perm.name }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
</div>
</section>
{% endfor %}
{% endblock %}

View file

@ -1,49 +0,0 @@
{% extends 'kfet/base_form.html' %}
{% load staticfiles %}
{% load widget_tweaks %}
{% block extra_head %}
<link rel="stylesheet" text="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script src="{% static 'kfet/js/multiple-select.js' %}"></script>
{% endblock %}
{% block title %}Permissions - Édition{% endblock %}
{% block header-title %}Modification des permissions{% endblock %}
{% block main %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.name.id_for_label }}" class="col-sm-2 control-label">{{ form.name.label }}</label>
<div class="col-sm-10">
<div class="input-group">
<span class="input-group-addon">K-Fêt</span>
{{ form.name|add_class:"form-control" }}
</div>
{% if form.name.errors %}
<span class="help-block">{{ form.name.errors }}</span>
{% endif %}
{% if form.name.help_text %}
<span class="help-block">{{ form.name.help_text }}</span>
{% endif %}
</div>
</div>
{% include "kfet/form_field_snippet.html" with field=form.permissions %}
{% if not perms.kfet.manage_perms %}
{% include "kfet/form_authentication_snippet.html" %}
{% endif %}
{% include "kfet/form_submit_snippet.html" with value="Enregistrer" %}
</form>
<script type="text/javascript">
$(document).ready(function() {
let $name_input = $("#id_name");
let raw_name = $name_input.val();
let prefix = "K-Fêt ";
if (raw_name.startsWith(prefix))
$name_input.val(raw_name.substring(prefix.length));
});
</script>
{% endblock %}

View file

@ -24,6 +24,7 @@
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery.formset.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>

View file

@ -0,0 +1,19 @@
{% load widget_tweaks %}
{% with widget=field.field.widget %}
{% if field|widget_type == "checkboxselectmultiple" %}
<div class="checkbox-select-multiple {{ widget.attrs.field_class }}">
{{ field }}
</div>
{% elif field|widget_type == "checkboxinput" %}
<div class="checkbox">
<label>
{{ field }} {{ field.label }}
</label>
</div>
{% else %}
{{ field|add_class:'form-control' }}
{% endif %}
{% endwith %}

View file

@ -1,26 +1,24 @@
{% load widget_tweaks %}
<div class="form-group">
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10">
{% if field|widget_type == "checkboxselectmultiple" %}
<ul class="list-unstyled checkbox-select-multiple">
{% for choice in form.permissions %}
<li class="col-sm-6 col-lg-4">
<label for="{{ choice.id_for_label }}">
{{ choice.tag }} {{ choice.choice_label }}
</label>
</li>
{% endfor %}
</ul>
{% else %}
{{ field|add_class:'form-control' }}
{% endif %}
{% if not field.label %}
{% elif field|widget_type == "checkboxinput" %}
{# label is displayed along the checkbox #}
{% else %}
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">
{{ field.label }}
</label>
{% endif %}
<div class="{% if not field.label %}col-sm-12{% elif field|widget_type == "checkboxinput" %}col-sm-10 col-sm-offset-2{% else %}col-sm-10{% endif %}">
{% include "kfet/form_field_base_snippet.html" with field=field %}
{% if field.errors %}
<span class="help-block">{{field.errors}}</span>
<span class="help-block">{{ field.errors }}</span>
{% endif %}
{% if field.help_text %}
<span class="help-block">{{field.help_text}}</span>
<span class="help-block">{{ field.help_text|safe }}</span>
{% endif %}
</div>
</div>

View file

@ -1,4 +1,4 @@
<form action="" method="post" class="form-horizontal">
<form action="" method="post">
{% csrf_token %}
{% include "kfet/form_snippet.html" %}
{% if not authz %}

View file

@ -1,3 +1,5 @@
{% for field in form %}
{% include 'kfet/form_field_snippet.html' with field=field %}
{% endfor %}
<div class="form-horizontal">
{% for field in form %}
{% include 'kfet/form_field_snippet.html' with field=field %}
{% endfor %}
</div>

View file

@ -1,5 +1,11 @@
<div class="form-group">
<div class="col-sm-6 col-sm-offset-3 text-center">
<input type="submit" value="{{ value }}" class="btn btn-primary btn-lg">
{% load i18n %}
{% trans "Enregistrer" as default_value %}
<div class="form-horizontal">
<div class="form-group">
<div class="col-sm-6 col-sm-offset-3 text-center">
<input type="submit" value="{% firstof value default_value %}" class="btn btn-primary btn-lg">
</div>
</div>
</div>

View file

@ -0,0 +1,12 @@
{% load i18n %}
{% load widget_tweaks %}
<div class="form-group form-actions">
{{ form.DELETE|add_class:"hide" }}
<button type="button" class="btn btn-danger btn-sm" title="{% trans "Supprimer" %}" data-formset-delete-button>
<span class="glyphicon glyphicon-trash"></span>
</button>
<button type="button" class="btn btn-default btn-sm" title="{% trans "Restaurer" %}" data-formset-restore-button style="display: none">
<span class="glyphicon glyphicon-plus"></span>
</button>
</div>

View file

@ -1,5 +0,0 @@
from django.template.defaulttags import register
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)

View file

@ -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)]

View file

@ -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()

View file

@ -3,7 +3,6 @@ from datetime import datetime, timedelta
from decimal import Decimal
from unittest import mock
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.test import Client, TestCase
from django.utils import timezone
@ -15,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):
@ -335,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='K-Fêt 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, 'K-Fêt 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'
@ -484,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):

View file

@ -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<trigramme>.{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<pk>\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')),
]

View file

@ -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)

View file

@ -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:

View file

@ -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

3
setup.cfg Normal file
View file

@ -0,0 +1,3 @@
[flake8]
# E731: lambda expressions
ignore = E731

0
utils/__init__.py Normal file
View file

61
utils/forms.py Normal file
View file

@ -0,0 +1,61 @@
class KeepUnselectableModelFormMixin:
"""
Keep unselectable items of queryset-based fields.
Mixin for 'ModelForm'.
Attribute
keep_unselectable_fields (list of field names)
This adding is performed in 'save' method. Specifically, if 'commit' arg
is False, it's done in 'save_m2m' method.
These fields must have a 'queryset' attribute (the selectable items), like
'ModelMultipleChoiceField' (default field for ManyToMany model fields).
"""
keep_unselectable_fields = []
def get_unselectable(self, field_name):
"""
Returns 'field_name' model field items of instance which can't be
selected with the corresponding form field.
Should be used before 'form.save' call, or 'form.save_m2m' call if
'commit=False' was passed as argument to 'form.save'.
"""
if self.instance.pk:
previous = getattr(self.instance, field_name).all()
selectable = self.fields[field_name].queryset
return previous.exclude(pk__in=[o.pk for o in selectable])
else:
# Instance is being created, there is no previous item.
return []
def save(self, commit=True):
# Use 'commit=False' to get the 'save_m2m' method.
instance = super().save(commit=False)
_save_m2m = self.save_m2m
def save_m2m():
# Get the unselectable items.
# Force evaluate because those items are going to change.
unselectable_f = {
f_name: list(self.get_unselectable(f_name))
for f_name in self.keep_unselectable_fields
}
# Default 'save_m2m' use 'set' method of m2m relationships with
# fields' cleaned data.
_save_m2m()
# Add the unselectable elements.
for f_name, unselectable in unselectable_f.items():
getattr(instance, f_name).add(*unselectable)
# Implement the default behavior of 'save' method, with our own
# 'save_m2m'.
if commit:
instance.save()
save_m2m()
else:
self.save_m2m = save_m2m
return instance

0
utils/tests/__init__.py Normal file
View file

141
utils/tests/test_forms.py Normal file
View file

@ -0,0 +1,141 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from utils.forms import KeepUnselectableModelFormMixin
User = get_user_model()
class KeepUnselectableModelFormMixinTests(TestCase):
def _get_form_cls(self):
"""
We recreate a new form class for each test, because `queryset` may not
be reevaluated and may lack the permissions created by `setUp`.
"""
class ExampleForm(KeepUnselectableModelFormMixin, forms.ModelForm):
user_permissions = forms.ModelMultipleChoiceField(
queryset=Permission.objects.filter(
codename__startswith='selec'),
)
keep_unselectable_fields = ['user_permissions']
class Meta:
model = User
fields = ('username', 'user_permissions')
return ExampleForm
def setUp(self):
ct = ContentType.objects.get_for_model(Permission)
self.unselec_perm1 = Permission.objects.create(
content_type=ct, codename='unselec_perm1', name='Unselectable 1')
self.unselec_perm2 = Permission.objects.create(
content_type=ct, codename='unselec_perm2', name='Unselectable 2')
# These two perms are the only selectable permissions from
# 'permissions' field of ExampleForm.
self.selec_perm1 = Permission.objects.create(
content_type=ct, codename='selec_perm1', name='Selectable 1')
self.selec_perm2 = Permission.objects.create(
content_type=ct, codename='selec_perm2', name='Selectable 2')
def test_creation(self):
"""
The mixin functions properly when instance is being created.
"""
ExampleForm = self._get_form_cls()
data = {
'username': 'user',
'user_permissions': [self.selec_perm1.pk],
}
form = ExampleForm(data)
instance = form.save()
self.assertQuerysetEqual(
instance.user_permissions.all(),
map(repr, [self.selec_perm1]),
)
def test_creation_commit_false(self):
"""
When instance is being created and 'save' method is called with
'commit=False', 'save_m2m' method functions properly.
https://docs.djangoproject.com/en/1.11/topics/forms/modelforms/#the-save-method
"""
ExampleForm = self._get_form_cls()
data = {
'username': 'user',
'user_permissions': [self.selec_perm1.pk],
}
form = ExampleForm(data)
instance = form.save(commit=False)
with self.assertRaises(User.DoesNotExist):
User.objects.get(username='user')
instance.save()
form.save_m2m()
self.assertQuerysetEqual(
instance.user_permissions.all(),
map(repr, [self.selec_perm1]),
)
def test_existing(self):
"""
Unselectable items of an instance are kept.
"""
instance = User.objects.create(username='user')
# Link instance with an unselectable and a selectable permissions.
instance.user_permissions.add(self.unselec_perm1, self.selec_perm2)
ExampleForm = self._get_form_cls()
data = {
'username': 'user',
'user_permissions': [self.selec_perm1.pk],
}
form = ExampleForm(data, instance=instance)
instance = form.save()
self.assertQuerysetEqual(
instance.user_permissions.all(),
map(repr, [self.selec_perm1, self.unselec_perm1]),
)
def test_existing_commit_false(self):
"""
When 'save' is called with 'commit=False', unselectable items of an
instance are kept by 'save_m2m'.
"""
instance = User.objects.create(username='user')
# Link instance with an unselectable and a selectable permissions.
instance.user_permissions.add(self.unselec_perm1, self.selec_perm2)
ExampleForm = self._get_form_cls()
data = {
'username': 'changed',
'user_permissions': [self.selec_perm1.pk],
}
form = ExampleForm(data, instance=instance)
instance = form.save(commit=False)
with self.assertRaises(User.DoesNotExist):
User.objects.get(username='changed')
instance.save()
form.save_m2m()
self.assertQuerysetEqual(
instance.user_permissions.all(),
map(repr, [self.selec_perm1, self.unselec_perm1]),
)