Compare commits
26 commits
master
...
aureplop/k
Author | SHA1 | Date | |
---|---|---|---|
|
e3e8608563 | ||
|
02f1e83420 | ||
|
6d90311ae1 | ||
|
a7dbc64e2b | ||
|
94ab754f82 | ||
|
13f01020f7 | ||
|
2b62c3a785 | ||
|
d36a813e15 | ||
|
94f21c062a | ||
|
e39f88991e | ||
|
03a0c58869 | ||
|
aa405d212a | ||
|
c524da22fe | ||
|
09290131d5 | ||
|
40ceaf411a | ||
|
097ee44131 | ||
|
2c76bea1e6 | ||
|
07f1a53532 | ||
|
82582866b4 | ||
|
5502c6876a | ||
|
df7594a105 | ||
|
e6fab703ee | ||
|
c17ed416c4 | ||
|
085a068020 | ||
|
8ea5775d61 | ||
|
ded824bddd |
55 changed files with 2283 additions and 520 deletions
|
@ -69,6 +69,7 @@ INSTALLED_APPS = [
|
||||||
'autocomplete_light',
|
'autocomplete_light',
|
||||||
'captcha',
|
'captcha',
|
||||||
'django_cas_ng',
|
'django_cas_ng',
|
||||||
|
'djangoformsetjs',
|
||||||
'bootstrapform',
|
'bootstrapform',
|
||||||
'kfet',
|
'kfet',
|
||||||
'kfet.open',
|
'kfet.open',
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import (absolute_import, division,
|
|
||||||
print_function, unicode_literals)
|
|
||||||
from builtins import *
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class KFetConfig(AppConfig):
|
class KFetConfig(AppConfig):
|
||||||
name = 'kfet'
|
name = 'kfet'
|
||||||
verbose_name = "Application K-Fêt"
|
verbose_name = "Application K-Fêt"
|
||||||
|
@ -15,5 +11,5 @@ class KFetConfig(AppConfig):
|
||||||
|
|
||||||
def register_config(self):
|
def register_config(self):
|
||||||
import djconfig
|
import djconfig
|
||||||
from kfet.forms import KFetConfigForm
|
from .config import KFetConfigForm
|
||||||
djconfig.register(KFetConfigForm)
|
djconfig.register(KFetConfigForm)
|
||||||
|
|
|
@ -1,20 +1,77 @@
|
||||||
|
import re
|
||||||
|
from itertools import groupby
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import Permission
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.forms import widgets
|
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(
|
class BasePermissionsField(forms.ModelMultipleChoiceField):
|
||||||
content_type__in=ContentType.objects.filter(app_label="kfet"),
|
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,
|
||||||
)
|
)
|
||||||
super().__init__(
|
|
||||||
queryset=queryset,
|
|
||||||
widget=widgets.CheckboxSelectMultiple,
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
)
|
||||||
|
next_default_pos = 0
|
||||||
|
|
||||||
def label_from_instance(self, obj):
|
for p in perms:
|
||||||
return obj.name
|
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)
|
||||||
|
|
|
@ -1,47 +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 GroupsField, CorePermissionsField
|
||||||
|
from .models import Group
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(forms.ModelForm):
|
class GroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
|
||||||
permissions = KFetPermissionsField()
|
permissions = CorePermissionsField(label='', 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
|
||||||
|
|
54
kfet/auth/migrations/0002_create_group_permission_models.py
Normal file
54
kfet/auth/migrations/0002_create_group_permission_models.py
Normal 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',),
|
||||||
|
),
|
||||||
|
]
|
31
kfet/auth/migrations/0003_existing_groups.py
Normal file
31
kfet/auth/migrations/0003_existing_groups.py
Normal 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),
|
||||||
|
]
|
81
kfet/auth/migrations/0004_update_permissions.py
Normal file
81
kfet/auth/migrations/0004_update_permissions.py
Normal 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),
|
||||||
|
]
|
|
@ -1,5 +1,11 @@
|
||||||
|
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 _
|
||||||
|
|
||||||
|
from kfet.cms.models import CmsPermissionManager
|
||||||
|
|
||||||
|
|
||||||
class GenericTeamTokenManager(models.Manager):
|
class GenericTeamTokenManager(models.Manager):
|
||||||
|
@ -15,3 +21,42 @@ 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 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")
|
||||||
|
|
58
kfet/auth/templates/kfet/group_form.html
Normal file
58
kfet/auth/templates/kfet/group_form.html
Normal 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 %}
|
58
kfet/auth/templates/kfet/group_list.html
Normal file
58
kfet/auth/templates/kfet/group_list.html
Normal 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 %}
|
|
@ -1,76 +1,197 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from unittest import mock
|
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 import signing
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
|
|
||||||
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 kfet.tests.testcases import ViewTestCaseMixin
|
||||||
|
from kfet.tests.utils import create_team, get_perms
|
||||||
|
|
||||||
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 GroupsField, CorePermissionsField
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# 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):
|
class UserGroupFormTests(TestCase):
|
||||||
"""Test suite for UserGroupForm."""
|
|
||||||
|
|
||||||
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.
|
||||||
|
"""
|
||||||
|
from .forms import UserGroupForm
|
||||||
# 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 = {
|
selected = [self.kf_group1, self.kf_group2]
|
||||||
'groups': [group.pk for group in self.kfet_groups],
|
data = {'groups': [str(g.pk) for g in selected]}
|
||||||
}
|
form = UserGroupForm(data, instance=self.user)
|
||||||
form = UserGroupForm(data, instance=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):
|
||||||
|
@ -139,6 +260,10 @@ class GenericLoginViewTests(TestCase):
|
||||||
patcher_messages.start()
|
patcher_messages.start()
|
||||||
self.addCleanup(patcher_messages.stop)
|
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 = Account(trigramme='000')
|
||||||
user_acc.save({'username': 'user'})
|
user_acc.save({'username': 'user'})
|
||||||
self.user = user_acc.user
|
self.user = user_acc.user
|
||||||
|
@ -230,14 +355,14 @@ class GenericLoginViewTests(TestCase):
|
||||||
"""
|
"""
|
||||||
token = GenericTeamToken.objects.create(token='valid')
|
token = GenericTeamToken.objects.create(token='valid')
|
||||||
self._set_signed_cookie(
|
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)
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
self.assertRedirects(r, reverse('kfet.kpsul'))
|
self.assertRedirects(r, reverse('kfet.kpsul'))
|
||||||
self.assertEqual(r.wsgi_request.user, self.generic_user)
|
self.assertEqual(r.wsgi_request.user, self.generic_user)
|
||||||
self._is_cookie_deleted(
|
self._is_cookie_deleted(
|
||||||
self.client, GenericLoginView.TOKEN_COOKIE_NAME)
|
self.client, self.view_cls.TOKEN_COOKIE_NAME)
|
||||||
with self.assertRaises(GenericTeamToken.DoesNotExist):
|
with self.assertRaises(GenericTeamToken.DoesNotExist):
|
||||||
token.refresh_from_db()
|
token.refresh_from_db()
|
||||||
|
|
||||||
|
@ -246,14 +371,14 @@ class GenericLoginViewTests(TestCase):
|
||||||
If token is invalid, delete it and try again.
|
If token is invalid, delete it and try again.
|
||||||
"""
|
"""
|
||||||
self._set_signed_cookie(
|
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)
|
r = self.client.get(self.url)
|
||||||
|
|
||||||
self.assertRedirects(r, self.url, fetch_redirect_response=False)
|
self.assertRedirects(r, self.url, fetch_redirect_response=False)
|
||||||
self.assertEqual(r.wsgi_request.user, AnonymousUser())
|
self.assertEqual(r.wsgi_request.user, AnonymousUser())
|
||||||
self._is_cookie_deleted(
|
self._is_cookie_deleted(
|
||||||
self.client, GenericLoginView.TOKEN_COOKIE_NAME)
|
self.client, self.view_cls.TOKEN_COOKIE_NAME)
|
||||||
|
|
||||||
def test_flow_ok(self):
|
def test_flow_ok(self):
|
||||||
"""
|
"""
|
||||||
|
@ -269,6 +394,154 @@ class GenericLoginViewTests(TestCase):
|
||||||
self.assertEqual(r.wsgi_request.path, '/k-fet/')
|
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
|
# Temporary authentication
|
||||||
#
|
#
|
||||||
|
|
18
kfet/auth/urls.py
Normal file
18
kfet/auth/urls.py
Normal 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'),
|
||||||
|
]
|
|
@ -1,21 +1,22 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.auth import authenticate, get_user_model, 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.views import redirect_to_login
|
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.db.models import Prefetch
|
||||||
from django.http import QueryDict
|
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.decorators import method_decorator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.decorators.http import require_http_methods
|
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 .forms import GroupForm
|
||||||
from .models import GenericTeamToken
|
from .models import GenericTeamToken, Group
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class GenericLoginView(View):
|
class GenericLoginView(View):
|
||||||
|
@ -104,33 +105,105 @@ class GenericLoginView(View):
|
||||||
login_generic = GenericLoginView.as_view()
|
login_generic = GenericLoginView.as_view()
|
||||||
|
|
||||||
|
|
||||||
@permission_required('kfet.manage_perms')
|
@permission_required('kfetauth.view_group')
|
||||||
def account_group(request):
|
def group_index(request):
|
||||||
user_pre = Prefetch(
|
user_pre = Prefetch(
|
||||||
'user_set',
|
'user_set',
|
||||||
queryset=User.objects.select_related('profile__account_kfet'),
|
queryset=User.objects.select_related('profile__account_kfet'),
|
||||||
)
|
)
|
||||||
groups = (
|
groups = Group.objects.prefetch_related(user_pre)
|
||||||
Group.objects
|
return render(request, 'kfet/group_list.html', {
|
||||||
.filter(name__icontains='K-Fêt')
|
|
||||||
.prefetch_related('permissions', user_pre)
|
|
||||||
)
|
|
||||||
return render(request, 'kfet/account_group.html', {
|
|
||||||
'groups': groups,
|
'groups': groups,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class AccountGroupCreate(SuccessMessageMixin, CreateView):
|
_group_formview_extras = None
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
|
def get_group_formview_extras():
|
||||||
queryset = Group.objects.filter(name__icontains='K-Fêt')
|
global _group_formview_extras
|
||||||
template_name = 'kfet/account_group_form.html'
|
|
||||||
form_class = GroupForm
|
if _group_formview_extras is None:
|
||||||
success_message = 'Groupe modifié : %(name)s'
|
_group_formview_extras = []
|
||||||
success_url = reverse_lazy('kfet.account.group')
|
|
||||||
|
# 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
196
kfet/cms/forms.py
Normal 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
|
|
@ -1,8 +1,14 @@
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.management.base import BaseCommand
|
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):
|
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)
|
# Par défaut, il s'agit d'une copie du site K-Fêt (17-05)
|
||||||
|
|
||||||
call_command('loaddata', options['file'])
|
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
|
||||||
|
])
|
||||||
|
|
|
@ -11,7 +11,7 @@ from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
|
||||||
from wagtail.wagtailsnippets.blocks import SnippetChooserBlock
|
from wagtail.wagtailsnippets.blocks import SnippetChooserBlock
|
||||||
from wagtail.wagtailsnippets.models import register_snippet
|
from wagtail.wagtailsnippets.models import register_snippet
|
||||||
|
|
||||||
from kfet.cms.context_processors import get_articles
|
from .utils import get_page_model_names
|
||||||
|
|
||||||
|
|
||||||
@register_snippet
|
@register_snippet
|
||||||
|
@ -60,6 +60,7 @@ class MenuBlock(blocks.StaticBlock):
|
||||||
template = 'kfetcms/block_menu.html'
|
template = 'kfetcms/block_menu.html'
|
||||||
|
|
||||||
def get_context(self, *args, **kwargs):
|
def get_context(self, *args, **kwargs):
|
||||||
|
from .context_processors import get_articles
|
||||||
context = super().get_context(*args, **kwargs)
|
context = super().get_context(*args, **kwargs)
|
||||||
context.update(get_articles())
|
context.update(get_articles())
|
||||||
return context
|
return context
|
||||||
|
@ -172,3 +173,18 @@ class KFetPage(Page):
|
||||||
page.seo_title = page.title
|
page.seo_title = page.title
|
||||||
|
|
||||||
return context
|
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())
|
||||||
|
)
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
35
kfet/cms/utils.py
Normal 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
74
kfet/cms/views.py
Normal 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
|
|
@ -1,9 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from djconfig import config
|
from djconfig import config
|
||||||
|
from djconfig.forms import ConfigForm
|
||||||
|
|
||||||
|
from .models import Account
|
||||||
|
|
||||||
|
|
||||||
class KFetConfig(object):
|
class KFetConfig(object):
|
||||||
|
@ -19,8 +25,8 @@ class KFetConfig(object):
|
||||||
if key == 'subvention_cof':
|
if key == 'subvention_cof':
|
||||||
# Allows accessing to the reduction as a subvention
|
# Allows accessing to the reduction as a subvention
|
||||||
# Other reason: backward compatibility
|
# Other reason: backward compatibility
|
||||||
reduction_mult = 1 - self.reduction_cof/100
|
reduction_mult = 1 - self.reduction_cof / 100
|
||||||
return (1/reduction_mult - 1) * 100
|
return (1 / reduction_mult - 1) * 100
|
||||||
return getattr(config, self._get_dj_key(key))
|
return getattr(config, self._get_dj_key(key))
|
||||||
|
|
||||||
def list(self):
|
def list(self):
|
||||||
|
@ -30,8 +36,6 @@ class KFetConfig(object):
|
||||||
(key, value) for each configuration entry as list.
|
(key, value) for each configuration entry as list.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# prevent circular imports
|
|
||||||
from kfet.forms import KFetConfigForm
|
|
||||||
return [(field.label, getattr(config, name), )
|
return [(field.label, getattr(config, name), )
|
||||||
for name, field in KFetConfigForm.base_fields.items()]
|
for name, field in KFetConfigForm.base_fields.items()]
|
||||||
|
|
||||||
|
@ -46,9 +50,6 @@ class KFetConfig(object):
|
||||||
Config entries are updated to given values.
|
Config entries are updated to given values.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# prevent circular imports
|
|
||||||
from kfet.forms import KFetConfigForm
|
|
||||||
|
|
||||||
# get old config
|
# get old config
|
||||||
new_cfg = KFetConfigForm().initial
|
new_cfg = KFetConfigForm().initial
|
||||||
# update to new config
|
# update to new config
|
||||||
|
@ -69,3 +70,38 @@ class KFetConfig(object):
|
||||||
|
|
||||||
|
|
||||||
kfet_config = KFetConfig()
|
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),
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -9,16 +7,12 @@ from django.contrib.auth.models import User
|
||||||
from django.forms import modelformset_factory
|
from django.forms import modelformset_factory
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from djconfig.forms import ConfigForm
|
|
||||||
|
|
||||||
from kfet.models import (
|
from kfet.models import (
|
||||||
Account, Checkout, Article, OperationGroup, Operation,
|
Account, Checkout, Article, OperationGroup, Operation,
|
||||||
CheckoutStatement, ArticleCategory, AccountNegative, Transfer,
|
CheckoutStatement, ArticleCategory, AccountNegative, Transfer,
|
||||||
TransferGroup, Supplier)
|
TransferGroup, Supplier)
|
||||||
from gestioncof.models import CofProfile
|
from gestioncof.models import CofProfile
|
||||||
|
|
||||||
from .auth.forms import UserGroupForm # noqa
|
|
||||||
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Widgets
|
# Widgets
|
||||||
|
@ -369,46 +363,6 @@ class AddcostForm(forms.Form):
|
||||||
super(AddcostForm, self).clean()
|
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):
|
class FilterHistoryForm(forms.Form):
|
||||||
checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all())
|
checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all())
|
||||||
accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all())
|
accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all())
|
||||||
|
|
|
@ -7,13 +7,15 @@ import random
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.utils import timezone
|
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 django.core.management import call_command
|
||||||
|
|
||||||
from gestioncof.management.base import MyBaseCommand
|
from gestioncof.management.base import MyBaseCommand
|
||||||
from gestioncof.models import CofProfile
|
from gestioncof.models import CofProfile
|
||||||
from kfet.models import (Account, Checkout, CheckoutStatement, Supplier,
|
from kfet.models import (
|
||||||
SupplierArticle, Article)
|
Account, Article, Checkout, CheckoutStatement, Group, Supplier,
|
||||||
|
SupplierArticle, Article,
|
||||||
|
)
|
||||||
|
|
||||||
# Où sont stockés les fichiers json
|
# Où sont stockés les fichiers json
|
||||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
@ -28,22 +30,13 @@ 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
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|
||||||
from kfet.forms import KFetConfigForm
|
from kfet.config import KFetConfigForm
|
||||||
|
|
||||||
|
|
||||||
def adapt_settings(apps, schema_editor):
|
def adapt_settings(apps, schema_editor):
|
||||||
|
|
189
kfet/migrations/0062_change_models_opts.py
Normal file
189
kfet/migrations/0062_change_models_opts.py
Normal 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': (),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
99
kfet/migrations/0063_update_permissions.py
Normal file
99
kfet/migrations/0063_update_permissions.py
Normal 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),
|
||||||
|
]
|
|
@ -15,9 +15,8 @@ from datetime import date
|
||||||
import re
|
import re
|
||||||
|
|
||||||
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 .utils import to_ukf
|
from .utils import to_ukf
|
||||||
|
|
||||||
def choices_length(choices):
|
def choices_length(choices):
|
||||||
|
@ -85,9 +84,11 @@ class Account(models.Model):
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("Compte")
|
||||||
|
verbose_name_plural = _("Comptes")
|
||||||
|
default_permissions = ('add', 'change')
|
||||||
permissions = (
|
permissions = (
|
||||||
('is_team', 'Is part of the team'),
|
('is_team', "Membre de l'équipe"),
|
||||||
('manage_perms', 'Gérer les permissions K-Fêt'),
|
|
||||||
('manage_addcosts', 'Gérer les majorations'),
|
('manage_addcosts', 'Gérer les majorations'),
|
||||||
('edit_balance_account', "Modifier la balance d'un compte"),
|
('edit_balance_account', "Modifier la balance d'un compte"),
|
||||||
('change_account_password',
|
('change_account_password',
|
||||||
|
@ -102,6 +103,11 @@ class Account(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%s)' % (self.trigramme, self.name)
|
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
|
# Propriétés pour accéder aux attributs de cofprofile et user
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
|
@ -168,6 +174,7 @@ class Account(models.Model):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def perms_to_perform_operation(self, amount):
|
def perms_to_perform_operation(self, amount):
|
||||||
|
from .config import kfet_config
|
||||||
overdraft_duration_max = kfet_config.overdraft_duration
|
overdraft_duration_max = kfet_config.overdraft_duration
|
||||||
overdraft_amount_max = kfet_config.overdraft_amount
|
overdraft_amount_max = kfet_config.overdraft_amount
|
||||||
perms = set()
|
perms = set()
|
||||||
|
@ -332,12 +339,13 @@ class AccountNegative(models.Model):
|
||||||
comment = models.CharField("commentaire", max_length=255, blank=True)
|
comment = models.CharField("commentaire", max_length=255, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
verbose_name = _("Compte en négatif")
|
||||||
('view_negs', 'Voir la liste des négatifs'),
|
verbose_name_plural = _("Comptes en négatif")
|
||||||
)
|
default_permissions = ('view', 'change')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def until_default(self):
|
def until_default(self):
|
||||||
|
from .config import kfet_config
|
||||||
return self.start + kfet_config.overdraft_duration
|
return self.start + kfet_config.overdraft_duration
|
||||||
|
|
||||||
|
|
||||||
|
@ -357,7 +365,10 @@ class Checkout(models.Model):
|
||||||
return reverse('kfet.checkout.read', kwargs={'pk': self.pk})
|
return reverse('kfet.checkout.read', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("Caisse")
|
||||||
|
verbose_name_plural = _("Caisses")
|
||||||
ordering = ['-valid_to']
|
ordering = ['-valid_to']
|
||||||
|
default_permissions = ('add', 'change')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -372,6 +383,10 @@ class CheckoutTransfer(models.Model):
|
||||||
amount = models.DecimalField(
|
amount = models.DecimalField(
|
||||||
max_digits = 6, decimal_places = 2)
|
max_digits = 6, decimal_places = 2)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CheckoutStatement(models.Model):
|
class CheckoutStatement(models.Model):
|
||||||
by = models.ForeignKey(
|
by = models.ForeignKey(
|
||||||
|
@ -410,6 +425,11 @@ class CheckoutStatement(models.Model):
|
||||||
"montant des chèques",
|
"montant des chèques",
|
||||||
default=0, max_digits=6, decimal_places=2)
|
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):
|
def __str__(self):
|
||||||
return '%s %s' % (self.checkout, self.at)
|
return '%s %s' % (self.checkout, self.at)
|
||||||
|
|
||||||
|
@ -450,6 +470,11 @@ class ArticleCategory(models.Model):
|
||||||
"appliquée aux articles de "
|
"appliquée aux articles de "
|
||||||
"cette catégorie.")
|
"cette catégorie.")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Catégorie d'articles")
|
||||||
|
verbose_name_plural = _("Catégories d'articles")
|
||||||
|
default_permissions = ('change',)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -486,6 +511,11 @@ class Article(models.Model):
|
||||||
"capacité du contenant",
|
"capacité du contenant",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Article")
|
||||||
|
verbose_name_plural = _("Articles")
|
||||||
|
default_permissions = ('add', 'change')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s - %s' % (self.category.name, self.name)
|
return '%s - %s' % (self.category.name, self.name)
|
||||||
|
|
||||||
|
@ -505,6 +535,10 @@ class ArticleRule(models.Model):
|
||||||
related_name = "rule_to")
|
related_name = "rule_to")
|
||||||
ratio = models.PositiveSmallIntegerField()
|
ratio = models.PositiveSmallIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
class Inventory(models.Model):
|
class Inventory(models.Model):
|
||||||
articles = models.ManyToManyField(
|
articles = models.ManyToManyField(
|
||||||
Article,
|
Article,
|
||||||
|
@ -521,7 +555,10 @@ class Inventory(models.Model):
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("Inventaire")
|
||||||
|
verbose_name_plural = _("Inventaires")
|
||||||
ordering = ['-at']
|
ordering = ['-at']
|
||||||
|
default_permissions = ('add',)
|
||||||
permissions = (
|
permissions = (
|
||||||
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
|
('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_new = models.IntegerField()
|
||||||
stock_error = models.IntegerField(default = 0)
|
stock_error = models.IntegerField(default = 0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# S'il s'agit d'un inventaire provenant d'une livraison, il n'y a pas
|
# S'il s'agit d'un inventaire provenant d'une livraison, il n'y a pas
|
||||||
# d'erreur
|
# d'erreur
|
||||||
|
@ -557,6 +597,11 @@ class Supplier(models.Model):
|
||||||
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
|
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
|
||||||
comment = models.TextField(_("commentaire"), blank=True)
|
comment = models.TextField(_("commentaire"), blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Fournisseur")
|
||||||
|
verbose_name_plural = _("Fournisseurs")
|
||||||
|
default_permissions = ('change',)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -577,6 +622,9 @@ class SupplierArticle(models.Model):
|
||||||
max_digits = 7, decimal_places = 4,
|
max_digits = 7, decimal_places = 4,
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
supplier = models.ForeignKey(
|
supplier = models.ForeignKey(
|
||||||
Supplier, on_delete = models.PROTECT,
|
Supplier, on_delete = models.PROTECT,
|
||||||
|
@ -590,7 +638,10 @@ class Order(models.Model):
|
||||||
max_digits = 6, decimal_places = 2, default = 0)
|
max_digits = 6, decimal_places = 2, default = 0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("Commande")
|
||||||
|
verbose_name_plural = _("Commandes")
|
||||||
ordering = ['-at']
|
ordering = ['-at']
|
||||||
|
default_permissions = ('add',)
|
||||||
|
|
||||||
class OrderArticle(models.Model):
|
class OrderArticle(models.Model):
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
|
@ -600,6 +651,9 @@ class OrderArticle(models.Model):
|
||||||
quantity_ordered = models.IntegerField()
|
quantity_ordered = models.IntegerField()
|
||||||
quantity_received = models.IntegerField(default = 0)
|
quantity_received = models.IntegerField(default = 0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
class TransferGroup(models.Model):
|
class TransferGroup(models.Model):
|
||||||
at = models.DateTimeField(default=timezone.now)
|
at = models.DateTimeField(default=timezone.now)
|
||||||
# Optional
|
# Optional
|
||||||
|
@ -611,6 +665,9 @@ class TransferGroup(models.Model):
|
||||||
related_name = "+",
|
related_name = "+",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
class Transfer(models.Model):
|
class Transfer(models.Model):
|
||||||
group = models.ForeignKey(
|
group = models.ForeignKey(
|
||||||
|
@ -631,6 +688,11 @@ class Transfer(models.Model):
|
||||||
canceled_at = models.DateTimeField(
|
canceled_at = models.DateTimeField(
|
||||||
null=True, blank=True, default=None)
|
null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Transfert")
|
||||||
|
verbose_name_plural = _("Transferts")
|
||||||
|
default_permissions = ('add',)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount)
|
return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount)
|
||||||
|
|
||||||
|
@ -656,6 +718,9 @@ class OperationGroup(models.Model):
|
||||||
related_name = "+",
|
related_name = "+",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return ', '.join(map(str, self.opes.all()))
|
return ', '.join(map(str, self.opes.all()))
|
||||||
|
|
||||||
|
@ -706,6 +771,9 @@ class Operation(models.Model):
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("Opération")
|
||||||
|
verbose_name_plural = _("Opérations")
|
||||||
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('perform_deposit', 'Effectuer une charge'),
|
('perform_deposit', 'Effectuer une charge'),
|
||||||
('perform_negative_operations',
|
('perform_negative_operations',
|
||||||
|
|
52
kfet/static/kfet/css/base/forms.css
Normal file
52
kfet/static/kfet/css/base/forms.css
Normal 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; }
|
||||||
|
}
|
|
@ -4,10 +4,12 @@
|
||||||
/* Libs customizations */
|
/* Libs customizations */
|
||||||
@import url("libs/jconfirm-kfet.css");
|
@import url("libs/jconfirm-kfet.css");
|
||||||
@import url("libs/multiple-select-kfet.css");
|
@import url("libs/multiple-select-kfet.css");
|
||||||
|
@import url("libs/formset-kfet.css");
|
||||||
|
|
||||||
/* Base */
|
/* Base */
|
||||||
@import url("base/misc.css");
|
@import url("base/misc.css");
|
||||||
@import url("base/buttons.css");
|
@import url("base/buttons.css");
|
||||||
|
@import url("base/forms.css");
|
||||||
|
|
||||||
/* Blocks */
|
/* Blocks */
|
||||||
@import url("base/main.css");
|
@import url("base/main.css");
|
||||||
|
@ -35,6 +37,11 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header small {
|
||||||
|
color: #FFF;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
.nopadding {
|
.nopadding {
|
||||||
padding: 0 !important;
|
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 creation */
|
||||||
|
|
||||||
.statement-create-summary table {
|
.statement-create-summary table {
|
||||||
|
|
36
kfet/static/kfet/css/libs/formset-kfet.css
Normal file
36
kfet/static/kfet/css/libs/formset-kfet.css
Normal 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;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
{% extends "kfet/base_col_2.html" %}
|
{% extends "kfet/base_col_2.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}Comptes{% endblock %}
|
{% block title %}Comptes{% endblock %}
|
||||||
{% block header-title %}Comptes{% endblock %}
|
{% block header-title %}Comptes{% endblock %}
|
||||||
|
@ -22,11 +23,11 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if perms.kfet.manage_perms %}
|
{% if perms.kfetauth.view_group %}
|
||||||
<a class="btn btn-primary" href="{% url 'kfet.account.group' %}">Permissions</a>
|
<a class="btn btn-primary" href="{% url 'kfet.group' %}">{% trans "Permissions" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if perms.kfet.view_negs %}
|
{% if perms.kfet.view_accountnegative %}
|
||||||
<a class="btn btn-primary" href="{% url 'kfet.account.negative' %}">Négatifs</a>
|
<a class="btn btn-primary" href="{% url 'kfet.account.negative' %}">Négatifs</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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 %}
|
|
|
@ -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 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-ui.min.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.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/reconnecting-websocket.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/moment.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>
|
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
|
||||||
|
|
19
kfet/templates/kfet/form_field_base_snippet.html
Normal file
19
kfet/templates/kfet/form_field_base_snippet.html
Normal 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 %}
|
|
@ -1,26 +1,24 @@
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
|
{% if not field.label %}
|
||||||
<div class="col-sm-10">
|
{% elif field|widget_type == "checkboxinput" %}
|
||||||
{% if field|widget_type == "checkboxselectmultiple" %}
|
{# label is displayed along the checkbox #}
|
||||||
<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 %}
|
{% else %}
|
||||||
{{ field|add_class:'form-control' }}
|
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">
|
||||||
|
{{ field.label }}
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% 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 %}
|
{% 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|safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form action="" method="post" class="form-horizontal">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "kfet/form_snippet.html" %}
|
{% include "kfet/form_snippet.html" %}
|
||||||
{% if not authz %}
|
{% if not authz %}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
{% for field in form %}
|
<div class="form-horizontal">
|
||||||
|
{% for field in form %}
|
||||||
{% include 'kfet/form_field_snippet.html' with field=field %}
|
{% include 'kfet/form_field_snippet.html' with field=field %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<div class="form-group">
|
{% 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">
|
<div class="col-sm-6 col-sm-offset-3 text-center">
|
||||||
<input type="submit" value="{{ value }}" class="btn btn-primary btn-lg">
|
<input type="submit" value="{% firstof value default_value %}" class="btn btn-primary btn-lg">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
12
kfet/templates/kfet/formset_form_actions.html
Normal file
12
kfet/templates/kfet/formset_form_actions.html
Normal 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>
|
|
@ -1,5 +0,0 @@
|
||||||
from django.template.defaulttags import register
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def get_item(dictionary, key):
|
|
||||||
return dictionary.get(key)
|
|
8
kfet/templatetags/kfet_extras.py
Normal file
8
kfet/templatetags/kfet_extras.py
Normal 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)]
|
|
@ -12,6 +12,16 @@ class AccountTests(TestCase):
|
||||||
self.account = Account(trigramme='000')
|
self.account = Account(trigramme='000')
|
||||||
self.account.save({'username': 'user'})
|
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):
|
def test_password(self):
|
||||||
self.account.change_pwd('anna')
|
self.account.change_pwd('anna')
|
||||||
self.account.save()
|
self.account.save()
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -15,7 +14,7 @@ from ..models import (
|
||||||
SupplierArticle, Transfer, TransferGroup,
|
SupplierArticle, Transfer, TransferGroup,
|
||||||
)
|
)
|
||||||
from .testcases import ViewTestCaseMixin
|
from .testcases import ViewTestCaseMixin
|
||||||
from .utils import create_team, create_user, get_perms
|
from .utils import create_team, create_user
|
||||||
|
|
||||||
|
|
||||||
class AccountListViewTests(ViewTestCaseMixin, TestCase):
|
class AccountListViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
@ -335,146 +334,6 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase):
|
||||||
self.assertForbiddenKfet(r)
|
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):
|
class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase):
|
||||||
url_name = 'kfet.account.negative'
|
url_name = 'kfet.account.negative'
|
||||||
url_expected = '/k-fet/accounts/negatives'
|
url_expected = '/k-fet/accounts/negatives'
|
||||||
|
@ -484,7 +343,8 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
|
||||||
def get_users_extra(self):
|
def get_users_extra(self):
|
||||||
return {
|
return {
|
||||||
'team1': create_team('team1', '101', perms=['kfet.view_negs']),
|
'team1': create_team('team1', '101', perms=[
|
||||||
|
'kfet.view_accountnegative']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
20
kfet/urls.py
20
kfet/urls.py
|
@ -8,8 +8,6 @@ from kfet.decorators import teamkfet_required
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^login/generic$', views.login_generic,
|
|
||||||
name='kfet.login.generic'),
|
|
||||||
url(r'^history$', views.history,
|
url(r'^history$', views.history,
|
||||||
name='kfet.history'),
|
name='kfet.history'),
|
||||||
|
|
||||||
|
@ -50,20 +48,8 @@ urlpatterns = [
|
||||||
url(r'^accounts/(?P<trigramme>.{3})/edit$', views.account_update,
|
url(r'^accounts/(?P<trigramme>.{3})/edit$', views.account_update,
|
||||||
name='kfet.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$',
|
url(r'^accounts/negatives$',
|
||||||
permission_required('kfet.view_negs')
|
permission_required('kfet.view_accountnegative')
|
||||||
(views.AccountNegativeList.as_view()),
|
(views.AccountNegativeList.as_view()),
|
||||||
name='kfet.account.negative'),
|
name='kfet.account.negative'),
|
||||||
|
|
||||||
|
@ -240,6 +226,6 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
# K-Fêt Open urls
|
url(r'^', include('kfet.auth.urls')),
|
||||||
url('^open/', include('kfet.open.urls')),
|
url(r'^open/', include('kfet.open.urls')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,11 +7,10 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from channels.channel import Group
|
from channels.channel import Group
|
||||||
from channels.generic.websockets import JsonWebsocketConsumer
|
from channels.generic.websockets import JsonWebsocketConsumer
|
||||||
|
|
||||||
from .config import kfet_config
|
|
||||||
|
|
||||||
|
|
||||||
def to_ukf(balance, is_cof=False):
|
def to_ukf(balance, is_cof=False):
|
||||||
"""Convert euro to UKF."""
|
"""Convert euro to UKF."""
|
||||||
|
from .config import kfet_config
|
||||||
subvention = kfet_config.subvention_cof
|
subvention = kfet_config.subvention_cof
|
||||||
grant = (1 + subvention / 100) if is_cof else 1
|
grant = (1 + subvention / 100) if is_cof else 1
|
||||||
return math.floor(balance * 10 * grant)
|
return math.floor(balance * 10 * grant)
|
||||||
|
|
|
@ -24,7 +24,9 @@ from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
from gestioncof.models import CofProfile
|
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.decorators import teamkfet_required
|
||||||
from kfet.models import (
|
from kfet.models import (
|
||||||
Account, Checkout, Article, AccountNegative,
|
Account, Checkout, Article, AccountNegative,
|
||||||
|
@ -33,14 +35,14 @@ from kfet.models import (
|
||||||
TransferGroup, Transfer, ArticleCategory)
|
TransferGroup, Transfer, ArticleCategory)
|
||||||
from kfet.forms import (
|
from kfet.forms import (
|
||||||
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
|
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
|
||||||
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
|
UserRestrictTeamForm, AccountForm, CofRestrictForm,
|
||||||
AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm,
|
AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm,
|
||||||
CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
|
CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
|
||||||
CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
|
CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
|
||||||
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
|
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
|
||||||
KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
|
KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
|
||||||
TransferFormSet, InventoryArticleForm, OrderArticleForm,
|
TransferFormSet, InventoryArticleForm, OrderArticleForm,
|
||||||
OrderArticleToInventoryForm, CategoryForm, KFetConfigForm
|
OrderArticleToInventoryForm, CategoryForm,
|
||||||
)
|
)
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from kfet import consumers
|
from kfet import consumers
|
||||||
|
@ -50,10 +52,6 @@ import heapq
|
||||||
import statistics
|
import statistics
|
||||||
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
|
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):
|
def put_cleaned_data_in_dict(dict, form):
|
||||||
for field in form.cleaned_data:
|
for field in form.cleaned_data:
|
||||||
|
|
|
@ -29,5 +29,9 @@ wagtailmenus==2.2.*
|
||||||
# Remove this when we switch to Django 1.11
|
# Remove this when we switch to Django 1.11
|
||||||
djangorestframework==3.6.4
|
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
|
# Production tools
|
||||||
wheel
|
wheel
|
||||||
|
|
3
setup.cfg
Normal file
3
setup.cfg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[flake8]
|
||||||
|
# E731: lambda expressions
|
||||||
|
ignore = E731
|
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
61
utils/forms.py
Normal file
61
utils/forms.py
Normal 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
0
utils/tests/__init__.py
Normal file
141
utils/tests/test_forms.py
Normal file
141
utils/tests/test_forms.py
Normal 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]),
|
||||||
|
)
|
Loading…
Reference in a new issue