Compare commits

...

11 commits

Author SHA1 Message Date
Aurélien Delobelle
84bbe78e50 kfet -- Fix auth tests 2019-01-14 23:17:26 +01:00
Aurélien Delobelle
fcf4a25745 Merge branch 'master' into aureplop/kfet-auth 2019-01-14 22:41:38 +01: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
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
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
15 changed files with 490 additions and 150 deletions

View file

@ -1,17 +1,26 @@
from django import forms 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.forms import widgets
from .models import Group, Permission
class KFetPermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, *args, **kwargs): class GroupsField(forms.ModelMultipleChoiceField):
queryset = Permission.objects.filter( def __init__(self, **kwargs):
content_type__in=ContentType.objects.filter(app_label="kfet") kwargs.setdefault("queryset", Group.objects.all())
) kwargs.setdefault("widget", widgets.CheckboxSelectMultiple)
super().__init__( super().__init__(**kwargs)
queryset=queryset, widget=widgets.CheckboxSelectMultiple, *args, **kwargs
)
class BasePermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, **kwargs):
kwargs.setdefault("widget", widgets.CheckboxSelectMultiple)
super().__init__(**kwargs)
def label_from_instance(self, obj): def label_from_instance(self, obj):
return obj.name return obj.name
class CorePermissionsField(BasePermissionsField):
def __init__(self, **kwargs):
kwargs.setdefault("queryset", Permission.kfetcore.all())
super().__init__(**kwargs)

View file

@ -1,48 +1,27 @@
from django import forms 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 CorePermissionsField, GroupsField
from .models import Group
class GroupForm(forms.ModelForm): class GroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
permissions = KFetPermissionsField() permissions = CorePermissionsField(label=_("Permissions"), required=False)
def clean_name(self): keep_unselectable_fields = ["permissions"]
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)
class Meta: class Meta:
model = Group model = Group
fields = ["name", "permissions"] fields = ["name", "permissions"]
class UserGroupForm(forms.ModelForm): class UserGroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
groups = forms.ModelMultipleChoiceField( groups = GroupsField(label=_("Statut équipe"), required=False)
Group.objects.filter(name__icontains="K-Fêt"),
label="Statut équipe",
required=False,
)
def clean_groups(self): keep_unselectable_fields = ["groups"]
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)
class Meta: class Meta:
model = User model = User

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("kfetauth", "0001_initial")]
operations = [
migrations.CreateModel(
name="Group",
fields=[
(
"group_ptr",
models.OneToOneField(
parent_link=True,
serialize=False,
primary_key=True,
auto_created=True,
to="auth.Group",
),
)
],
options={"verbose_name": "Groupe", "verbose_name_plural": "Groupes"},
bases=("auth.group",),
),
migrations.CreateModel(
name="Permission",
fields=[],
options={
"verbose_name": "Permission",
"verbose_name_plural": "Permissions",
"proxy": True,
},
bases=("auth.permission",),
),
]

View file

@ -0,0 +1,26 @@
# -*- 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_local_group_and_perm")]
operations = [migrations.RunPython(existing_groups)]

View file

@ -1,5 +1,10 @@
from django.contrib.auth.models import (
Group as DjangoGroup,
Permission as DjangoPermission,
)
from django.db import models from django.db import models
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
class GenericTeamTokenManager(models.Manager): class GenericTeamTokenManager(models.Manager):
@ -14,3 +19,38 @@ class GenericTeamToken(models.Model):
token = models.CharField(max_length=50, unique=True) token = models.CharField(max_length=50, unique=True)
objects = GenericTeamTokenManager() objects = GenericTeamTokenManager()
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")
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()
class Meta:
proxy = True
verbose_name = _("Permission")
verbose_name_plural = _("Permissions")

View file

@ -1,70 +1,172 @@
from unittest import mock from unittest import mock
from django.contrib.auth.models import AnonymousUser, Group, Permission, User 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 import signing
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from kfet.forms import UserGroupForm
from kfet.models import Account from kfet.models import Account
from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME
from .backends import AccountBackend, GenericBackend from .backends import AccountBackend, GenericBackend
from .fields import CorePermissionsField, GroupsField
from .forms import GroupForm, UserGroupForm
from .middleware import TemporaryAuthMiddleware from .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken from .models import GenericTeamToken, Group, Permission
from .utils import get_kfet_generic_user from .utils import get_kfet_generic_user
from .views import GenericLoginView from .views import GenericLoginView
## ##
# Forms # Forms and form fields
## ##
class UserGroupFormTests(TestCase): class CorePermissionsFieldTests(TestCase):
"""Test suite for UserGroupForm.""" 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):
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.
"""
self.kf_group.permissions.add(self.ot_perm)
selected = [self.kf_perm1, self.kf_perm2]
data = {"name": "A Group", "permissions": [perm.pk for perm 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):
def setUp(self): def setUp(self):
# create user # create user
self.user = User.objects.create(username="foo", password="foo") self.user = User.objects.create(username="foo", password="foo")
# create some K-Fêt groups # create some K-Fêt groups
prefix_name = "K-Fêt " self.kf_group1 = Group.objects.create(name="KF Group1")
names = ["Group 1", "Group 2", "Group 3"] self.kf_group2 = Group.objects.create(name="KF Group2")
self.kfet_groups = [ self.kf_group3 = Group.objects.create(name="KF Group3")
Group.objects.create(name=prefix_name + name) for name in names
]
# create a non-K-Fêt group # create a non-K-Fêt group
self.other_group = Group.objects.create(name="Other group") self.ot_group = DjangoGroup.objects.create(name="OT 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
)
def test_keep_others(self): 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.
"""
# add user to a non-K-Fêt group # 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 # add user to some K-Fêt groups through UserGroupForm
data = {"groups": [group.pk for group in self.kfet_groups]} selected = [self.kf_group1, self.kf_group2]
form = UserGroupForm(data, instance=user) data = {"groups": [str(g.pk) for g in selected]}
form = UserGroupForm(data, instance=self.user)
form.is_valid()
form.save() form.save()
transform = lambda g: g.pk
self.assertQuerysetEqual( self.assertQuerysetEqual(
user.groups.all(), self.user.groups.all(),
[repr(g) for g in [self.other_group] + self.kfet_groups], map(transform, [self.ot_group] + selected),
ordered=False, 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): class KFetGenericUserTests(TestCase):
def test_exists(self): def test_exists(self):
""" """

View file

@ -1,7 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import Group, User from django.contrib.auth.models import User
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
@ -15,7 +15,7 @@ from django.views.generic import View
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView
from .forms import GroupForm from .forms import GroupForm
from .models import GenericTeamToken from .models import GenericTeamToken, Group
class GenericLoginView(View): class GenericLoginView(View):
@ -113,23 +113,24 @@ def account_group(request):
user_pre = Prefetch( user_pre = Prefetch(
"user_set", queryset=User.objects.select_related("profile__account_kfet") "user_set", queryset=User.objects.select_related("profile__account_kfet")
) )
groups = Group.objects.filter(name__icontains="K-Fêt").prefetch_related( groups = Group.objects.prefetch_related("permissions", user_pre)
"permissions", user_pre
)
return render(request, "kfet/account_group.html", {"groups": groups}) return render(request, "kfet/account_group.html", {"groups": groups})
class AccountGroupCreate(SuccessMessageMixin, CreateView): class BaseAccountGroupFormViewMixin:
model = Group model = Group
template_name = "kfet/account_group_form.html"
form_class = GroupForm form_class = GroupForm
template_name = "kfet/account_group_form.html"
success_url = reverse_lazy("kfet.account.group")
class AccountGroupFormViewMixin(SuccessMessageMixin, BaseAccountGroupFormViewMixin):
pass
class AccountGroupCreate(AccountGroupFormViewMixin, CreateView):
success_message = "Nouveau groupe : %(name)s" success_message = "Nouveau groupe : %(name)s"
success_url = reverse_lazy("kfet.account.group")
class AccountGroupUpdate(SuccessMessageMixin, UpdateView): class AccountGroupUpdate(AccountGroupFormViewMixin, 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_message = "Groupe modifié : %(name)s"
success_url = reverse_lazy("kfet.account.group")

View file

@ -6,7 +6,7 @@ import os
import random import random
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import ContentType, Group, Permission, User from django.contrib.auth.models import User
from django.core.management import call_command from django.core.management import call_command
from django.utils import timezone from django.utils import timezone
@ -17,6 +17,7 @@ from kfet.models import (
Article, Article,
Checkout, Checkout,
CheckoutStatement, CheckoutStatement,
Group,
Supplier, Supplier,
SupplierArticle, SupplierArticle,
) )
@ -33,23 +34,11 @@ class Command(MyBaseCommand):
# Groupes # 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_chef.give_admin_access()
group_boy = Group(name="K-Fêt Légionnaire") group_boy.give_staff_access()
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)
# --- # ---
# Comptes # Comptes

View file

@ -13,7 +13,7 @@ from django.utils.translation import ugettext_lazy as _
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from .auth import KFET_GENERIC_TRIGRAMME 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 .config import kfet_config
from .utils import to_ukf from .utils import to_ukf

View file

@ -1,49 +1,10 @@
{% extends 'kfet/base_form.html' %} {% 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 title %}Permissions - Édition{% endblock %}
{% block header-title %}Modification des permissions{% endblock %} {% block header-title %}Modification des permissions{% endblock %}
{% block main %} {% block main %}
<form action="" method="post" class="form-horizontal"> {% include "kfet/form_full_snippet.html" with authz=perms.kfet.manage_perms submit_text="Enregistrer" %}
{% 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 %} {% endblock %}

View file

@ -5,7 +5,7 @@
<div class="col-sm-10"> <div class="col-sm-10">
{% if field|widget_type == "checkboxselectmultiple" %} {% if field|widget_type == "checkboxselectmultiple" %}
<ul class="list-unstyled checkbox-select-multiple"> <ul class="list-unstyled checkbox-select-multiple">
{% for choice in form.permissions %} {% for choice in field %}
<li class="col-sm-6 col-lg-4"> <li class="col-sm-6 col-lg-4">
<label for="{{ choice.id_for_label }}"> <label for="{{ choice.id_for_label }}">
{{ choice.tag }} {{ choice.choice_label }} {{ choice.tag }} {{ choice.choice_label }}
@ -17,10 +17,10 @@
{{ field|add_class:'form-control' }} {{ field|add_class:'form-control' }}
{% endif %} {% endif %}
{% if field.errors %} {% if field.errors %}
<span class="help-block">{{field.errors}}</span> <span class="help-block">{{ field.errors }}</span>
{% endif %} {% endif %}
{% if field.help_text %} {% if field.help_text %}
<span class="help-block">{{field.help_text}}</span> <span class="help-block">{{ field.help_text }}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

@ -3,11 +3,12 @@ from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from unittest import mock from unittest import mock
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import Client, TestCase from django.test import Client, TestCase
from django.utils import timezone from django.utils import timezone
from kfet.auth.models import Group
from ..config import kfet_config from ..config import kfet_config
from ..models import ( from ..models import (
Account, Account,
@ -398,7 +399,7 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase):
r = self.client.post(self.url, self.post_data) r = self.client.post(self.url, self.post_data)
self.assertRedirects(r, reverse("kfet.account.group")) self.assertRedirects(r, reverse("kfet.account.group"))
group = Group.objects.get(name="K-Fêt The Group") group = Group.objects.get(name="The Group")
self.assertQuerysetEqual( self.assertQuerysetEqual(
group.permissions.all(), group.permissions.all(),
@ -452,7 +453,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
self.group.refresh_from_db() self.group.refresh_from_db()
self.assertEqual(self.group.name, "K-Fêt The Group") self.assertEqual(self.group.name, "The Group")
self.assertQuerysetEqual( self.assertQuerysetEqual(
self.group.permissions.all(), self.group.permissions.all(),
map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]),

62
utils/forms.py Normal file
View file

@ -0,0 +1,62 @@
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

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

@ -0,0 +1,131 @@
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]),
)