WIP: Les permissions du CMS peuvent être attribuées aux groupes K-Fêt (depuis le site K-Fêt) + quelques améliorations #565

Draft
delobell wants to merge 17 commits from aureplop/kfet-auth_cms into aureplop/kfet-auth
50 changed files with 1814 additions and 411 deletions

View file

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

View file

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

View file

@ -1,23 +1,74 @@
import re
from itertools import groupby
from operator import attrgetter
from django import forms from django import forms
from django.forms import widgets from django.utils.translation import ugettext_lazy as _
from .models import Group, Permission from .models import Group, Permission
DEFAULT_PERMS = ['view', 'add', 'change', 'delete']
DEFAULT_PERMS_LABELS = {
'view': _("Voir"),
'add': _("Ajouter"),
'change': _("Modifier"),
'delete': _("Supprimer"),
}
class GroupsField(forms.ModelMultipleChoiceField): class GroupsField(forms.ModelMultipleChoiceField):
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs.setdefault('queryset', Group.objects.all()) kwargs.setdefault('queryset', Group.objects.all())
kwargs.setdefault('widget', widgets.CheckboxSelectMultiple) kwargs.setdefault('widget', forms.CheckboxSelectMultiple)
super().__init__(**kwargs) super().__init__(**kwargs)
class BasePermissionsField(forms.ModelMultipleChoiceField): class BasePermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs.setdefault('widget', widgets.CheckboxSelectMultiple) kwargs.setdefault('widget', forms.CheckboxSelectMultiple(attrs={
'field_class': 'permissions-field',
}))
super().__init__(**kwargs) super().__init__(**kwargs)
def label_from_instance(self, obj): # Contain permissions grouped by `ContentType`. Used as choices for
return obj.name # this field.
grouped_choices = []
for ct, perms in groupby(self.queryset, attrgetter('content_type')):
model_opts = ct.model_class()._meta
choices = []
# This helps for the default permissions, if they exists, to appear
# at the beginning of the permissions list and use custom labels.
reg_defaults_p = re.compile(
r'^(?P<p_type>({defaults}))_{model_name}$'
.format(
defaults='|'.join(DEFAULT_PERMS),
model_name=model_opts.model_name,
)
)
next_default_pos = 0
for p in perms:
match = reg_defaults_p.match(p.codename)
if match:
# `p` is a default permission. Use shorter label and insert
# after the last default permission seen in `choices`.
p_type = match.group('p_type')
choices.insert(
next_default_pos,
(p.id, DEFAULT_PERMS_LABELS[p_type])
)
next_default_pos += 1
else:
# Non-default permissions. Use the permission description,
# instead of its `__str__`.
choices.append((p.id, p.name))
grouped_choices.append((model_opts.verbose_name_plural, choices))
self.choices = grouped_choices
class CorePermissionsField(BasePermissionsField): class CorePermissionsField(BasePermissionsField):

View file

@ -4,13 +4,12 @@ from django.utils.translation import ugettext_lazy as _
from utils.forms import KeepUnselectableModelFormMixin from utils.forms import KeepUnselectableModelFormMixin
from .fields import GroupsField, CorePermissionsField from .fields import GroupsField, CorePermissionsField
from .models import Group from .models import Group
class GroupForm(KeepUnselectableModelFormMixin, forms.ModelForm): class GroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
permissions = CorePermissionsField(label=_("Permissions"), required=False) permissions = CorePermissionsField(label='', required=False)
keep_unselectable_fields = ['permissions'] keep_unselectable_fields = ['permissions']

View file

@ -21,26 +21,4 @@ class Migration(migrations.Migration):
('token', models.CharField(unique=True, max_length=50)), ('token', models.CharField(unique=True, max_length=50)),
], ],
), ),
migrations.CreateModel(
name='Group',
fields=[
('group_ptr', models.OneToOneField(parent_link=True, serialize=False, primary_key=True, auto_created=True, to='auth.Group')),
],
options={
'verbose_name': 'Groupe',
'verbose_name_plural': 'Groupes',
},
bases=('auth.group',),
),
migrations.CreateModel(
name='Permission',
fields=[
],
options={
'verbose_name': 'Permission',
'verbose_name_plural': 'Permissions',
'proxy': True,
},
bases=('auth.permission',),
),
] ]

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfetauth', '0001_initial'),
]
operations = [
# See also `kfetauth.0004` migration which deletes already created
# permissions, if applicable.
migrations.AlterModelOptions(
name='genericteamtoken',
options={
'default_permissions': (),
},
),
# See also `kfetauth.0003` migration which imports existing K-Fêt
# groups.
# See also `kfetauth.0004` migration which gives the default
# permissions to `Group` objects which have the deleted
# `kfet.manage_perms` permission.
migrations.CreateModel(
name='Group',
fields=[
('group_ptr', models.OneToOneField(
parent_link=True,
serialize=False,
primary_key=True,
auto_created=True,
to='auth.Group',
)),
],
options={
'default_permissions': ('view', 'add', 'change'),
'verbose_name': 'Groupe',
'verbose_name_plural': 'Groupes',
},
bases=('auth.group',),
),
migrations.CreateModel(
name='Permission',
fields=[
],
options={
'verbose_name': 'Permission',
'verbose_name_plural': 'Permissions',
'proxy': True,
},
bases=('auth.permission',),
),
]

View file

@ -22,7 +22,8 @@ class Migration(migrations.Migration):
""" """
dependencies = [ dependencies = [
('kfetauth', '0001_initial'), ('kfetauth', '0002_create_group_permission_models'),
('auth', '0006_require_contenttypes_0002'),
] ]
operations = [ operations = [

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from django.db import migrations
from django.db.models import Q
def convert_manage_perms(apps, schema_editor):
"""
Use the permissions of `kfet.auth.models.Group` model, instead of using the
`manage_perms` permission, which is deleted, of `kfet.models.Account`.
`Group` which have this last permission will get the permissions:
`kfetauth.view_group`, `kfetauth.add_group` and `kfetauth.change_group`.
"""
Group = apps.get_model('auth', 'Group')
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
try:
old_p = Permission.objects.get(
content_type__app_label='kfet',
content_type__model='account',
codename='manage_perms',
)
except Permission.DoesNotExist:
return
groups = old_p.group_set.all()
ct_group, _ = ContentType.objects.get_or_create(
app_label='kfetauth',
model='group',
)
view_p, _ = Permission.objects.get_or_create(
content_type=ct_group, codename='view_group',
defaults={'name': 'Can view Groupe'})
add_p, _ = Permission.objects.get_or_create(
content_type=ct_group, codename='add_group',
defaults={'name': 'Can add Groupe'})
change_p, _ = Permission.objects.get_or_create(
content_type=ct_group, codename='change_group',
defaults={'name': 'Can change Group'})
GroupPermission = Group.permissions.through
GroupPermission.objects.bulk_create([
GroupPermission(permission=p, group=g)
for g in groups
for p in (view_p, add_p, change_p)
])
old_p.delete()
def delete_unused_permissions(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
to_delete_q = Q(content_type__model='genericteamtoken') | Q(
content_type__model='group',
codename='delete_group',
)
to_delete_q &= Q(content_type__app_label='kfetauth')
Permission.objects.filter(to_delete_q).delete()
class Migration(migrations.Migration):
"""
Data migration about permissions.
"""
dependencies = [
('kfetauth', '0003_existing_groups'),
('auth', '0006_require_contenttypes_0002'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.RunPython(convert_manage_perms),
migrations.RunPython(delete_unused_permissions),
]

View file

@ -5,6 +5,8 @@ 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 django.utils.translation import ugettext_lazy as _
from kfet.cms.models import CmsPermissionManager
class GenericTeamTokenManager(models.Manager): class GenericTeamTokenManager(models.Manager):
@ -20,6 +22,9 @@ class GenericTeamToken(models.Model):
objects = GenericTeamTokenManager() objects = GenericTeamTokenManager()
class Meta:
default_permissions = ()
class Group(DjangoGroup): class Group(DjangoGroup):
@ -35,6 +40,7 @@ class Group(DjangoGroup):
class Meta: class Meta:
verbose_name = _("Groupe") verbose_name = _("Groupe")
verbose_name_plural = _("Groupes") verbose_name_plural = _("Groupes")
default_permissions = ('view', 'add', 'change')
KFET_CORE_APP_LABELS = ['kfet', 'kfetauth'] KFET_CORE_APP_LABELS = ['kfet', 'kfetauth']
@ -48,6 +54,7 @@ class CorePermissionManager(models.Manager):
class Permission(DjangoPermission): class Permission(DjangoPermission):
kfetcore = CorePermissionManager() kfetcore = CorePermissionManager()
kfetcms = CmsPermissionManager()
class Meta: class Meta:
proxy = True proxy = True

View file

@ -0,0 +1,58 @@
{% extends "kfet/base_form.html" %}
{% load i18n %}
{% block title %}
{% if not form.instance.pk %}
{% trans "Création d'un groupe" %}
{% else %}
{% blocktrans with name=form.instance.name %}
Modification du groupe "{{ name }}"
{% endblocktrans %}
{% endif %}
{% endblock %}
{% block header-title %}
{% if not form.instance.pk %}
{% trans "Création d'un groupe" %}
{% else %}
{% blocktrans with name=form.instance.name %}
{{ name }}
<small>Modification du groupe</small>
{% endblocktrans %}
{% endif %}
{% endblock %}
{% block main %}
<form action="" method="post" class="group-form">
{% csrf_token %}
{# Base form #}
<div class="form-horizontal">
{% include "kfet/form_snippet.html" with form=form %}
</div>
{# Extra forms #}
{% for extra in extras %}
<h3>
{{ extra.title }}<br>
<small>{{ extra.description }}</small>
</h3>
{% for extra_form in extra.forms %}
<div class="extra-form">
{% with as_panel=extra_form.as_admin_panel %}
{% if as_panel %}
{{ as_panel }}
{% else %}
{% include "kfet/form_snippet.html" with form=extra_form %}
{% endif %}
{% endwith %}
</div>
{% endfor %}
{% endfor %}
{% include "kfet/form_submit_snippet.html" %}
</form>
{% endblock %}

View file

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

View file

@ -10,15 +10,15 @@ from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
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 .fields import GroupsField, CorePermissionsField
from .forms import GroupForm, UserGroupForm
from .middleware import TemporaryAuthMiddleware from .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken, Group, Permission from .models import GenericTeamToken, Group, Permission
from .utils import get_kfet_generic_user from .utils import get_kfet_generic_user
from .views import GenericLoginView
## ##
@ -85,6 +85,7 @@ class GroupFormTests(TestCase):
content_type=ot_ct, codename='cool') content_type=ot_ct, codename='cool')
def test_creation(self): def test_creation(self):
from .forms import GroupForm
data = { data = {
'name': 'Another Group', 'name': 'Another Group',
'permissions': [self.kf_perm1.pk], 'permissions': [self.kf_perm1.pk],
@ -101,6 +102,7 @@ class GroupFormTests(TestCase):
Non-kfet permissions of Group are kept when the form is submitted. Non-kfet permissions of Group are kept when the form is submitted.
Regression test for #168. Regression test for #168.
""" """
from .forms import GroupForm
self.kf_group.permissions.add(self.ot_perm) self.kf_group.permissions.add(self.ot_perm)
selected = [self.kf_perm1, self.kf_perm2] selected = [self.kf_perm1, self.kf_perm2]
@ -139,6 +141,7 @@ class UserGroupFormTests(TestCase):
User stays in its non-K-Fêt groups. User stays in its non-K-Fêt groups.
Regression test for #161. 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
self.user.groups.add(self.ot_group) self.user.groups.add(self.ot_group)
@ -257,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
@ -348,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()
@ -364,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):
""" """
@ -387,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
View file

@ -0,0 +1,18 @@
from django.conf.urls import include, url
from . import views
group_patterns = [
url(r'^$', views.group_index,
name='kfet.group'),
url(r'^nouveau/$', views.group_create,
name='kfet.group.create'),
url('^(?P<pk>\d+)/edition/$', views.group_update,
name='kfet.group.update'),
]
urlpatterns = [
url(r'^groupes/', include(group_patterns)),
url(r'^login/generic', views.login_generic,
name='kfet.login.generic'),
]

View file

@ -1,22 +1,23 @@
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 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, Group from .models import GenericTeamToken, Group
User = get_user_model()
class GenericLoginView(View): class GenericLoginView(View):
""" """
@ -104,38 +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', {
.prefetch_related('permissions', user_pre)
)
return render(request, 'kfet/account_group.html', {
'groups': groups, 'groups': groups,
}) })
class BaseAccountGroupFormViewMixin: _group_formview_extras = None
model = Group
form_class = GroupForm
template_name = 'kfet/account_group_form.html'
success_url = reverse_lazy('kfet.account.group')
class AccountGroupFormViewMixin( def get_group_formview_extras():
SuccessMessageMixin, global _group_formview_extras
BaseAccountGroupFormViewMixin,
): if _group_formview_extras is None:
pass _group_formview_extras = []
# Register additional group forms below.
_group_formview_extras.append(
get_kfetcms_group_formview_extra())
return [extra.copy() for extra in _group_formview_extras]
class AccountGroupCreate(AccountGroupFormViewMixin, CreateView): @permission_required('kfetauth.add_group')
success_message = 'Nouveau groupe : %(name)s' def group_create(request):
group = Group()
extras = get_group_formview_extras()
if request.method == 'POST':
form = GroupForm(request.POST, instance=group)
for extra in extras:
extra['forms'] = [
form_cls(
request.POST, request.FILES, instance=group, **form_kwargs)
for form_cls, form_kwargs in extra['form_classes']
]
extra_forms = sum((extra['forms'] for extra in extras), [])
if form.is_valid() and all(form.is_valid() for form in extra_forms):
group = form.save()
for extra_form in extra_forms:
extra_form.save()
messages.success(request, _("Nouveau groupe : {}").format(group))
return redirect('kfet.group')
else:
form = GroupForm(instance=group)
for extra in extras:
extra['forms'] = [
form_cls(instance=group, **form_kwargs)
for form_cls, form_kwargs in extra['form_classes']
]
return render(request, 'kfet/group_form.html', {
'form': form,
'extras': extras,
})
class AccountGroupUpdate(AccountGroupFormViewMixin, UpdateView): @permission_required('kfetauth.change_group')
success_message = 'Groupe modifié : %(name)s' def group_update(request, pk):
group = get_object_or_404(Group, pk=pk)
extras = get_group_formview_extras()
if request.method == 'POST':
form = GroupForm(request.POST, instance=group)
for extra in extras:
extra['forms'] = [
form_cls(
request.POST, request.FILES, instance=group, **form_kwargs)
for form_cls, form_kwargs in extra['form_classes']
]
extra_forms = sum((extra['forms'] for extra in extras), [])
if form.is_valid() and all(form.is_valid() for form in extra_forms):
group = form.save()
for extra_form in extra_forms:
extra_form.save()
messages.success(request, _("Groupe modifié : {}").format(group))
return redirect('kfet.group')
else:
form = GroupForm(instance=group)
for extra in extras:
extra['forms'] = [
form_cls(instance=group, **form_kwargs)
for form_cls, form_kwargs in extra['form_classes']
]
return render(request, 'kfet/group_form.html', {
'form': form,
'extras': extras,
})

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

@ -0,0 +1,196 @@
from django import forms
from django.contrib.auth.models import Group
from django.template.loader import render_to_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailusers.forms import GroupPagePermissionFormSet
from utils.forms import KeepUnselectableModelFormMixin
from kfet.auth.fields import BasePermissionsField
from kfet.auth.models import Permission
class CmsGroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
"""
Allow to change permissions related to the cms of a `Group` object.
"""
access_admin = forms.BooleanField(
label=_("Accès à l'administration"),
required=False,
help_text=_(
"<b>Attention :</b> Aucun paramétrage effectué dans cette section "
"n'aura d'effet si cette case est décochée."
),
)
class Meta:
model = Group
fields = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
group = self.instance
if group.pk is not None:
self.fields['access_admin'].initial = (
group.permissions.filter(pk=self.access_admin_perm.pk).exists()
)
def save(self):
group = super().save()
if self.cleaned_data['access_admin']:
group.permissions.add(self.access_admin_perm)
else:
group.permissions.remove(self.access_admin_perm)
return group
@cached_property
def access_admin_perm(self):
return Permission.objects.get(
content_type__app_label='wagtailadmin',
codename='access_admin',
)
class SnippetsCmsGroupForm(KeepUnselectableModelFormMixin, forms.ModelForm):
permissions = BasePermissionsField(
label='', required=False,
queryset=Permission.kfetcms.all(),
)
keep_unselectable_fields = ['permissions']
class Meta:
model = Group
fields = ('permissions',)
def prepare_page_permissions_formset(pages):
"""
Create a new formset from base `GroupPagePermissionFormSet` Wagtail
formset.
- The choices of `pages` field of forms are limited to `pages`.
- If there is no initial data, add an initial with the first collection
selected.
- Make 'as_admin_panel' use a custom template.
Arguments
pages (iterable of `Page`)
Returns
A formset to select group permissions of pages.
"""
class RestrictedFormSet(GroupPagePermissionFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove forms cache, as theirs options are changed.
del self.forms
# 'initial' must be setup before accessing 'forms'.
self.initial = list(
filter(lambda i: i['page'] in pages, self.initial))
# This is just little kindness.
if not self.initial and pages:
self.initial = [{'page': pages[0]}]
for form in self.forms:
self.customize_form(form)
@property
def empty_form(self):
form = super().empty_form
self.customize_form(form)
return form
def customize_form(self, form):
# Widget must be setup before queryset.
form.fields['page'].widget = forms.Select()
form.fields['page'].queryset = pages
# Force use of `CheckboxInput` for `DELETE` field, as `HiddenInput`
# is not compatible with `django-formset-js`.
form.fields['DELETE'].widget = forms.CheckboxInput()
def as_admin_panel(self):
# http://docs.wagtail.io/en/latest/reference/hooks.html#register-group-permission-panel
template = 'kfet/permissions/page_permissions_formset.html'
return render_to_string(template, {'formset': self})
return RestrictedFormSet
def prepare_collection_member_permissions_formset(formset_cls, collections):
"""
Create a new formset from base `formset_cls`.
- The choices of `collections` field of forms produced by `formset_cls` are
limited to `collections`.
- If there is no initial data, add an initial with the first collection
selected.
- Make 'as_admin_panel' use a custom template.
Arguments
formset_cls (subclass of
`wagtail.wagtailcore.forms`
`.BaseGroupCollectionMemberPermissionFormSet`
):
Formset to select group permissions of a collection member model.
It should be rerieved from the `group_permission_panel` Wagtail
hook. This includes the `Document` and `Image` models of Wagtail.
collections (iterable of `wagtail.wagtailcore.models.Collection`)
Returns
A formset to select group permissions of collection-related models.
"""
class RestrictedFormSet(formset_cls):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove forms cache, as theirs options are changed.
del self.forms
# 'initial' must be setup before accessing 'forms'.
self.initial = list(
filter(lambda i: i['collection'] in collections, self.initial))
# This is just little kindness.
if not self.initial and collections:
self.initial = [{'collection': collections[0]}]
for form in self.forms:
self.customize_form(form)
@property
def empty_form(self):
form = super().empty_form
self.customize_form(form)
return form
def customize_form(self, form):
form.fields['collection'].queryset = collections
# Force use of `CheckboxInput` for `DELETE` field, as `HiddenInput`
# is not compatible with `django-formset-js`.
form.fields['DELETE'].widget = forms.CheckboxInput()
def as_admin_panel(self):
template = (
'kfet/permissions/collection_member_permissions_formset.html')
model = self.permission_queryset[0].content_type.model_class()
return render_to_string(template, {
'formset': self,
'model_name': model._meta.verbose_name_plural,
})
return RestrictedFormSet

View file

@ -1,8 +1,14 @@
from django.contrib.auth.models import Group
from django.core.management import call_command from django.core.management 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
])

View file

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

View file

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

View file

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

View file

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

View file

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

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

@ -0,0 +1,35 @@
from django.apps import apps
from wagtail.wagtailcore.models import Collection, Page
def get_kfet_root_page():
"""
Returns the K-Fêt root page, or 'None' if it does not exist.
"""
from .models import KFetPage
return KFetPage.objects.first()
def get_kfet_root_collection():
"""
Returns the K-Fêt root collection, or 'None' if it does not exist.
"""
return Collection.objects.filter(name='K-Fêt').first()
def get_page_model_names():
"""
Returns all model names of `Page` subclasses.
This uses the django apps registry (instead of `ContentType.objects.all()`)
in order to be usuable even before migrations are applied. E.g. this can be
used in `Field.__init__`.
Note these model names are the same in `model` attribute of `ContentType`
objects.
"""
return [
model._meta.model_name
for model in apps.get_models() if issubclass(model, Page)
]

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

@ -0,0 +1,74 @@
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.forms import (
BaseGroupCollectionMemberPermissionFormSet,
)
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Collection, Page
from .utils import get_kfet_root_collection, get_kfet_root_page
def get_kfetcms_group_formview_extra():
# Prevents querying the database too soon.
from .forms import (
CmsGroupForm, SnippetsCmsGroupForm, prepare_page_permissions_formset,
prepare_collection_member_permissions_formset,
)
forms = []
# Misc cms-related permissions.
forms.append((CmsGroupForm, {'prefix': 'kfetcms'}))
# Snippets permissions.
forms.append((SnippetsCmsGroupForm, {'prefix': 'kfetcms-snippets'}))
# Setup PagePermissionsFormSet for kfet root page descendants only.
root_page = get_kfet_root_page()
if root_page:
pages = Page.objects.descendant_of(root_page, inclusive=True)
forms.append(
(prepare_page_permissions_formset(pages=pages), {})
)
# Setup CollectionPermissions for kfet root collection descendants only.
root_collection = get_kfet_root_collection()
if root_collection:
collections = Collection.objects.descendant_of(
root_collection, inclusive=True)
# Retrieve forms based on CollectionMemberPermissionFormSet displayed
# by Wagtail admin site.
# http://docs.wagtail.io/en/stable/reference/hooks.html#register-group-permission-panel
collectionmember_form_classes = []
for fn in hooks.get_hooks('register_group_permission_panel'):
form_cls = fn()
if issubclass(
form_cls,
BaseGroupCollectionMemberPermissionFormSet
):
collectionmember_form_classes.append(form_cls)
# Apply choices limit.
for form_cls in collectionmember_form_classes:
forms.append((
prepare_collection_member_permissions_formset(
form_cls, collections=collections),
{},
))
# The 'extra' definition of kfetcms.
extra = {
'title': _("Site"),
'description': _(
"Permissions liées aux pages à contenu libre."
),
'form_classes': forms,
}
return extra

View file

@ -1,9 +1,15 @@
# -*- coding: utf-8 -*- # -*- 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),
)

View file

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

View file

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

View file

@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0061_add_perms_config'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={
'permissions': (
('is_team', "Membre de l'équipe"),
('manage_addcosts', 'Gérer les majorations'),
(
'edit_balance_account',
"Modifier la balance d'un compte"
),
(
'change_account_password',
"Modifier le mot de passe d'une personne de l'équipe"
),
(
'special_add_account',
'Créer un compte avec une balance initiale'
),
('can_force_close', 'Fermer manuellement la K-Fêt')
),
'default_permissions': ('add', 'change'),
'verbose_name_plural': 'Comptes',
'verbose_name': 'Compte',
},
),
migrations.AlterModelOptions(
name='accountnegative',
options={
'default_permissions': ('view', 'change',),
'verbose_name_plural': 'Comptes en négatif',
'verbose_name': 'Compte en négatif',
},
),
migrations.AlterModelOptions(
name='article',
options={
'default_permissions': ('add', 'change'),
'verbose_name_plural': 'Articles',
'verbose_name': 'Article',
},
),
migrations.AlterModelOptions(
name='articlecategory',
options={
'default_permissions': ('change',),
'verbose_name_plural': "Catégories d'articles",
'verbose_name': "Catégorie d'articles",
},
),
migrations.AlterModelOptions(
name='articlerule',
options={
'default_permissions': (),
},
),
migrations.AlterModelOptions(
name='checkout',
options={
'default_permissions': ('add', 'change'),
'ordering': ['-valid_to'],
'verbose_name_plural': 'Caisses',
'verbose_name': 'Caisse',
},
),
migrations.AlterModelOptions(
name='checkoutstatement',
options={
'default_permissions': ('add', 'change'),
'verbose_name_plural': 'Relevés de caisses',
'verbose_name': 'Relevé de caisse',
},
),
migrations.AlterModelOptions(
name='checkouttransfer',
options={
'default_permissions': (),
},
),
migrations.AlterModelOptions(
name='inventory',
options={
'permissions': (
(
'order_to_inventory',
"Générer un inventaire à partir d'une commande"
),
),
'ordering': ['-at'],
'verbose_name_plural': 'Inventaires',
'default_permissions': ('add',),
'verbose_name': 'Inventaire',
},
),
migrations.AlterModelOptions(
name='inventoryarticle',
options={
'default_permissions': (),
},
),
migrations.AlterModelOptions(
name='operation',
options={
'permissions': (
('perform_deposit', 'Effectuer une charge'),
(
'perform_negative_operations',
'Enregistrer des commandes en négatif'
),
(
'override_frozen_protection',
"Forcer le gel d'un compte"
),
(
'cancel_old_operations',
'Annuler des commandes non récentes'
),
(
'perform_commented_operations',
'Enregistrer des commandes avec commentaires'
),
),
'default_permissions': (),
'verbose_name_plural': 'Opérations',
'verbose_name': 'Opération',
},
),
migrations.AlterModelOptions(
name='operationgroup',
options={
'default_permissions': (),
},
),
migrations.AlterModelOptions(
name='order',
options={
'default_permissions': ('add',),
'ordering': ['-at'],
'verbose_name_plural': 'Commandes',
'verbose_name': 'Commande',
},
),
migrations.AlterModelOptions(
name='orderarticle',
options={
'default_permissions': (),
},
),
migrations.AlterModelOptions(
name='supplier',
options={
'default_permissions': ('change',),
'verbose_name_plural': 'Fournisseurs',
'verbose_name': 'Fournisseur',
},
),
migrations.AlterModelOptions(
name='supplierarticle',
options={
'default_permissions': (),
},
),
migrations.AlterModelOptions(
name='transfer',
options={
'default_permissions': ('add',),
'verbose_name_plural': 'Transferts',
'verbose_name': 'Transfert',
},
),
migrations.AlterModelOptions(
name='transfergroup',
options={
'default_permissions': (),
},
),
]

View file

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from django.db.models import Q
def update_permissions(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
Account = apps.get_model('kfet', 'Account')
# If `kfet.is_team` permission exists, rename it.
Permission.objects.filter(
content_type__app_label='kfet',
content_type__model='account',
codename='is_team',
).update(name="Membre de l'équipe")
# If `kfet.view_negs` permission exists, move it as
# `kfet.view_accountnegative`.
try:
view_negs_p = Permission.objects.get(
content_type__app_label='kfet',
content_type__model='accountnegative',
codename='view_negs',
)
except Permission.DoesNotExist:
pass
else:
# Avoid failure due to unicity constraint if migrations were partially
# applied.
# Because `view_negs` still exists here, it should be safe to consider
# that nothing uses `view_accountnegative` so that it can be deleted.
Permission.objects.filter(
content_type__app_label='kfet',
content_type__model='accountnegative',
codename='view_accountnegative',
).delete()
view_negs_p.codename = 'view_accountnegative'
view_negs_p.name = 'Can view Compte en négatif'
view_negs_p.save()
# Delete unused permissions.
to_delete = {
'account': ['delete_account'],
'accountnegative': [
'add_accountnegative', 'delete_accountnegative', 'view_negs'],
'article': ['delete_article'],
'articlecategory': ['add_articlecategory', 'delete_articlecategory'],
'articlerule': '__all__',
'checkout': ['delete_checkout'],
'checkoutstatement': ['delete_checkoutstatement'],
'checkouttransfer': '__all__',
'inventory': ['change_inventory', 'delete_inventory'],
'inventoryarticle': '__all__',
'operation': ['add_operation', 'change_operation', 'delete_operation'],
'operationgroup': '__all__',
'order': ['change_order', 'delete_order'],
'orderarticle': '__all__',
'supplier': ['add_supplier', 'delete_supplier'],
'supplierarticle': '__all__',
'transfer': ['change_transfer', 'delete_transfer'],
'transfergroup': '__all__',
}
to_delete_q = Q()
for model_name, codenames in to_delete.items():
if codenames == '__all__':
to_delete_q |= Q(content_type__model=model_name)
else:
to_delete_q |= Q(
content_type__model=model_name,
codename__in=codenames,
)
to_delete_q &= Q(content_type__app_label='kfet')
Permission.objects.filter(to_delete_q).delete()
class Migration(migrations.Migration):
"""
Data migration which performs permissions cleaning.
"""
dependencies = [
('kfet', '0062_change_models_opts'),
('auth', '0006_require_contenttypes_0002'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.RunPython(update_permissions),
]

View file

@ -17,7 +17,6 @@ import re
from .auth import KFET_GENERIC_TRIGRAMME from .auth import KFET_GENERIC_TRIGRAMME
from .auth.models import GenericTeamToken, Group, Permission # 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',

View file

@ -0,0 +1,52 @@
.extra-form {
margin-bottom: 15px;
}
/* Checkbox select multiple field */
.checkbox-select-multiple > ul,
.checkbox-select-multiple > ul > li > ul {
padding-left: 0;
list-style-type: none;
}
.checkbox-select-multiple label {
font-weight: normal;
}
/* Permissions field */
.permissions-field > ul {
font-weight: bold;
}
.permissions-field > ul > li {
margin-bottom: 15px;
}
.permissions-field > ul > li > ul {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
padding: 5px 10px 0;
border: 1px solid #CCC;
border-radius: 3px;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
}
.permissions-field > ul > li > ul > li {
float: left;
flex: 0 1 100%;
}
.permissions-field > ul > li > ul > li:not(:last-child) {
margin-right: 15px;
}
@media (min-width: 768px) {
.permissions-field > ul > li > ul > li { flex: 0 1 auto; }
}

View file

@ -4,10 +4,12 @@
/* Libs customizations */ /* 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 {

View file

@ -0,0 +1,36 @@
[data-formset-form] {
margin-bottom: 5px;
padding: 10px;
border: 1px solid #CCC;
border-radius: 3px;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
transition: border 0.3s, color 0.3s;
}
@media (min-width: 768px) {
[data-formset-form] > :not(:last-child) {
margin-right: 15px;
}
[data-formset-form] .form-actions {
float: right;
margin-left: 15px;
margin-right: 0;
}
}
[data-formset-add] {
position: relative;
top: -5px;
right: 0;
}
[data-formset-form-deleted] {
background: repeating-linear-gradient(
45deg,
#e9afb9, #e9afb9 2px, transparent 2px, transparent 12px
);
border: 1px solid #e9afb9;
color: #999;
}

View file

@ -1,4 +1,5 @@
{% extends "kfet/base_col_2.html" %} {% 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>

View file

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

View file

@ -1,10 +0,0 @@
{% extends 'kfet/base_form.html' %}
{% block title %}Permissions - Édition{% endblock %}
{% block header-title %}Modification des permissions{% endblock %}
{% block main %}
{% include "kfet/form_full_snippet.html" with authz=perms.kfet.manage_perms submit_text="Enregistrer" %}
{% endblock %}

View file

@ -24,6 +24,7 @@
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script 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>

View file

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

View file

@ -1,26 +1,24 @@
{% load widget_tweaks %} {% 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"> {% else %}
{% for choice in field %} <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">
<li class="col-sm-6 col-lg-4"> {{ field.label }}
<label for="{{ choice.id_for_label }}"> </label>
{{ choice.tag }} {{ choice.choice_label }} {% endif %}
</label>
</li> <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 %}">
{% endfor %}
</ul> {% include "kfet/form_field_base_snippet.html" with field=field %}
{% else %}
{{ field|add_class:'form-control' }}
{% endif %}
{% if field.errors %} {% if field.errors %}
<span class="help-block">{{ field.errors }}</span> <span class="help-block">{{ field.errors }}</span>
{% endif %} {% endif %}
{% if field.help_text %} {% if field.help_text %}
<span class="help-block">{{ field.help_text }}</span> <span class="help-block">{{ field.help_text|safe }}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
from django.template.defaulttags import register
@register.filter
def get(o, key):
if hasattr(o, 'get'):
return o.get(key)
return o[int(key)]

View file

@ -12,6 +12,16 @@ class AccountTests(TestCase):
self.account = Account(trigramme='000') self.account = 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()

View file

@ -7,8 +7,6 @@ from django.core.urlresolvers import reverse
from django.test import Client, TestCase from django.test import Client, TestCase
from django.utils import timezone from django.utils import timezone
from kfet.auth.models import Group
from ..config import kfet_config from ..config import kfet_config
from ..models import ( from ..models import (
Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory, Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory,
@ -16,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):
@ -336,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='The Group')
self.assertQuerysetEqual(
group.permissions.all(),
map(repr, [
self.perms['kfet.is_team'],
self.perms['kfet.manage_perms'],
]),
ordered=False,
)
class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase):
url_name = 'kfet.account.group.update'
http_methods = ['GET', 'POST']
auth_user = 'team1'
auth_forbidden = [None, 'user', 'team']
@property
def url_kwargs(self):
return {'pk': self.group.pk}
@property
def url_expected(self):
return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk)
def get_users_extra(self):
return {
'team1': create_team('team1', '101', perms=['kfet.manage_perms']),
}
@property
def post_data(self):
return {
'name': 'The Group',
'permissions': [
str(self.perms['kfet.is_team'].pk),
str(self.perms['kfet.manage_perms'].pk),
],
}
def setUp(self):
super().setUp()
self.perms = get_perms(
'kfet.is_team',
'kfet.manage_perms',
)
self.group = Group.objects.create(name='K-Fêt - Group')
self.group.permissions = self.perms.values()
def test_get_ok(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post_ok(self):
r = self.client.post(self.url, self.post_data)
self.assertRedirects(r, reverse('kfet.account.group'))
self.group.refresh_from_db()
self.assertEqual(self.group.name, 'The Group')
self.assertQuerysetEqual(
self.group.permissions.all(),
map(repr, [
self.perms['kfet.is_team'],
self.perms['kfet.manage_perms'],
]),
ordered=False,
)
class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): 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'
@ -485,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):

View file

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

View file

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

View file

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

View file

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