Merge branch 'beta-soon' into 'master'

Pre-beta fixes

Closes #51

See merge request bde/nk20!86
This commit is contained in:
ynerant 2020-08-01 16:12:09 +02:00
commit e50bff8e14
100 changed files with 5020 additions and 2196 deletions

3
.gitignore vendored
View File

@ -36,8 +36,11 @@ coverage
# Local data
secrets.py
.env
map.json
*.log
media/
# Virtualenv
env/
venv/

View File

@ -1,4 +1,4 @@
image: python:3.6
image: python:3.8
stages:
- test
@ -17,8 +17,13 @@ py37-django22:
stage: test
script: tox -e py37-django22
py38-django22:
image: python:3.8
stage: test
script: tox -e py38-django22
linters:
image: python:3.6
image: python:3.8
stage: quality-assurance
script: tox -e linters

View File

@ -11,11 +11,15 @@
git:
repo: https://gitlab.crans.org/bde/nk20.git
dest: /var/www/note_kfet
version: beta-soon
version: master
force: true
- name: Use default env vars (should be updated!)
command: cp /var/www/note_kfet/.env_example /var/www/note_kfet/.env
template:
src: "env_example"
dest: "/var/www/note_kfet/.env"
mode: 0644
force: false
- name: Update permissions for note_kfet dir
file:

View File

@ -0,0 +1 @@
../../../../.env_example

View File

@ -15,6 +15,11 @@
group: www-data
state: link
- name: Disable default Nginx site
file:
dest: /etc/nginx/sites-enabled/default
state: absent
- name: Copy conf of UWSGI
file:
src: /var/www/note_kfet/uwsgi_note.ini

View File

@ -0,0 +1,63 @@
# the upstream component nginx needs to connect to
upstream note{
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
}
# Redirect HTTP to nk20 HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
return 301 https://nk20-beta.crans.org$request_uri;
}
}
# Redirect all HTTPS to nk20 HTTPS
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
location / {
return 301 https://nk20-beta.crans.org$request_uri;
}
ssl_certificate /etc/letsencrypt/live/nk20-beta.crans.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nk20-beta.crans.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# configuration of the server
server {
listen 443 ssl;
listen [::]:443 ssl;
# the port your site will be served on
# the domain name it will serve for
server_name nk20-beta.crans.org; # substitute your machine's IP address or FQDN
charset utf-8;
# max upload size
client_max_body_size 75M; # adjust to taste
# Django media
location /media {
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
}
location /static {
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
}
# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass note;
include /var/www/note_kfet/uwsgi_params; # the uwsgi_params file you installed
}
ssl_certificate /etc/letsencrypt/live/nk20-beta.crans.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nk20-beta.crans.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

View File

@ -163,7 +163,7 @@ class Entry(models.Model):
amount=self.activity.activity_type.guest_entry_fee,
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
valid=True,
guest=self.guest,
entry=self,
).save()
return ret
@ -240,8 +240,8 @@ class Guest(models.Model):
class GuestTransaction(Transaction):
guest = models.OneToOneField(
Guest,
entry = models.OneToOneField(
Entry,
on_delete=models.PROTECT,
)

View File

@ -23,6 +23,7 @@ from .tables import ActivityTable, GuestTable, EntryTable
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Activity
form_class = ActivityForm
extra_context = {"title": _("Create new activity")}
def form_valid(self, form):
form.instance.creater = self.request.user
@ -37,12 +38,14 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
model = Activity
table_class = ActivityTable
ordering = ('-date_start',)
extra_context = {"title": _("Activities")}
def get_queryset(self):
return super().get_queryset().distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Activities")
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
@ -55,6 +58,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Activity
context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
def get_context_data(self, **kwargs):
context = super().get_context_data()
@ -71,6 +75,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Activity
form_class = ActivityForm
extra_context = {"title": _("Update activity")}
def get_success_url(self, **kwargs):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
@ -81,6 +86,12 @@ class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
form_class = GuestForm
template_name = "activity/activity_invite.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
activity = context["form"].activity
context["title"] = _('Invite guest to the activity "{}"').format(activity.name)
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\

View File

@ -14,9 +14,11 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
user = get_current_authenticated_user()
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
@ -26,6 +28,8 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
user = get_current_authenticated_user()
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))

View File

@ -4,9 +4,12 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note.templatetags.pretty_money import pretty_money
from note_kfet.admin import admin_site
from .forms import ProfileForm
from .models import Club, Membership, Profile, Role
from .models import Club, Membership, Profile
class ProfileInline(admin.StackedInline):
@ -17,6 +20,7 @@ class ProfileInline(admin.StackedInline):
can_delete = False
@admin.register(User, site=admin_site)
class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
@ -32,11 +36,33 @@ class CustomUserAdmin(UserAdmin):
return super().get_inline_instances(request, obj)
# Update Django User with profile
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
@admin.register(Club, site=admin_site)
class ClubAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_club', 'email', 'require_memberships', 'pretty_fee_paid',
'pretty_fee_unpaid', 'membership_start', 'membership_end',)
ordering = ('name',)
search_fields = ('name', 'email',)
# Add other models
admin.site.register(Club)
admin.site.register(Membership)
admin.site.register(Role)
def pretty_fee_paid(self, obj):
return pretty_money(obj.membership_fee_paid)
def pretty_fee_unpaid(self, obj):
return pretty_money(obj.membership_fee_unpaid)
pretty_fee_paid.short_description = _("membership fee (paid students)")
pretty_fee_unpaid.short_description = _("membership fee (unpaid students)")
@admin.register(Membership, site=admin_site)
class MembershipAdmin(admin.ModelAdmin):
list_display = ('user', 'club', 'date_start', 'date_end', 'view_roles', 'pretty_fee',)
ordering = ('-date_start', 'club')
def view_roles(self, obj):
return ", ".join(role.name for role in obj.roles.all())
def pretty_fee(self, obj):
return pretty_money(obj.fee)
view_roles.short_description = _("roles")
pretty_fee.short_description = _("fee")

View File

@ -3,7 +3,7 @@
from rest_framework import serializers
from ..models import Profile, Club, Role, Membership
from ..models import Profile, Club, Membership
class ProfileSerializer(serializers.ModelSerializer):
@ -29,17 +29,6 @@ class ClubSerializer(serializers.ModelSerializer):
fields = '__all__'
class RoleSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Roles.
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
"""
class Meta:
model = Role
fields = '__all__'
class MembershipSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Memberships.

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
def register_members_urls(router, path):
@ -10,5 +10,4 @@ def register_members_urls(router, path):
"""
router.register(path + '/profile', ProfileViewSet)
router.register(path + '/club', ClubViewSet)
router.register(path + '/role', RoleViewSet)
router.register(path + '/membership', MembershipViewSet)

View File

@ -4,8 +4,8 @@
from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
from ..models import Profile, Club, Role, Membership
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
from ..models import Profile, Club, Membership
class ProfileViewSet(ReadProtectedModelViewSet):
@ -30,18 +30,6 @@ class ClubViewSet(ReadProtectedModelViewSet):
search_fields = ['$name', ]
class RoleViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
then render it on /api/members/role/
"""
queryset = Role.objects.all()
serializer_class = RoleSerializer
filter_backends = [SearchFilter]
search_fields = ['$name', ]
class MembershipViewSet(ReadProtectedModelViewSet):
"""
REST API View set.

View File

@ -5,11 +5,11 @@ from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial
from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask
from permission.models import PermissionMask, Role
from .models import Profile, Club, Membership, Role
from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm):
@ -20,6 +20,18 @@ class CustomAuthenticationForm(AuthenticationForm):
)
class UserForm(forms.ModelForm):
def _get_validation_exclusions(self):
# Django usernames can only contain letters, numbers, @, ., +, - and _.
# We want to allow users to have uncommon and unpractical usernames:
# That is their problem, and we have normalized aliases for us.
return super()._get_validation_exclusions() + ["username"]
class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email',)
class ProfileForm(forms.ModelForm):
"""
A form for the extras field provided by the :model:`member.Profile` model.
@ -38,6 +50,15 @@ class ProfileForm(forms.ModelForm):
class ClubForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if not self.instance.pk: # Creating a club
if Alias.objects.filter(normalized_name=Alias.normalize(self.cleaned_data["name"])).exists():
self.add_error('name', _("An alias with a similar name already exists."))
return cleaned_data
class Meta:
model = Club
fields = '__all__'
@ -56,8 +77,6 @@ class ClubForm(forms.ModelForm):
class MembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,
@ -96,7 +115,7 @@ class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
fields = ('user', 'roles', 'date_start')
fields = ('user', 'date_start')
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les noms d'utilisateur valides
@ -112,3 +131,28 @@ class MembershipForm(forms.ModelForm):
),
'date_start': DatePickerInput(),
}
class MembershipRolesForm(forms.ModelForm):
user = forms.ModelChoiceField(
queryset=User.objects,
label=_("User"),
disabled=True,
widget=Autocomplete(
User,
attrs={
'api_url': '/api/user/',
'name_field': 'username',
'placeholder': 'Nom ...',
},
),
)
roles = forms.ModelMultipleChoiceField(
queryset=Role.objects.filter(weirole=None).all(),
label=_("Roles"),
)
class Meta:
model = Membership
fields = ('user', 'roles')

View File

@ -3,8 +3,10 @@
import hashlib
from django.conf import settings
from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import constant_time_compare
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
class CustomNK15Hasher(PBKDF2PasswordHasher):
@ -20,8 +22,37 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
"""
algorithm = "custom_nk15"
def must_update(self, encoded):
if settings.DEBUG:
current_user = get_current_authenticated_user()
if current_user is not None and current_user.is_superuser:
return False
return True
def verify(self, password, encoded):
if settings.DEBUG:
current_user = get_current_authenticated_user()
if current_user is not None and current_user.is_superuser\
and get_current_session().get("permission_mask", -1) >= 42:
return True
if '|' in encoded:
salt, db_hashed_pass = encoded.split('$')[2].split('|')
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
return super().verify(password, encoded)
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
"""
In debug mode and during the beta, superusers can login into other accounts for tests.
"""
def must_update(self, encoded):
return False
def verify(self, password, encoded):
if settings.DEBUG:
current_user = get_current_authenticated_user()
if current_user is not None and current_user.is_superuser\
and get_current_session().get("permission_mask", -1) >= 42:
return True
return super().verify(password, encoded)

View File

@ -131,7 +131,7 @@ class Profile(models.Model):
return reverse('user_detail', args=(self.pk,))
def send_email_validation_link(self):
subject = "Activate your Note Kfet account"
subject = _("Activate your Note Kfet account")
message = loader.render_to_string('registration/mails/email_validation_email.html',
{
'user': self.user,
@ -247,24 +247,6 @@ class Club(models.Model):
return reverse_lazy('member:club_detail', args=(self.pk,))
class Role(models.Model):
"""
Role that an :model:`auth.User` can have in a :model:`member.Club`
"""
name = models.CharField(
verbose_name=_('name'),
max_length=255,
unique=True,
)
class Meta:
verbose_name = _('role')
verbose_name_plural = _('roles')
def __str__(self):
return str(self.name)
class Membership(models.Model):
"""
Register the membership of a user to a club, including roles and membership duration.
@ -284,7 +266,7 @@ class Membership(models.Model):
)
roles = models.ManyToManyField(
Role,
"permission.Role",
verbose_name=_("roles"),
)
@ -302,6 +284,7 @@ class Membership(models.Model):
verbose_name=_('fee'),
)
@property
def valid(self):
"""
A membership is valid if today is between the start and the end date.
@ -319,6 +302,14 @@ class Membership(models.Model):
if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists():
raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
if self.pk:
for role in self.roles.all():
club = role.for_club
if club is not None:
if club.pk != self.club_id:
raise ValidationError(_('The role {role} does not apply to the club {club}.')
.format(role=role.name, club=club.name))
created = not self.pk
if created:
if Membership.objects.filter(

View File

@ -131,3 +131,31 @@ class MembershipTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
model = Membership
class ClubManagerTable(tables.Table):
"""
List managers of a club.
"""
def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.username
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
return s
def render_roles(self, record):
roles = record.roles.all()
return ", ".join(str(role) for role in roles)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover',
'style': 'table-layout: fixed;'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user.first_name', 'user.last_name', 'roles', )
model = Membership

View File

@ -16,6 +16,7 @@ urlpatterns = [
path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
path('club/<int:pk>/members/', views.ClubMembersListView.as_view(), name="club_members"),
path('user/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),

View File

@ -6,12 +6,14 @@ from datetime import datetime, timedelta
from PIL import Image
from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
from django.db.models import Q
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
@ -21,12 +23,14 @@ from note.forms import ImageForm
from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable
from note_kfet.middlewares import _set_current_user_and_ip
from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
from .models import Club, Membership, Role
from .tables import ClubTable, UserTable, MembershipTable
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm
from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
class CustomLoginView(LoginView):
@ -36,6 +40,8 @@ class CustomLoginView(LoginView):
form_class = CustomAuthenticationForm
def form_valid(self, form):
logout(self.request)
_set_current_user_and_ip(form.get_user(), self.request.session, None)
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form)
@ -45,9 +51,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
Update the user information.
"""
model = User
fields = ['first_name', 'last_name', 'username', 'email']
form_class = UserForm
template_name = 'member/profile_update.html'
context_object_name = 'user_object'
extra_context = {"title": _("Update Profile")}
profile_form = ProfileForm
def get_context_data(self, **kwargs):
@ -62,7 +70,6 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].help_text = _("This address must be valid.")
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
context['title'] = _("Update Profile")
return context
def form_valid(self, form):
@ -101,6 +108,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
if olduser.email != user.email:
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
user.profile.email_confirmed = False
user.profile.save()
user.profile.send_email_validation_link()
return super().form_valid(form)
@ -117,6 +125,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = User
context_object_name = "user_object"
template_name = "member/profile_detail.html"
extra_context = {"title": _("Profile detail")}
def get_queryset(self, **kwargs):
"""
@ -129,7 +138,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
user = context['user_object']
history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
.order_by("-created_at", "-id")\
.order_by("-created_at")\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
@ -150,12 +159,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = User
table_class = UserTable
template_name = 'member/user_list.html'
extra_context = {"title": _("Search user")}
def get_queryset(self, **kwargs):
"""
Filter the user list with the given pattern.
"""
qs = super().get_queryset().filter(profile__registration_valid=True)
qs = super().get_queryset().distinct().filter(profile__registration_valid=True)
if "search" in self.request.GET:
pattern = self.request.GET["search"]
@ -175,13 +185,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return qs[:20]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Search user")
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
@ -190,6 +193,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = User
template_name = 'member/profile_alias.html'
context_object_name = 'user_object'
extra_context = {"title": _("Note aliases")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -203,6 +207,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
Update profile picture of the user note.
"""
form_class = ImageForm
extra_context = {"title": _("Update note picture")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -260,6 +265,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
"""
model = Token
template_name = "member/manage_auth_tokens.html"
extra_context = {"title": _("Manage auth token")}
def get(self, request, *args, **kwargs):
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
@ -287,6 +293,7 @@ class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Club
form_class = ClubForm
success_url = reverse_lazy('member:club_list')
extra_context = {"title": _("Create new club")}
def form_valid(self, form):
return super().form_valid(form)
@ -298,12 +305,13 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
model = Club
table_class = ClubTable
extra_context = {"title": _("Search club")}
def get_queryset(self, **kwargs):
"""
Filter the user list with the given pattern.
"""
qs = super().get_queryset().filter()
qs = super().get_queryset().distinct()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
@ -322,6 +330,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
model = Club
context_object_name = "club"
extra_context = {"title": _("Club detail")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -330,9 +339,13 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates()
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\
.order_by('user__last_name').all()
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.order_by('-created_at', '-id')
.order_by('-created_at')
history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table
@ -342,7 +355,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
membership_table = MembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
context['member_list'] = membership_table
# Check if the user has the right to create a membership, to display the button.
@ -366,6 +379,7 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Club
template_name = 'member/club_alias.html'
context_object_name = 'club'
extra_context = {"title": _("Note aliases")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -382,6 +396,7 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
context_object_name = "club"
form_class = ClubForm
template_name = "member/club_form.html"
extra_context = {"title": _("Update club")}
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
@ -415,6 +430,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
extra_context = {"title": _("Add new member to the club")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -425,7 +441,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
# If the concerned club is the BDE, then we add the option that Société générale pays the membership.
if club.name != "BDE":
@ -444,7 +459,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
user = old_membership.user
form.fields['user'].initial = user
form.fields['user'].disabled = True
form.fields['roles'].initial = old_membership.roles.all()
form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \
else club.membership_fee_unpaid
@ -560,7 +574,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
form.add_error('bank', _("This field is required."))
return self.form_invalid(form)
SpecialTransaction.objects.create(
transaction = SpecialTransaction(
source=credit_type,
destination=user.note,
quantity=1,
@ -571,9 +585,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
bank=bank,
valid=True,
)
transaction._force_save = True
transaction.save()
ret = super().form_valid(form)
member_role = Role.objects.filter(name="Membre de club").all()
form.instance.roles.set(member_role)
form.instance._force_save = True
form.instance.save()
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
# Kfet membership.
if soge:
@ -595,6 +616,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
date_start=old_membership.get().date_end + timedelta(days=1)
if old_membership.exists() else form.instance.date_start,
)
membership._force_save = True
membership._soge = True
membership.save()
membership.refresh_from_db()
@ -615,8 +637,9 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
Manage the roles of a user in a club
"""
model = Membership
form_class = MembershipForm
form_class = MembershipRolesForm
template_name = 'member/add_members.html'
extra_context = {"title": _("Manage roles of an user in the club")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -626,15 +649,61 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
# We don't create a full membership, we only update one field
form.fields['user'].disabled = True
del form.fields['date_start']
del form.fields['credit_type']
del form.fields['credit_amount']
del form.fields['last_name']
del form.fields['first_name']
del form.fields['bank']
club = self.object.club
form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
return form
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = Membership
table_class = MembershipTable
template_name = "member/club_members.html"
extra_context = {"title": _("Members of the club")}
def get_queryset(self, **kwargs):
qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
if 'search' in self.request.GET:
pattern = self.request.GET['search']
qs = qs.filter(
Q(user__first_name__iregex='^' + pattern)
| Q(user__last_name__iregex='^' + pattern)
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
)
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
if only_active:
qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())
if "roles" in self.request.GET:
if not self.request.GET["roles"]:
return qs.none()
roles_str = self.request.GET["roles"].replace(' ', '').split(',')
roles_int = map(int, roles_str)
qs = qs.filter(roles__in=roles_int)
qs = qs.order_by('-date_start', 'user__username')
return qs.distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = Club.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Club, "view")
).get(pk=self.kwargs["pk"])
context["club"] = club
applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
context["applicable_roles"] = applicable_roles
context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
return context

View File

@ -5,10 +5,12 @@ from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction, SpecialTransaction
from .templatetags.pretty_money import pretty_money
class AliasInlines(admin.TabularInline):
@ -19,7 +21,7 @@ class AliasInlines(admin.TabularInline):
model = Alias
@admin.register(Note)
@admin.register(Note, site=admin_site)
class NoteAdmin(PolymorphicParentModelAdmin):
"""
Parent regrouping all note types as children
@ -36,13 +38,12 @@ class NoteAdmin(PolymorphicParentModelAdmin):
# Organize notes by registration date
date_hierarchy = 'created_at'
ordering = ['-created_at']
# Search by aliases
search_fields = ['alias__name']
@admin.register(NoteClub)
@admin.register(NoteClub, site=admin_site)
class NoteClubAdmin(PolymorphicChildModelAdmin):
"""
Child for a club note, see NoteAdmin
@ -66,15 +67,27 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
return False
@admin.register(NoteSpecial)
@admin.register(NoteSpecial, site=admin_site)
class NoteSpecialAdmin(PolymorphicChildModelAdmin):
"""
Child for a special note, see NoteAdmin
"""
readonly_fields = ('balance',)
def has_add_permission(self, request):
"""
A club note should not be manually added
"""
return False
@admin.register(NoteUser)
def has_delete_permission(self, request, obj=None):
"""
A club note should not be manually removed
"""
return False
@admin.register(NoteUser, site=admin_site)
class NoteUserAdmin(PolymorphicChildModelAdmin):
"""
Child for an user note, see NoteAdmin
@ -97,16 +110,16 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
return False
@admin.register(Transaction)
@admin.register(Transaction, site=admin_site)
class TransactionAdmin(PolymorphicParentModelAdmin):
"""
Admin customisation for Transaction
"""
child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction)
child_models = (Transaction, RecurrentTransaction, MembershipTransaction, SpecialTransaction)
list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'valid')
list_filter = ('valid',)
autocomplete_fields = (
readonly_fields = (
'source',
'destination',
)
@ -138,27 +151,35 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
return []
@admin.register(MembershipTransaction)
@admin.register(MembershipTransaction, site=admin_site)
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for MembershipTransaction
"""
@admin.register(SpecialTransaction)
@admin.register(RecurrentTransaction, site=admin_site)
class RecurrentTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for RecurrentTransaction
"""
@admin.register(SpecialTransaction, site=admin_site)
class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for SpecialTransaction
"""
@admin.register(TransactionTemplate)
@admin.register(TransactionTemplate, site=admin_site)
class TransactionTemplateAdmin(admin.ModelAdmin):
"""
Admin customisation for TransactionTemplate
"""
list_display = ('name', 'poly_destination', 'amount', 'category', 'display',)
list_filter = ('category', 'display')
list_display = ('name', 'poly_destination', 'pretty_amount', 'category', 'display', 'highlighted',)
list_filter = ('category', 'display', 'highlighted',)
search_fields = ('name', 'destination__club__name', 'amount',)
autocomplete_fields = ('destination',)
def poly_destination(self, obj):
@ -169,11 +190,15 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
poly_destination.short_description = _('destination')
def pretty_amount(self, obj):
return pretty_money(obj.amount)
@admin.register(TemplateCategory)
pretty_amount.short_description = _("amount")
@admin.register(TemplateCategory, site=admin_site)
class TemplateCategoryAdmin(admin.ModelAdmin):
"""
Admin customisation for TransactionTemplate
"""
list_display = ('name',)
list_filter = ('name',)

View File

@ -118,9 +118,8 @@ class ConsumerSerializer(serializers.ModelSerializer):
"""
# If the user has no right to see the note, then we only display the note identifier
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
print(obj.pk)
return NotePolymorphicSerializer().to_representation(obj.note)
return dict(id=obj.id)
return dict(id=obj.note.id, name=str(obj.note))
def get_email_confirmed(self, obj):
if isinstance(obj.note, NoteUser):

View File

@ -109,7 +109,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = queryset.filter(
Q(name__regex="^" + alias)
| Q(normalized_name__regex="^" + Alias.normalize(alias))
| Q(normalized_name__regex="^" + alias.lower()))
| Q(normalized_name__regex="^" + alias.lower()))\
.order_by('name').prefetch_related('note')
return queryset

View File

@ -62,6 +62,7 @@ class TransactionTemplate(models.Model):
category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
related_name='templates',
verbose_name=_('type'),
max_length=31,
)
@ -71,6 +72,11 @@ class TransactionTemplate(models.Model):
verbose_name=_("display"),
)
highlighted = models.BooleanField(
default=False,
verbose_name=_("highlighted"),
)
description = models.CharField(
verbose_name=_('description'),
max_length=255,
@ -202,7 +208,9 @@ class Transaction(PolymorphicModel):
super().save(*args, **kwargs)
# Save notes
self.source._force_save = True
self.source.save()
self.destination._force_save = True
self.destination.save()
def delete(self, **kwargs):

View File

@ -8,6 +8,8 @@ from django.db.models import F
from django.utils.html import format_html
from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from .models.notes import Alias
from .models.transactions import Transaction, TransactionTemplate
@ -52,14 +54,26 @@ class HistoryTable(tables.Table):
attrs={
"td": {
"id": lambda record: "validate_" + str(record.id),
"class": lambda record: str(record.valid).lower() + ' validate',
"class": lambda record:
str(record.valid).lower()
+ (' validate' if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason",
record) else ''),
"data-toggle": "tooltip",
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"),
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')',
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) else None,
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')'
if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) else None,
"onmouseover": lambda record: '$("#invalidity_reason_'
+ str(record.id) + '").show();$("#invalidity_reason_'
+ str(record.id) + '").focus();',
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()',
+ str(record.id) + '").focus();'
if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) else None,
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()'
if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) else None,
}
}
)
@ -88,6 +102,10 @@ class HistoryTable(tables.Table):
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
"""
val = "" if value else ""
if not PermissionBackend\
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record):
return val
val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
+ "' value='" + (html.escape(record.invalidity_reason)
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
@ -131,12 +149,10 @@ class ButtonTable(tables.Table):
row_attrs = {
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: record.pk
}
model = TransactionTemplate
exclude = ('id',)
order_by = ('type', '-display', 'destination__name', 'name',)
edit = tables.LinkColumn('note:template_update',
args=[A('pk')],

View File

@ -5,6 +5,7 @@ import json
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, UpdateView
from django_tables2 import SingleTableView
@ -14,7 +15,7 @@ from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
from .models.transactions import SpecialTransaction
from .tables import HistoryTable, ButtonTable
@ -29,16 +30,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
model = Transaction
# Transaction history table
table_class = HistoryTable
extra_context = {"title": _("Transfer money")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
return super().get_queryset(**kwargs).order_by("-created_at").all()[:20]
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
"""
context = super().get_context_data(**kwargs)
context['title'] = _('Transfer money')
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
@ -63,6 +64,7 @@ class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, Cr
model = TransactionTemplate
form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
extra_context = {"title": _("Create new button")}
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
@ -71,6 +73,20 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
"""
model = TransactionTemplate
table_class = ButtonTable
extra_context = {"title": _("Search button")}
def get_queryset(self, **kwargs):
"""
Filter the user list with the given pattern.
"""
qs = super().get_queryset().distinct()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
qs = qs.filter(Q(name__iregex="^" + pattern) | Q(destination__club__name__iregex="^" + pattern))
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
return qs
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
@ -80,6 +96,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
model = TransactionTemplate
form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
extra_context = {"title": _("Update button")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -116,25 +133,28 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
model = Transaction
template_name = "note/conso_form.html"
extra_context = {"title": _("Consumptions")}
# Transaction history table
table_class = HistoryTable
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-created_at", "-id")[:20]
return super().get_queryset(**kwargs).order_by("-created_at")[:20]
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
"""
context = super().get_context_data(**kwargs)
from django.db.models import Count
buttons = TransactionTemplate.objects.filter(
categories = TemplateCategory.objects.order_by('name').all()
for category in categories:
category.templates_filtered = category.templates.filter(
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
).filter(display=True).order_by('name').all()
context['categories'] = [cat for cat in categories if cat.templates_filtered]
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
).filter(display=True).annotate(clicks=Count('recurrenttransaction')).order_by('category__name', 'name')
context['transaction_templates'] = buttons
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
context['title'] = _("Consumptions")
).order_by('name').all()
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
# select2 compatibility

View File

@ -2,11 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-lateré
from django.contrib import admin
from note_kfet.admin import admin_site
from .models import Permission, PermissionMask, RolePermissions
from .models import Permission, PermissionMask, Role
@admin.register(PermissionMask)
@admin.register(PermissionMask, site=admin_site)
class PermissionMaskAdmin(admin.ModelAdmin):
"""
Admin customisation for PermissionMask
@ -14,17 +15,19 @@ class PermissionMaskAdmin(admin.ModelAdmin):
list_display = ('description', 'rank', )
@admin.register(Permission)
@admin.register(Permission, site=admin_site)
class PermissionAdmin(admin.ModelAdmin):
"""
Admin customisation for Permission
"""
list_display = ('type', 'model', 'field', 'mask', 'description', )
list_display = ('description', 'type', 'model', 'field', 'mask', )
list_filter = ('type', 'mask', 'model',)
search_fields = ('description', 'field',)
@admin.register(RolePermissions)
class RolePermissionsAdmin(admin.ModelAdmin):
@admin.register(Role, site=admin_site)
class RoleAdmin(admin.ModelAdmin):
"""
Admin customisation for RolePermissions
Admin customisation for Role
"""
list_display = ('role', )
list_display = ('name', )

View File

@ -3,7 +3,7 @@
from rest_framework import serializers
from ..models import Permission, RolePermissions
from ..models import Permission, Role
class PermissionSerializer(serializers.ModelSerializer):
@ -17,12 +17,12 @@ class PermissionSerializer(serializers.ModelSerializer):
fields = '__all__'
class RolePermissionsSerializer(serializers.ModelSerializer):
class RoleSerializer(serializers.ModelSerializer):
"""
REST API Serializer for RolePermissions types.
The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API.
REST API Serializer for Role types.
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
"""
class Meta:
model = RolePermissions
model = Role
fields = '__all__'

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import PermissionViewSet, RolePermissionsViewSet
from .views import PermissionViewSet, RoleViewSet
def register_permission_urls(router, path):
@ -9,4 +9,4 @@ def register_permission_urls(router, path):
Configure router for permission REST API.
"""
router.register(path + "/permission", PermissionViewSet)
router.register(path + "/roles", RolePermissionsViewSet)
router.register(path + "/roles", RoleViewSet)

View File

@ -4,8 +4,8 @@
from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer, RolePermissionsSerializer
from ..models import Permission, RolePermissions
from .serializers import PermissionSerializer, RoleSerializer
from ..models import Permission, Role
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
@ -20,13 +20,13 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
filterset_fields = ['model', 'type', ]
class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet):
class RoleViewSet(ReadOnlyProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
then render it on /api/permission/roles/
"""
queryset = RolePermissions.objects.all()
serializer_class = RolePermissionsSerializer
queryset = Role.objects.all()
serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['role', ]

View File

@ -1,7 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.contenttypes.models import ContentType
@ -36,27 +35,19 @@ class PermissionBackend(ModelBackend):
# Unauthenticated users have no permissions
return Permission.objects.none()
qs = Permission.objects.annotate(
club=F("rolepermissions__role__membership__club"),
membership=F("rolepermissions__role__membership"),
).filter(
(
Q(
rolepermissions__role__membership__date_start__lte=timezone.now().today(),
rolepermissions__role__membership__date_end__gte=timezone.now().today(),
)
| Q(permanent=True)
)
& Q(rolepermissions__role__membership__user=user)
& Q(type=t)
& Q(mask__rank__lte=get_current_session().get("permission_mask", 0))
)
memberships = Membership.objects.filter(user=user).all()
if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
qs = qs.distinct('pk', 'club')
else: # SQLite doesn't support distinct fields.
qs = qs.distinct()
return qs
perms = []
for membership in memberships:
for role in membership.roles.all():
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
if not perm.permanent:
if membership.date_start > timezone.now().date() or membership.date_end < timezone.now().date():
continue
perm.membership = membership
perms.append(perm)
return perms
@staticmethod
def permissions(user, model, type):
@ -67,22 +58,13 @@ class PermissionBackend(ModelBackend):
:param type: The type of the permissions: view, change, add or delete
:return: A generator of the requested permissions
"""
clubs = {}
memberships = {}
for permission in PermissionBackend.get_raw_permissions(user, type):
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
continue
if permission.club not in clubs:
clubs[permission.club] = club = Club.objects.get(pk=permission.club)
else:
club = clubs[permission.club]
if permission.membership not in memberships:
memberships[permission.membership] = membership = Membership.objects.get(pk=permission.membership)
else:
membership = memberships[permission.membership]
membership = permission.membership
club = membership.club
permission = permission.about(
user=user,
@ -113,12 +95,11 @@ class PermissionBackend(ModelBackend):
:param field: The field of the model to test, if concerned
:return: A query that corresponds to the filter to give to a queryset
"""
if user is None or isinstance(user, AnonymousUser):
# Anonymous users can't do anything
return Q(pk=-1)
if user.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
# Superusers have all rights
return Q()
@ -154,7 +135,7 @@ class PermissionBackend(ModelBackend):
if sess is not None and sess.session_key is None:
return False
if user_obj.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
return True
if obj is None:
@ -163,6 +144,7 @@ class PermissionBackend(ModelBackend):
perm = perm.split('.')[-1].split('_', 2)
perm_type = perm[0]
perm_field = perm[2] if len(perm) == 3 else None
ct = ContentType.objects.get_for_model(obj)
if any(permission.applies(obj, perm_type, perm_field)
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):

View File

@ -4,6 +4,7 @@
from functools import lru_cache
from time import time
from django.conf import settings
from django.contrib.sessions.models import Session
from note_kfet.middlewares import get_current_session
@ -32,6 +33,10 @@ def memoize(f):
sess_funs = new_sess_funs
def func(*args, **kwargs):
if settings.DEBUG:
# Don't memoize in DEBUG mode
return f(*args, **kwargs)
nonlocal last_collect
if time() - last_collect > 60:

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,13 @@
import functools
import json
import operator
from time import sleep
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Q, Model
from django.utils.translation import gettext_lazy as _
from member.models import Role
class InstancedPermission:
@ -45,7 +45,17 @@ class InstancedPermission:
else:
oldpk = obj.pk
# Ensure previous models are deleted
self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete()
count = 0
while count < 1000:
if self.model.model_class().objects.filter(pk=obj.pk).exists():
# If the object exists, that means that one permission is currently checked.
# We wait before the other permission, at most 1 second.
sleep(1)
continue
break
for o in self.model.model_class().objects.filter(pk=obj.pk).all():
o._force_delete = True
Model.delete(o)
# Force insertion, no data verification, no trigger
obj._force_save = True
Model.save(obj, force_insert=True)
@ -114,10 +124,10 @@ class PermissionMask(models.Model):
class Permission(models.Model):
PERMISSION_TYPES = [
('add', 'add'),
('view', 'view'),
('change', 'change'),
('delete', 'delete')
('add', _('add')),
('view', _('view')),
('change', _('change')),
('delete', _('delete'))
]
model = models.ForeignKey(
@ -239,6 +249,9 @@ class Permission(models.Model):
field = Permission.compute_param(value[i], **kwargs)
continue
if not hasattr(field, value[i][0]):
return False
field = getattr(field, value[i][0])
params = []
call_kwargs = {}
@ -252,6 +265,9 @@ class Permission(models.Model):
params.append(param)
field = field(*params, **call_kwargs)
else:
if not hasattr(field, value[i]):
return False
field = getattr(field, value[i])
return field
@ -276,7 +292,7 @@ class Permission(models.Model):
elif query[0] == 'NOT':
return ~Permission._about(query[1], **kwargs)
else:
return Q(pk=F("pk"))
return Q(pk=F("pk")) if Permission.compute_param(query, **kwargs) else ~Q(pk=F("pk"))
elif isinstance(query, dict):
q_kwargs = {}
for key in query:
@ -307,23 +323,30 @@ class Permission(models.Model):
return self.description
class RolePermissions(models.Model):
class Role(models.Model):
"""
Permissions associated with a Role
"""
role = models.OneToOneField(
Role,
on_delete=models.PROTECT,
related_name='permissions',
verbose_name=_('role'),
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
permissions = models.ManyToManyField(
Permission,
verbose_name=_("permissions"),
)
for_club = models.ForeignKey(
"member.Club",
verbose_name=_("for club"),
on_delete=models.PROTECT,
null=True,
default=None,
)
def __str__(self):
return str(self.role)
return self.name
class Meta:
verbose_name = _("role permissions")

View File

@ -19,8 +19,8 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'PUT': [], # ['%(app_label)s.change_%(model_name)s'],
'PATCH': [], # ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}

View File

@ -50,6 +50,7 @@ def pre_save_object(sender, instance, **kwargs):
# In the other case, we check if he/she has the right to change one field
previous = qs.get()
for field in instance._meta.fields:
field_name = field.name
old_value = getattr(previous, field.name)
@ -81,7 +82,8 @@ def pre_delete_object(instance, **kwargs):
if instance._meta.label_lower in EXCLUDED:
return
if hasattr(instance, "_force_delete"):
if hasattr(instance, "_force_delete") or hasattr(instance, "pk") and instance.pk == 0:
# Don't check permissions on force-deleted objects
return
user = get_current_authenticated_user()

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter
from django import template
@ -16,9 +17,9 @@ def not_empty_model_list(model_name):
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None:
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
qs = model_list(model_name)
return qs.exists()
@ -31,28 +32,38 @@ def not_empty_model_change_list(model_name):
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None:
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
qs = model_list(model_name, "change")
return qs.exists()
@stringfilter
def model_list(model_name, t="view"):
def model_list(model_name, t="view", fetch=True):
"""
Return the queryset of all visible instances of the given model.
"""
user = get_current_authenticated_user()
if user is None:
return False
spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all()
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t))
if user is None or isinstance(user, AnonymousUser):
return qs.none()
if fetch:
qs = qs.all()
return qs
@stringfilter
def model_list_length(model_name, t="view"):
"""
Return the length of queryset of all visible instances of the given model.
"""
return model_list(model_name, t, False).count()
def has_perm(perm, obj):
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
@ -63,9 +74,9 @@ def can_create_transaction():
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None:
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
if session.get("can_create_transaction", None):
return session.get("can_create_transaction", None) == 1
@ -85,4 +96,5 @@ register = template.Library()
register.filter('not_empty_model_list', not_empty_model_list)
register.filter('not_empty_model_change_list', not_empty_model_change_list)
register.filter('model_list', model_list)
register.filter('model_list_length', model_list_length)
register.filter('has_perm', has_perm)

View File

@ -76,7 +76,7 @@ class PermissionQueryTestCase(TestCase):
model = perm.model.model_class()
model.objects.filter(query).all()
# print("Good query for permission", perm)
except (FieldError, AttributeError, ValueError):
except (FieldError, AttributeError, ValueError, TypeError):
print("Query error for permission", perm)
print("Query:", perm.query)
if instanced.query:

View File

@ -5,9 +5,10 @@ from datetime import date
from django.forms import HiddenInput
from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView, TemplateView
from member.models import Role, Membership
from member.models import Membership
from .backends import PermissionBackend
from .models import Role
class ProtectQuerysetMixin:
@ -19,7 +20,7 @@ class ProtectQuerysetMixin:
"""
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct()
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
def get_form(self, form_class=None):
form = super().get_form(form_class)
@ -40,6 +41,7 @@ class ProtectQuerysetMixin:
class RightsView(TemplateView):
template_name = "permission/all_rights.html"
extra_context = {"title": _("Rights")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

View File

@ -24,7 +24,8 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
# Truncate microseconds so that tokens are consistent even if the
# database doesn't support microseconds.
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
return str(user.pk) + str(user.profile.email_confirmed) + str(login_timestamp) + str(timestamp)
return str(user.pk) + str(user.email) + str(user.profile.email_confirmed)\
+ str(login_timestamp) + str(timestamp)
email_validation_token = AccountActivationTokenGenerator()

View File

@ -15,10 +15,11 @@ from django.views.generic import CreateView, TemplateView, DetailView
from django.views.generic.edit import FormMixin
from django_tables2 import SingleTableView
from member.forms import ProfileForm
from member.models import Membership, Club, Role
from member.models import Membership, Club
from note.models import SpecialTransaction
from note.templatetags.pretty_money import pretty_money
from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin
from .forms import SignUpForm, ValidationForm
@ -34,6 +35,7 @@ class UserCreateView(CreateView):
form_class = SignUpForm
template_name = 'registration/signup.html'
second_form = ProfileForm
extra_context = {"title": _("Register new user")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -77,6 +79,7 @@ class UserValidateView(TemplateView):
"""
title = _("Email validation")
template_name = 'registration/email_validation_complete.html'
extra_context = {"title": _("Validate email")}
def get(self, *args, **kwargs):
"""
@ -90,16 +93,13 @@ class UserValidateView(TemplateView):
# Validate the token
if user is not None and email_validation_token.check_token(user, token):
self.validlink = True
# The user must wait that someone validates the account before the user can be active and login.
self.validlink = True
user.is_active = user.profile.registration_valid or user.is_superuser
user.profile.email_confirmed = True
user.save()
user.profile.save()
return super().dispatch(*args, **kwargs)
else:
# Display the "Email validation unsuccessful" page.
return self.render_to_response(self.get_context_data())
return self.render_to_response(self.get_context_data())
def get_user(self, uidb64):
"""
@ -132,7 +132,7 @@ class UserValidationEmailSentView(TemplateView):
Display the information that the validation link has been sent.
"""
template_name = 'registration/email_validation_email_sent.html'
title = _('Email validation email sent')
extra_context = {"title": _('Email validation email sent')}
class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
@ -140,6 +140,7 @@ class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, De
Rensend the email validation link.
"""
model = User
extra_context = {"title": _("Resend email validation link")}
def get(self, request, *args, **kwargs):
user = self.get_object()
@ -157,6 +158,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
model = User
table_class = FutureUserTable
template_name = 'registration/future_user_list.html'
extra_context = {"title": _("Pre-registered users list")}
def get_queryset(self, **kwargs):
"""
@ -164,7 +166,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
:param kwargs:
:return:
"""
qs = super().get_queryset().filter(profile__registration_valid=False)
qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
if "search" in self.request.GET:
pattern = self.request.GET["search"]
@ -198,6 +200,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
form_class = ValidationForm
context_object_name = "user_object"
template_name = "registration/future_profile_detail.html"
extra_context = {"title": _("Registration detail")}
def post(self, request, *args, **kwargs):
form = self.get_form()
@ -354,6 +357,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
"""
Delete a pre-registered user.
"""
extra_context = {"title": _("Invalidate pre-registration")}
def get(self, request, *args, **kwargs):
"""

@ -1 +1 @@
Subproject commit ee54fca89ee247a4ba4af080dd3036d92340eade
Subproject commit dd8b48c31d4501d95b3afb00ac9b387397fca957

View File

@ -2,11 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-lateré
from django.contrib import admin
from note_kfet.admin import admin_site
from .models import RemittanceType, Remittance, SogeCredit
@admin.register(RemittanceType)
@admin.register(RemittanceType, site=admin_site)
class RemittanceTypeAdmin(admin.ModelAdmin):
"""
Admin customisation for RemiitanceType
@ -14,7 +15,7 @@ class RemittanceTypeAdmin(admin.ModelAdmin):
list_display = ('note', )
@admin.register(Remittance)
@admin.register(Remittance, site=admin_site)
class RemittanceAdmin(admin.ModelAdmin):
"""
Admin customisation for Remittance
@ -27,4 +28,14 @@ class RemittanceAdmin(admin.ModelAdmin):
return not obj.closed and super().has_change_permission(request, obj)
admin.site.register(SogeCredit)
@admin.register(SogeCredit, site=admin_site)
class SogeCreditAdmin(admin.ModelAdmin):
"""
Admin customisation for Remittance
"""
list_display = ('user', 'valid',)
readonly_fields = ('transactions', 'credit_transaction',)
def has_add_permission(self, request):
# Don't create a credit manually
return False

View File

@ -8,7 +8,6 @@ from crispy_forms.layout import Submit
from django import forms
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import DatePickerInput, AmountInput
from permission.backends import PermissionBackend
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
@ -132,8 +131,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
# Add submit button
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
def clean_last_name(self):
"""

View File

@ -15,6 +15,7 @@ from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, UpdateView, DetailView
from django.views.generic.base import View, TemplateView
from django.views.generic.edit import BaseFormView
@ -35,6 +36,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
model = Invoice
form_class = InvoiceForm
extra_context = {"title": _("Create new invoice")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -77,6 +79,7 @@ class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView)
"""
model = Invoice
table_class = InvoiceTable
extra_context = {"title": _("Invoices list")}
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
@ -85,6 +88,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
model = Invoice
form_class = InvoiceForm
extra_context = {"title": _("Update an invoice")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -167,7 +171,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
del tex
# The file has to be rendered twice
for _ in range(2):
for ignored in range(2):
error = subprocess.Popen(
["pdflatex", "invoice-{}.tex".format(pk)],
cwd=tmp_dir,
@ -198,6 +202,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView)
"""
model = Remittance
form_class = RemittanceForm
extra_context = {"title": _("Create a new remittance")}
def get_success_url(self):
return reverse_lazy('treasury:remittance_list')
@ -218,27 +223,46 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
List existing Remittances
"""
template_name = "treasury/remittance_list.html"
extra_context = {"title": _("Remittances list")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["opened_remittances"] = RemittanceTable(
opened_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
context["closed_remittances"] = RemittanceTable(
data=Remittance.objects.filter(closed=True).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all())
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
prefix="opened-remittances-",
)
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
context["opened_remittances"] = opened_remittances
context["special_transactions_no_remittance"] = SpecialTransactionTable(
closed_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=True).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all(),
prefix="closed-remittances-",
)
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
context["closed_remittances"] = closed_remittances
no_remittance_tr = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance=None).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
exclude=('remittance_remove', ))
context["special_transactions_with_remittance"] = SpecialTransactionTable(
exclude=('remittance_remove', ),
prefix="no-remittance-",
)
no_remittance_tr.paginate(page=self.request.GET.get("no-remittance-page", 1), per_page=10)
context["special_transactions_no_remittance"] = no_remittance_tr
with_remittance_tr = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance__closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
exclude=('remittance_add', ))
exclude=('remittance_add', ),
prefix="with-remittance-",
)
with_remittance_tr.paginate(page=self.request.GET.get("with-remittance-page", 1), per_page=10)
context["special_transactions_with_remittance"] = with_remittance_tr
return context
@ -249,6 +273,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
"""
model = Remittance
form_class = RemittanceForm
extra_context = {"title": _("Update a remittance")}
def get_success_url(self):
return reverse_lazy('treasury:remittance_list')
@ -271,9 +296,9 @@ class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin,
"""
Attach a special transaction to a remittance
"""
model = SpecialTransactionProxy
form_class = LinkTransactionToRemittanceForm
extra_context = {"title": _("Attach a transaction to a remittance")}
def get_success_url(self):
return reverse_lazy('treasury:remittance_list')
@ -317,6 +342,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
"""
model = SogeCredit
table_class = SogeCreditTable
extra_context = {"title": _("List of credits from the Société générale")}
def get_queryset(self, **kwargs):
"""
@ -355,6 +381,7 @@ class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormVie
"""
model = SogeCredit
form_class = Form
extra_context = {"title": _("Manage credits from the Société générale")}
def form_valid(self, form):
if "validate" in form.data:

View File

@ -1,13 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from note_kfet.admin import admin_site
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
admin.site.register(WEIClub)
admin.site.register(WEIRegistration)
admin.site.register(WEIMembership)
admin.site.register(WEIRole)
admin.site.register(Bus)
admin.site.register(BusTeam)
admin_site.register(WEIClub)
admin_site.register(WEIRegistration)
admin_site.register(WEIMembership)
admin_site.register(WEIRole)
admin_site.register(Bus)
admin_site.register(BusTeam)

View File

@ -96,7 +96,7 @@ class WEIMembershipForm(forms.ModelForm):
class BusForm(forms.ModelForm):
class Meta:
model = Bus
fields = '__all__'
exclude = ('information_json',)
widgets = {
"wei": Autocomplete(
WEIClub,

View File

@ -8,8 +8,9 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import gettext_lazy as _
from member.models import Role, Club, Membership
from member.models import Club, Membership
from note.models import MembershipTransaction
from permission.models import Role
class WEIClub(Club):
@ -113,6 +114,7 @@ class BusTeam(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
@ -188,6 +190,28 @@ class WEIRegistration(models.Model):
verbose_name=_("gender"),
)
clothing_cut = models.CharField(
max_length=16,
choices=(
('male', _("Male")),
('female', _("Female")),
),
verbose_name=_("clothing cut"),
)
clothing_size = models.CharField(
max_length=4,
choices=(
('XS', "XS"),
('S', "S"),
('M', "M"),
('L', "L"),
('XL', "XL"),
('XXL', "XXL"),
),
verbose_name=_("clothing size"),
)
health_issues = models.TextField(
blank=True,
default="",

View File

@ -103,7 +103,7 @@ class WEIMembershipTable(tables.Table):
team = tables.LinkColumn(
'wei:manage_bus_team',
args=[A('bus.pk')],
args=[A('team.pk')],
)
def render_year(self, record):
@ -144,10 +144,10 @@ class BusTable(tables.Table):
)
def render_teams(self, value):
return ", ".join(team.name for team in value.all())
return ", ".join(team.name for team in value.order_by('name').all())
def render_count(self, value):
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
return str(value) + " " + (str(_("members")) if value > 1 else str(_("member")))
class Meta:
attrs = {
@ -178,7 +178,7 @@ class BusTeamTable(tables.Table):
)
def render_count(self, value):
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
return str(value) + " " + (str(_("members")) if value > 1 else str(_("member")))
count = tables.Column(
verbose_name=_("Members count"),

View File

@ -17,6 +17,7 @@ from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from django.views import View
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
from django.utils.translation import gettext_lazy as _
@ -52,6 +53,16 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = WEIClub
table_class = WEITable
ordering = '-year'
extra_context = {"title": _("Search WEI")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub(
year=0,
date_start=timezone.now().date(),
date_end=timezone.now().date(),
))
return context
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
@ -60,6 +71,7 @@ class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
model = WEIClub
form_class = WEIForm
extra_context = {"title": _("Create WEI")}
def form_valid(self, form):
form.instance.requires_membership = True
@ -79,6 +91,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
model = WEIClub
context_object_name = "club"
extra_context = {"title": _("WEI Detail")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -132,6 +145,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
else:
# Check if the user has the right to create a registration of a random first year member.
empty_fy_registration = WEIRegistration(
wei=club,
user=random_user,
first_year=True,
birth_date="1970-01-01",
@ -144,6 +158,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# Check if the user has the right to create a registration of a random old member.
empty_old_registration = WEIRegistration(
wei=club,
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
first_year=False,
birth_date="1970-01-01",
@ -171,13 +186,14 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
"""
model = WEIMembership
table_class = WEIMembershipTable
extra_context = {"title": _("View members of the WEI")}
def dispatch(self, request, *args, **kwargs):
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(club=self.club)
qs = super().get_queryset(**kwargs).filter(club=self.club).distinct()
pattern = self.request.GET.get("search", "")
@ -208,13 +224,14 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
"""
model = WEIRegistration
table_class = WEIRegistrationTable
extra_context = {"title": _("View registrations to the WEI")}
def dispatch(self, request, *args, **kwargs):
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None)
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct()
pattern = self.request.GET.get("search", "")
@ -244,6 +261,7 @@ class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = WEIClub
context_object_name = "club"
form_class = WEIForm
extra_context = {"title": _("Update the WEI")}
def dispatch(self, request, *args, **kwargs):
wei = self.get_object()
@ -264,6 +282,7 @@ class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
model = Bus
form_class = BusForm
extra_context = {"title": _("Create new bus")}
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
@ -294,6 +313,7 @@ class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
model = Bus
form_class = BusForm
extra_context = {"title": _("Update bus")}
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
@ -323,6 +343,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
Manage Bus
"""
model = Bus
extra_context = {"title": _("Manage bus")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -330,7 +351,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
bus = self.object
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
.filter(bus=bus).annotate(count=Count("memberships"))
.filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
teams_table = BusTeamTable(data=teams, prefix="team-")
context["teams"] = teams_table
@ -349,6 +370,7 @@ class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
model = BusTeam
form_class = BusTeamForm
extra_context = {"title": _("Create new team")}
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
@ -380,6 +402,7 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
model = BusTeam
form_class = BusTeamForm
extra_context = {"title": _("Update team")}
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().bus.wei
@ -410,6 +433,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
Manage Bus team
"""
model = BusTeam
extra_context = {"title": _("Manage WEI team")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -431,6 +455,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
model = WEIRegistration
form_class = WEIRegistrationForm
extra_context = {"title": _("Register first year student to the WEI")}
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
@ -485,6 +510,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
model = WEIRegistration
form_class = WEIRegistrationForm
extra_context = {"title": _("Register old student to the WEI")}
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
@ -562,6 +588,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
"""
model = WEIRegistration
form_class = WEIRegistrationForm
extra_context = {"title": _("Update WEI Registration")}
def get_queryset(self, **kwargs):
return WEIRegistration.objects
@ -651,6 +678,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
Delete a non-validated WEI registration
"""
model = WEIRegistration
extra_context = {"title": _("Delete WEI registration")}
def dispatch(self, request, *args, **kwargs):
object = self.get_object()
@ -680,6 +708,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea
"""
model = WEIMembership
form_class = WEIMembershipForm
extra_context = {"title": _("Validate WEI registration")}
def dispatch(self, request, *args, **kwargs):
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
@ -797,6 +826,7 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
model = WEIRegistration
template_name = "wei/survey.html"
survey = None
extra_context = {"title": _("Survey WEI")}
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
@ -834,7 +864,6 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
context["title"] = _("Survey WEI")
return context
def form_valid(self, form):
@ -850,21 +879,21 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
template_name = "wei/survey_end.html"
extra_context = {"title": _("Survey WEI")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
context["title"] = _("Survey WEI")
return context
class WEIClosedView(LoginRequiredMixin, TemplateView):
template_name = "wei/survey_closed.html"
extra_context = {"title": _("Survey WEI")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
context["title"] = _("Survey WEI")
return context

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

25
note_kfet/admin.py Normal file
View File

@ -0,0 +1,25 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.admin import AdminSite
from django.contrib.sites.admin import Site, SiteAdmin
from member.views import CustomLoginView
from .middlewares import get_current_session
class StrongAdminSite(AdminSite):
def has_permission(self, request):
"""
Authorize only staff that have the correct permission mask
"""
session = get_current_session()
return request.user.is_active and request.user.is_staff and session.get("permission_mask", -1) >= 42
def login(self, request, extra_context=None):
return CustomLoginView.as_view()(request)
# Instantiate admin site and register some defaults
admin_site = StrongAdminSite()
admin_site.register(Site, SiteAdmin)

View File

@ -57,6 +57,8 @@ if "cas_server" in INSTALLED_APPS:
if "logs" in INSTALLED_APPS:
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
if "debug_toolbar" in INSTALLED_APPS:
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ['127.0.0.1']
if DEBUG:
PASSWORD_HASHERS += ['member.hashers.DebugSuperuserBackdoor']
if "debug_toolbar" in INSTALLED_APPS:
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ['127.0.0.1']

View File

@ -61,6 +61,7 @@ INSTALLED_APPS = [
'note',
'permission',
'registration',
'scripts',
'treasury',
'wei',
]

View File

@ -25,7 +25,7 @@ DATABASES = {
}
# Break it, fix it!
DEBUG = True
DEBUG = False
# Mandatory !
ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
@ -36,11 +36,12 @@ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = False
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org')
EMAIL_PORT = os.getenv('EMAIL_PORT', 443)
EMAIL_HOST_USER = os.getenv('EMAIL_USER', 'change_me')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', 'change_me')
EMAIL_PORT = os.getenv('EMAIL_PORT', 465)
EMAIL_HOST_USER = os.getenv('EMAIL_USER', None)
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', None)
SERVER_EMAIL = os.getenv("NOTE_MAIL", "notekfet@example.com")
DEFAULT_FROM_EMAIL = "NoteKfet2020 <" + SERVER_EMAIL + ">"
# Security settings
SECURE_CONTENT_TYPE_NOSNIFF = False

View File

@ -3,13 +3,14 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
from django.views.generic import RedirectView
from member.views import CustomLoginView
from .admin import admin_site
urlpatterns = [
# Dev so redirect to something random
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
@ -25,7 +26,7 @@ urlpatterns = [
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),
path('admin/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.urls, name="admin"),
path('admin/', admin_site.urls, name="admin"),
path('accounts/login/', CustomLoginView.as_view()),
path('accounts/', include('django.contrib.auth.urls')),
path('api/', include('api.urls')),

View File

@ -1 +1 @@
django-cas-server==1.1.0
django-cas-server==1.2.0

View File

@ -1,10 +1,4 @@
Copyright jQuery Foundation and other contributors, https://jquery.org/
This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available at https://github.com/jquery/jquery
====
Copyright JS Foundation and other contributors, https://js.foundation/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
#djDebug {
display: none !important;
}

View File

@ -0,0 +1,646 @@
/* http://www.positioniseverything.net/easyclearing.html */
#djDebug .djdt-clearfix:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
#djDebug .djdt-clearfix {display: inline-block;}
/* Hides from IE-mac \*/
#djDebug .djdt-clearfix {display: block;}
* html #djDebug .djdt-clearfix {height: 1%;}
/* end hide from IE-mac */
/* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */
#djDebug {color:#000;background:#FFF;}
#djDebug, #djDebug div, #djDebug span, #djDebug applet, #djDebug object, #djDebug iframe,
#djDebug h1, #djDebug h2, #djDebug h3, #djDebug h4, #djDebug h5, #djDebug h6, #djDebug p, #djDebug blockquote, #djDebug pre,
#djDebug a, #djDebug abbr, #djDebug acronym, #djDebug address, #djDebug big, #djDebug cite, #djDebug code,
#djDebug del, #djDebug dfn, #djDebug em, #djDebug font, #djDebug img, #djDebug ins, #djDebug kbd, #djDebug q, #djDebug s, #djDebug samp,
#djDebug small, #djDebug strike, #djDebug strong, #djDebug sub, #djDebug sup, #djDebug tt, #djDebug var,
#djDebug b, #djDebug u, #djDebug i, #djDebug center,
#djDebug dl, #djDebug dt, #djDebug dd, #djDebug ol, #djDebug ul, #djDebug li,
#djDebug fieldset, #djDebug form, #djDebug label, #djDebug legend,
#djDebug table, #djDebug caption, #djDebug tbody, #djDebug tfoot, #djDebug thead, #djDebug tr, #djDebug th, #djDebug td,
#djDebug button {
margin:0;
padding:0;
min-width:0;
width:auto;
border:0;
outline:0;
font-size:12px;
line-height:1.5em;
color:#000;
vertical-align:baseline;
background-color:transparent;
font-family:sans-serif;
text-align:left;
text-shadow: none;
white-space: normal;
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
#djDebug button, #djDebug a.button {
background-color: #eee;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #eee), color-stop(100%, #cccccc));
background-image: -webkit-linear-gradient(top, #eee, #cccccc);
background-image: -moz-linear-gradient(top, #eee, #cccccc);
background-image: -ms-linear-gradient(top, #eee, #cccccc);
background-image: -o-linear-gradient(top, #eee, #cccccc);
background-image: linear-gradient(top, #eee, #cccccc);
border: 1px solid #ccc;
border-bottom: 1px solid #bbb;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
color: #333;
line-height: 1;
padding: 0 8px;
text-align: center;
text-shadow: 0 1px 0 #eee;
}
#djDebug button:hover, #djDebug a.button:hover {
background-color: #ddd;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ddd), color-stop(100%, #bbb));
background-image: -webkit-linear-gradient(top, #ddd, #bbb);
background-image: -moz-linear-gradient(top, #ddd, #bbb);
background-image: -ms-linear-gradient(top, #ddd, #bbb);
background-image: -o-linear-gradient(top, #ddd, #bbb);
background-image: linear-gradient(top, #ddd, #bbb);
border-color: #bbb;
border-bottom-color: #999;
cursor: pointer;
text-shadow: 0 1px 0 #ddd;
}
#djDebug button:active, #djDebug a.button:active {
border: 1px solid #aaa;
border-bottom: 1px solid #888;
-webkit-box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
-moz-box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
}
#djDebug #djDebugToolbar {
background-color:#111;
width:200px;
z-index:100000000;
position:fixed;
top:0;
bottom:0;
right:0;
opacity:0.9;
overflow-y: auto;
}
#djDebug #djDebugToolbar small {
color:#999;
}
#djDebug #djDebugToolbar ul {
margin:0;
padding:0;
list-style:none;
}
#djDebug #djDebugToolbar li {
border-bottom:1px solid #222;
color:#fff;
display:block;
font-weight:bold;
float:none;
margin:0;
padding:0;
position:relative;
width:auto;
}
#djDebug #djDebugToolbar input[type=checkbox] {
float: right;
margin: 10px;
}
#djDebug #djDebugToolbar li>a,
#djDebug #djDebugToolbar li>div.djdt-contentless {
font-weight:normal;
font-style:normal;
text-decoration:none;
display:block;
font-size:16px;
padding:10px 10px 5px 25px;
color:#fff;
}
#djDebug #djDebugToolbar li>div.djdt-disabled {
font-style: italic;
color: #999;
}
#djDebug #djDebugToolbar li a:hover {
color:#111;
background-color:#ffc;
}
#djDebug #djDebugToolbar li.djdt-active {
background: #333 no-repeat left center;
background-image: url(../img/indicator.png);
padding-left:10px;
}
#djDebug #djDebugToolbar li.djdt-active a:hover {
color:#b36a60;
background-color:transparent;
}
#djDebug #djDebugToolbar li small {
font-size:12px;
color:#999;
font-style:normal;
text-decoration:none;
font-variant:small-caps;
}
#djDebug #djDebugToolbarHandle {
position:fixed;
background-color:#fff;
border:1px solid #111;
top:30px;
right:0;
z-index:100000000;
opacity:0.75;
}
#djDebug #djShowToolBarButton {
display:block;
height:75px;
width:30px;
border-right:none;
border-bottom:4px solid #fff;
border-top:4px solid #fff;
border-left:4px solid #fff;
color:#fff;
font-size:10px;
font-weight:bold;
text-decoration:none;
text-align:center;
text-indent:-999999px;
background: #000 no-repeat left center;
background-image: url(../img/djdt_vertical.png);
opacity:0.5;
}
#djDebug #djShowToolBarButton:hover {
background-color:#111;
border-top-color:#FFE761;
border-left-color:#FFE761;
border-bottom-color:#FFE761;
cursor:move;
opacity:1.0;
}
#djDebug code {
display:block;
font-family:Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace;
font-size: 12px;
white-space:pre;
overflow:auto;
}
#djDebug .djDebugOdd {
background-color:#f5f5f5;
}
#djDebug .djdt-panelContent {
display:none;
position:fixed;
margin:0;
top:0;
right:200px;
bottom:0;
left:0px;
background-color:#eee;
color:#666;
z-index:100000000;
}
#djDebug .djdt-panelContent > div {
border-bottom:1px solid #ddd;
}
#djDebug .djDebugPanelTitle {
position:absolute;
background-color:#ffc;
color:#666;
padding-left:20px;
top:0;
right:0;
left:0;
height:50px;
}
#djDebug .djDebugPanelTitle code {
display:inline;
font-size:inherit;
}
#djDebug .djDebugPanelContent {
position:absolute;
top:50px;
right:0;
bottom:0;
left:0;
height:auto;
padding:5px 0 0 20px;
}
#djDebug .djDebugPanelContent .djdt-loader {
display:block;
margin:80px auto;
}
#djDebug .djDebugPanelContent .djdt-scroll {
height:100%;
overflow:auto;
display:block;
padding:0 10px 0 0;
}
#djDebug h3 {
font-size:24px;
font-weight:normal;
line-height:50px;
}
#djDebug h4 {
font-size:20px;
font-weight:bold;
margin-top:0.8em;
}
#djDebug .djdt-panelContent table {
border:1px solid #ccc;
border-collapse:collapse;
width:100%;
background-color:#fff;
display:table;
margin-top:0.8em;
overflow: auto;
}
#djDebug .djdt-panelContent tbody td,
#djDebug .djdt-panelContent tbody th {
vertical-align:top;
padding:2px 3px;
}
#djDebug .djdt-panelContent tbody td.djdt-time {
text-align: center;
}
#djDebug .djdt-panelContent thead th {
padding:1px 6px 1px 3px;
text-align:left;
font-weight:bold;
font-size:14px;
white-space: nowrap;
}
#djDebug .djdt-panelContent tbody th {
width:12em;
text-align:right;
color:#666;
padding-right:.5em;
}
#djDebug .djTemplateContext {
background-color:#fff;
}
/*
#djDebug .djdt-panelContent p a:hover, #djDebug .djdt-panelContent dd a:hover {
color:#111;
background-color:#ffc;
}
#djDebug .djdt-panelContent p {
padding:0 5px;
}
#djDebug .djdt-panelContent p, #djDebug .djdt-panelContent table, #djDebug .djdt-panelContent ol, #djDebug .djdt-panelContent ul, #djDebug .djdt-panelContent dl {
margin:5px 0 15px;
background-color:#fff;
}
#djDebug .djdt-panelContent table {
clear:both;
border:0;
padding:0;
margin:0;
border-collapse:collapse;
border-spacing:0;
}
#djDebug .djdt-panelContent table a {
color:#000;
padding:2px 4px;
}
#djDebug .djdt-panelContent table a:hover {
background-color:#ffc;
}
#djDebug .djdt-panelContent table th {
background-color:#333;
font-weight:bold;
color:#fff;
padding:3px 7px 3px;
text-align:left;
cursor:pointer;
}
#djDebug .djdt-panelContent table td {
padding:5px 10px;
font-size:14px;
background-color:#fff;
color:#000;
vertical-align:top;
border:0;
}
#djDebug .djdt-panelContent table tr.djDebugOdd td {
background-color:#eee;
}
*/
#djDebug .djdt-panelContent .djDebugClose {
display:block;
position:absolute;
top:4px;
right:15px;
height:40px;
width:40px;
background: no-repeat center center;
background-image: url(../img/close.png);
}
#djDebug .djdt-panelContent .djDebugClose:hover {
background-image: url(../img/close_hover.png);
}
#djDebug .djdt-panelContent .djDebugClose.djDebugBack {
background-image: url(../img/back.png);
}
#djDebug .djdt-panelContent .djDebugClose.djDebugBack:hover {
background-image: url(../img/back_hover.png);
}
#djDebug .djdt-panelContent dt, #djDebug .djdt-panelContent dd {
display:block;
}
#djDebug .djdt-panelContent dt {
margin-top:0.75em;
}
#djDebug .djdt-panelContent dd {
margin-left:10px;
}
#djDebug a.toggleTemplate {
padding:4px;
background-color:#bbb;
-webkit-border-radius:3px;
-moz-border-radius:3px;
border-radius:3px;
}
#djDebug a.toggleTemplate:hover {
padding:4px;
background-color:#444;
color:#ffe761;
-webkit-border-radius:3px;
-moz-border-radius:3px;
border-radius:3px;
}
#djDebug .djDebugSqlWrap {
position:relative;
}
#djDebug .djDebugCollapsed {
display: none;
text-decoration: none;
color: #333;
}
#djDebug .djDebugUncollapsed {
color: #333;
text-decoration: none;
}
#djDebug .djUnselected {
display: none;
}
#djDebug tr.djHiddenByDefault {
display: none;
}
#djDebug tr.djSelected {
display: table-row;
}
#djDebug .djDebugSql {
word-break:break-word;
z-index:100000002;
}
#djDebug .djSQLDetailsDiv tbody th {
text-align: left;
}
#djDebug .djSqlExplain td {
white-space: pre;
}
#djDebug span.djDebugLineChart {
background-color:#777;
height:3px;
position:absolute;
bottom:0;
top:0;
left:0;
display:block;
z-index:1000000001;
}
#djDebug span.djDebugLineChartWarning {
background-color:#900;
}
#djDebug .highlight { color:#000; }
#djDebug .highlight .err { color:#000; } /* Error */
#djDebug .highlight .g { color:#000; } /* Generic */
#djDebug .highlight .k { color:#000; font-weight:bold } /* Keyword */
#djDebug .highlight .o { color:#000; } /* Operator */
#djDebug .highlight .n { color:#000; } /* Name */
#djDebug .highlight .mi { color:#000; font-weight:bold } /* Literal.Number.Integer */
#djDebug .highlight .l { color:#000; } /* Literal */
#djDebug .highlight .x { color:#000; } /* Other */
#djDebug .highlight .p { color:#000; } /* Punctuation */
#djDebug .highlight .m { color:#000; font-weight:bold } /* Literal.Number */
#djDebug .highlight .s { color:#333 } /* Literal.String */
#djDebug .highlight .w { color:#888888 } /* Text.Whitespace */
#djDebug .highlight .il { color:#000; font-weight:bold } /* Literal.Number.Integer.Long */
#djDebug .highlight .na { color:#333 } /* Name.Attribute */
#djDebug .highlight .nt { color:#000; font-weight:bold } /* Name.Tag */
#djDebug .highlight .nv { color:#333 } /* Name.Variable */
#djDebug .highlight .s2 { color:#333 } /* Literal.String.Double */
#djDebug .highlight .cp { color:#333 } /* Comment.Preproc */
#djDebug .djdt-timeline {
width: 30%;
}
#djDebug .djDebugTimeline {
position: relative;
height: 100%;
min-height: 100%;
}
#djDebug div.djDebugLineChart {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
vertical-align: middle;
}
#djDebug div.djDebugLineChart strong {
text-indent: -10000em;
display: block;
font-weight: normal;
vertical-align: middle;
background-color:#ccc;
}
#djDebug div.djDebugLineChartWarning strong {
background-color:#900;
}
#djDebug .djDebugInTransaction div.djDebugLineChart strong {
background-color: #d3ff82;
}
#djDebug .djDebugStartTransaction div.djDebugLineChart strong {
border-left: 1px solid #94b24d;
}
#djDebug .djDebugEndTransaction div.djDebugLineChart strong {
border-right: 1px solid #94b24d;
}
#djDebug .djdt-panelContent ul.djdt-stats {
position: relative;
list-style-type: none;
}
#djDebug .djdt-panelContent ul.djdt-stats li {
width: 30%;
float: left;
}
#djDebug .djdt-panelContent ul.djdt-stats li strong.djdt-label {
display: block;
}
#djDebug .djdt-panelContent ul.djdt-stats li span.djdt-color {
height: 12px;
width: 3px;
display: inline-block;
}
#djDebug .djdt-panelContent ul.djdt-stats li span.djdt-info {
display: block;
padding-left: 5px;
}
#djDebug .djdt-panelContent thead th {
white-space: nowrap;
}
#djDebug .djDebugRowWarning .djdt-time {
color: red;
}
#djdebug .djdt-panelContent table .djdt-toggle {
width: 14px;
padding-top: 3px;
}
#djDebug .djdt-panelContent table .djdt-actions {
min-width: 70px;
white-space: nowrap;
}
#djdebug .djdt-panelContent table .djdt-color {
width: 3px;
}
#djdebug .djdt-panelContent table .djdt-color span {
width: 3px;
height: 12px;
overflow: hidden;
padding: 0;
}
#djDebug .djToggleSwitch {
text-decoration: none;
border: 1px solid #999;
height: 12px;
width: 12px;
line-height: 12px;
text-align: center;
color: #777;
display: inline-block;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFF', endColorstr='#DCDCDC'); /* for IE */
background: -webkit-gradient(linear, left top, left bottom, from(#FFF), to(#DCDCDC)); /* for webkit browsers */
background:-moz-linear-gradient(center top , #FFFFFF 0pt, #DCDCDC 100%) repeat scroll 0 0 transparent;
}
#djDebug .djNoToggleSwitch {
height: 14px;
width: 14px;
display: inline-block;
}
#djDebug .djSQLDetailsDiv {
margin-top:0.8em;
}
#djDebug pre {
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
white-space: pre-wrap; /* CSS-3 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
color: #555;
border:1px solid #ccc;
border-collapse:collapse;
background-color:#fff;
display:block;
overflow: auto;
padding:2px 3px;
margin-bottom: 3px;
font-family:Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace;
}
#djDebug .djdt-stack span {
color: #000;
font-weight: bold;
}
#djDebug .djdt-stack span.djdt-path,
#djDebug .djdt-stack pre.djdt-locals,
#djDebug .djdt-stack pre.djdt-locals span {
color: #777;
font-weight: normal;
}
#djDebug .djdt-stack span.djdt-code {
font-weight: normal;
}
#djDebug .djdt-stack pre.djdt-locals {
margin: 0 27px 27px 27px;
}
#djDebug .djdt-width-20 {
width: 20%;
}
#djDebug .djdt-width-60 {
width: 60%;
}
#djDebug .djdt-highlighted {
background-color: lightgrey;
}
.djdt-hidden {
display: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

View File

@ -0,0 +1 @@
document.getElementById('redirect_to').focus();

View File

@ -0,0 +1,324 @@
(function () {
var $$ = {
on: function(root, eventName, selector, fn) {
root.addEventListener(eventName, function(event) {
var target = event.target.closest(selector);
if (root.contains(target)) {
fn.call(target, event);
}
});
},
show: function(element) {
element.style.display = 'block';
},
hide: function(element) {
element.style.display = 'none';
},
toggle: function(element, value) {
if (value) {
$$.show(element);
} else {
$$.hide(element);
}
},
visible: function(element) {
style = getComputedStyle(element);
return style.display !== 'none';
},
executeScripts: function(root) {
root.querySelectorAll('script').forEach(function(e) {
var clone = document.createElement('script');
clone.src = e.src;
root.appendChild(clone);
});
},
};
var onKeyDown = function(event) {
if (event.keyCode == 27) {
djdt.hide_one_level();
}
};
var ajax = function(url, init) {
init = Object.assign({credentials: 'same-origin'}, init);
return fetch(url, init).then(function(response) {
if (response.ok) {
return response.text();
} else {
var win = document.querySelector('#djDebugWindow');
win.innerHTML = '<div class="djDebugPanelTitle"><a class="djDebugClose djDebugBack" href=""></a><h3>'+response.status+': '+response.statusText+'</h3></div>';
$$.show(win);
return Promise.reject();
}
});
};
var djdt = {
handleDragged: false,
events: {
ready: []
},
isReady: false,
init: function() {
var djDebug = document.querySelector('#djDebug');
$$.show(djDebug);
$$.on(djDebug.querySelector('#djDebugPanelList'), 'click', 'li a', function(event) {
event.preventDefault();
if (!this.className) {
return;
}
var current = djDebug.querySelector('#' + this.className);
if ($$.visible(current)) {
djdt.hide_panels();
} else {
djdt.hide_panels();
$$.show(current);
this.parentElement.classList.add('djdt-active');
var inner = current.querySelector('.djDebugPanelContent .djdt-scroll'),
store_id = djDebug.dataset.storeId;
if (store_id && inner.children.length === 0) {
var url = djDebug.dataset.renderPanelUrl;
var url_params = new URLSearchParams();
url_params.append('store_id', store_id);
url_params.append('panel_id', this.className);
url += '?' + url_params.toString();
ajax(url).then(function(body) {
inner.previousElementSibling.remove(); // Remove AJAX loader
inner.innerHTML = body;
$$.executeScripts(inner);
});
}
}
});
$$.on(djDebug, 'click', 'a.djDebugClose', function(event) {
event.preventDefault();
djdt.hide_one_level();
});
$$.on(djDebug, 'click', '.djDebugPanelButton input[type=checkbox]', function() {
djdt.cookie.set(this.dataset.cookie, this.checked ? 'on' : 'off', {
path: '/',
expires: 10
});
});
// Used by the SQL and template panels
$$.on(djDebug, 'click', '.remoteCall', function(event) {
event.preventDefault();
var name = this.tagName.toLowerCase();
var ajax_data = {};
if (name == 'button') {
var form = this.closest('form');
ajax_data.url = this.getAttribute('formaction');
if (form) {
ajax_data.body = new FormData(form);
ajax_data.method = form.getAttribute('method') || 'POST';
}
}
if (name == 'a') {
ajax_data.url = this.getAttribute('href');
}
ajax(ajax_data.url, ajax_data).then(function(body) {
var win = djDebug.querySelector('#djDebugWindow');
win.innerHTML = body;
$$.executeScripts(win);
$$.show(win);
});
});
// Used by the cache, profiling and SQL panels
$$.on(djDebug, 'click', 'a.djToggleSwitch', function(event) {
event.preventDefault();
var self = this;
var id = this.dataset.toggleId;
var open_me = this.textContent == this.dataset.toggleOpen;
if (id === '' || !id) {
return;
}
var name = this.dataset.toggleName;
var container = this.closest('.djDebugPanelContent').querySelector('#' + name + '_' + id);
container.querySelectorAll('.djDebugCollapsed').forEach(function(e) {
$$.toggle(e, open_me);
});
container.querySelectorAll('.djDebugUncollapsed').forEach(function(e) {
$$.toggle(e, !open_me);
});
this.closest('.djDebugPanelContent').querySelectorAll('.djToggleDetails_' + id).forEach(function(e) {
if (open_me) {
e.classList.add('djSelected');
e.classList.remove('djUnselected');
self.textContent = self.dataset.toggleClose;
} else {
e.classList.remove('djSelected');
e.classList.add('djUnselected');
self.textContent = self.dataset.toggleOpen;
}
var switch_ = e.querySelector('.djToggleSwitch')
if (switch_) switch_.textContent = self.textContent;
});
});
djDebug.querySelector('#djHideToolBarButton').addEventListener('click', function(event) {
event.preventDefault();
djdt.hide_toolbar(true);
});
djDebug.querySelector('#djShowToolBarButton').addEventListener('click', function(event) {
event.preventDefault();
if (!djdt.handleDragged) {
djdt.show_toolbar();
}
});
var startPageY, baseY;
var handle = document.querySelector('#djDebugToolbarHandle');
var onHandleMove = function(event) {
// Chrome can send spurious mousemove events, so don't do anything unless the
// cursor really moved. Otherwise, it will be impossible to expand the toolbar
// due to djdt.handleDragged being set to true.
if (djdt.handleDragged || event.pageY != startPageY) {
var top = baseY + event.pageY;
if (top < 0) {
top = 0;
} else if (top + handle.offsetHeight > window.innerHeight) {
top = window.innerHeight - handle.offsetHeight;
}
handle.style.top = top + 'px';
djdt.handleDragged = true;
}
};
djDebug.querySelector('#djShowToolBarButton').addEventListener('mousedown', function(event) {
event.preventDefault();
startPageY = event.pageY;
baseY = handle.offsetTop - startPageY;
document.addEventListener('mousemove', onHandleMove);
});
document.addEventListener('mouseup', function (event) {
document.removeEventListener('mousemove', onHandleMove);
if (djdt.handleDragged) {
event.preventDefault();
djdt.cookie.set('djdttop', handle.offsetTop, {
path: '/',
expires: 10
});
setTimeout(function () {
djdt.handleDragged = false;
}, 10);
}
});
if (djdt.cookie.get('djdt') == 'hide') {
djdt.hide_toolbar(false);
} else {
djdt.show_toolbar();
}
djdt.isReady = true;
djdt.events.ready.forEach(function(callback) {
callback(djdt);
});
},
hide_panels: function() {
$$.hide(djDebug.querySelector('#djDebugWindow'));
djDebug.querySelectorAll('.djdt-panelContent').forEach(function(e) {
$$.hide(e);
});
djDebug.querySelectorAll('#djDebugToolbar li').forEach(function(e) {
e.classList.remove('djdt-active');
});
},
hide_toolbar: function(setCookie) {
djdt.hide_panels();
$$.hide(djDebug.querySelector('#djDebugToolbar'));
var handle = document.querySelector('#djDebugToolbarHandle');
$$.show(handle);
// set handle position
var handleTop = djdt.cookie.get('djdttop');
if (handleTop) {
handleTop = Math.min(handleTop, window.innerHeight - handle.offsetHeight);
handle.style.top = handleTop + 'px';
}
document.removeEventListener('keydown', onKeyDown);
if (setCookie) {
djdt.cookie.set('djdt', 'hide', {
path: '/',
expires: 10
});
}
},
hide_one_level: function(skipDebugWindow) {
if ($$.visible(djDebug.querySelector('#djDebugWindow'))) {
$$.hide(djDebug.querySelector('#djDebugWindow'));
} else if (djDebug.querySelector('#djDebugToolbar li.djdt-active')) {
djdt.hide_panels();
} else {
djdt.hide_toolbar(true);
}
},
show_toolbar: function() {
document.addEventListener('keydown', onKeyDown);
$$.hide(djDebug.querySelector('#djDebugToolbarHandle'));
$$.show(djDebug.querySelector('#djDebugToolbar'));
djdt.cookie.set('djdt', 'show', {
path: '/',
expires: 10
});
},
ready: function(callback){
if (djdt.isReady) {
callback(djdt);
} else {
djdt.events.ready.push(callback);
}
},
cookie: {
get: function(key){
if (document.cookie.indexOf(key) === -1) return null;
var cookieArray = document.cookie.split('; '),
cookies = {};
cookieArray.forEach(function(e){
var parts = e.split('=');
cookies[ parts[0] ] = parts[1];
});
return cookies[ key ];
},
set: function(key, value, options){
options = options || {};
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setDate(t.getDate() + days);
}
document.cookie = [
encodeURIComponent(key) + '=' + String(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '',
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join('');
return value;
}
},
};
window.djdt = {
show_toolbar: djdt.show_toolbar,
hide_toolbar: djdt.hide_toolbar,
init: djdt.init,
close: djdt.hide_one_level,
cookie: djdt.cookie,
};
document.addEventListener('DOMContentLoaded', djdt.init);
})();

View File

@ -0,0 +1,52 @@
(function () {
// Browser timing remains hidden unless we can successfully access the performance object
var perf = window.performance || window.msPerformance ||
window.webkitPerformance || window.mozPerformance;
if (!perf)
return;
var rowCount = 0,
timingOffset = perf.timing.navigationStart,
timingEnd = perf.timing.loadEventEnd,
totalTime = timingEnd - timingOffset;
function getLeft(stat) {
return ((perf.timing[stat] - timingOffset) / (totalTime)) * 100.0;
}
function getCSSWidth(stat, endStat) {
var width = ((perf.timing[endStat] - perf.timing[stat]) / (totalTime)) * 100.0;
// Calculate relative percent (same as sql panel logic)
width = 100.0 * width / (100.0 - getLeft(stat));
return (width < 1) ? "2px" : width + "%";
}
function addRow(stat, endStat) {
rowCount++;
var row = document.createElement('tr');
row.className = (rowCount % 2) ? 'djDebugOdd' : 'djDebugEven';
if (endStat) {
// Render a start through end bar
row.innerHTML = '<td>' + stat.replace('Start', '') + '</td>' +
'<td class="djdt-timeline"><div class="djDebugTimeline"><div class="djDebugLineChart"><strong>&#160;</strong></div></div></td>' +
'<td>' + (perf.timing[stat] - timingOffset) + ' (+' + (perf.timing[endStat] - perf.timing[stat]) + ')</td>';
row.querySelector('strong').style.width = getCSSWidth(stat, endStat);
} else {
// Render a point in time
row.innerHTML = '<td>' + stat + '</td>' +
'<td class="djdt-timeline"><div class="djDebugTimeline"><div class="djDebugLineChart"><strong>&#160;</strong></div></div></td>' +
'<td>' + (perf.timing[stat] - timingOffset) + '</td>';
row.querySelector('strong').style.width = '2px';
}
row.querySelector('.djDebugLineChart').style.left = getLeft(stat) + '%';
document.querySelector('#djDebugBrowserTimingTableBody').appendChild(row);
}
// This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param)
addRow('domainLookupStart', 'domainLookupEnd');
addRow('connectStart', 'connectEnd');
addRow('requestStart', 'responseEnd'); // There is no requestEnd
addRow('responseStart', 'responseEnd');
addRow('domLoading', 'domComplete'); // Spans the events below
addRow('domInteractive');
addRow('domContentLoadedEventStart', 'domContentLoadedEventEnd');
addRow('loadEventStart', 'loadEventEnd');
document.querySelector('#djDebugBrowserTiming').classList.remove('djdt-hidden');
})();

View File

@ -13,7 +13,7 @@
"note": note_id
}
).done(function(){
$("#alias_table").load(location.href+ " #alias_table");
$("#alias_table").load(location.pathname+ " #alias_table");
addMsg("Alias ajouté","success");
})
.fail(function(xhr, textStatus, error){
@ -29,7 +29,7 @@
})
.done(function(){
addMsg('Alias supprimé','success');
$("#alias_table").load(location.href + " #alias_table");
$("#alias_table").load(location.pathname + " #alias_table");
})
.fail(function(xhr,textStatus, error){
errMsg(xhr.responseJSON);

View File

@ -95,6 +95,8 @@ function li(id, text, extra_css) {
* @param note The concerned note.
*/
function displayStyle(note) {
if (!note)
return "";
let balance = note.balance;
var css = "";
if (balance < -5000)
@ -130,7 +132,6 @@ function displayNote(note, alias, user_note_field = null, profile_pic_field = nu
if (profile_pic_field != null) {
$("#" + profile_pic_field).attr('src', img);
$("#" + profile_pic_field).click(function () {
console.log(note);
if (note.resourcetype === "NoteUser") {
document.location.href = "/accounts/user/" + note.user;
} else if (note.resourcetype === "NoteClub") {
@ -275,7 +276,6 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
field.attr('data-original-title', aliases_matched_html).tooltip('show');
consumers.results.forEach(function (consumer) {
let note = consumer.note;
let consumer_obj = $("#" + alias_prefix + "_" + consumer.id);
consumer_obj.hover(function () {
displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field)
@ -283,8 +283,8 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
consumer_obj.click(function () {
var disp = null;
notes_display.forEach(function (d) {
// We compare the note ids
if (d.id === note.id) {
// We compare the alias ids
if (d.id === consumer.id) {
d.quantity += 1;
disp = d;
}
@ -294,7 +294,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
disp = {
name: consumer.name,
id: consumer.id,
note: note,
note: consumer.note,
quantity: 1
};
notes_display.push(disp);
@ -343,7 +343,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
// When a validate button is clicked, we switch the validation status
function de_validate(id, validated) {
let invalidity_reason = $("#invalidity_reason_" + id).val();
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳</strong>");
// Perform a PATCH request to the API in order to update the transaction
// If the user has insufficient rights, an error message will appear
@ -369,7 +369,7 @@ function de_validate(id, validated) {
},
error: function (err) {
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + err.responseText, "danger");
"de cette transaction : " + JSON.parse(err.responseText)["detail"], "danger", 10000);
refreshBalance();
// error if this method doesn't exist. Please define it.

View File

@ -156,7 +156,7 @@ function reset() {
function consumeAll() {
notes_display.forEach(function(note_display) {
buttons.forEach(function(button) {
consume(note_display.note.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
});
});
@ -164,7 +164,7 @@ function consumeAll() {
/**
* Create a new transaction from a button through the API.
* @param source The note that paid the item (type: int)
* @param source The note that paid the item (type: note)
* @param source_alias The alias used for the source (type: str)
* @param dest The note that sold the item (type: int)
* @param quantity The quantity sold (type: int)
@ -184,13 +184,25 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
"valid": true,
"polymorphic_ctype": type,
"resourcetype": "RecurrentTransaction",
"source": source,
"source": source.id,
"source_alias": source_alias,
"destination": dest,
"category": category,
"template": template
}, reset).fail(function (e) {
$.post("/api/note/transaction/transaction/",
})
.done(function () {
if (!isNaN(source.balance)) {
let newBalance = source.balance - quantity * amount;
if (newBalance <= -5000)
addMsg("Attention, la note émettrice " + source_alias + " passe en négatif sévère.",
"danger", 10000);
else if (newBalance < 0)
addMsg("Attention, la note émettrice " + source_alias + " passe en négatif.",
"warning", 10000);
}
reset();
}).fail(function (e) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": quantity,
@ -207,10 +219,10 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
"template": template
}).done(function() {
reset();
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger");
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000);
}).fail(function () {
reset();
errMsg(e.responseJSON);
errMsg(e.responseJSON, 10000);
});
});
}

View File

@ -7,22 +7,38 @@ function refreshHistory() {
$("#history").load("/note/transfer/ #history");
}
function reset() {
function reset(refresh=true) {
sources_notes_display.length = 0;
sources.length = 0;
dests_notes_display.length = 0;
dests.length = 0;
$("#source_note_list").html("");
$("#dest_note_list").html("");
$("#amount").val("");
$("#reason").val("");
let source_field = $("#source_note");
source_field.val("");
source_field.trigger("keyup");
source_field.removeClass('is-invalid');
let dest_field = $("#dest_note");
dest_field.val("");
dest_field.trigger("keyup");
dest_field.removeClass('is-invalid');
let amount_field = $("#amount");
amount_field.val("");
amount_field.removeClass('is-invalid');
$("#amount-required").html("");
let reason_field = $("#reason");
reason_field.val("");
reason_field.removeClass('is-invalid');
$("#reason-required").html("");
$("#last_name").val("");
$("#first_name").val("");
$("#bank").val("");
$("#user_note").val("");
$("#profile_pic").attr("src", "/media/pic/default.png");
refreshBalance();
refreshHistory();
if (refresh) {
refreshBalance();
refreshHistory();
}
}
$(document).ready(function() {
@ -71,17 +87,10 @@ $(document).ready(function() {
let source = $("#source_note");
let dest = $("#dest_note");
$("#type_gift").click(function() {
$("#special_transaction_div").addClass('d-none');
source.attr('disabled', true);
source.val(username);
source.tooltip('hide');
$("#source_note_list").addClass('d-none');
dest.attr('disabled', false);
$("#dest_note_list").removeClass('d-none');
});
$("#type_transfer").click(function() {
$("#source_me_div").removeClass('d-none');
$("#source_note").removeClass('is-invalid');
$("#dest_note").removeClass('is-invalid');
$("#special_transaction_div").addClass('d-none');
source.attr('disabled', false);
$("#source_note_list").removeClass('d-none');
@ -90,6 +99,9 @@ $(document).ready(function() {
});
$("#type_credit").click(function() {
$("#source_me_div").addClass('d-none');
$("#source_note").removeClass('is-invalid');
$("#dest_note").removeClass('is-invalid');
$("#special_transaction_div").removeClass('d-none');
$("#source_note_list").addClass('d-none');
$("#dest_note_list").removeClass('d-none');
@ -107,6 +119,9 @@ $(document).ready(function() {
});
$("#type_debit").click(function() {
$("#source_me_div").addClass('d-none');
$("#source_note").removeClass('is-invalid');
$("#dest_note").removeClass('is-invalid');
$("#special_transaction_div").removeClass('d-none');
$("#source_note_list").removeClass('d-none');
$("#dest_note_list").addClass('d-none');
@ -131,13 +146,11 @@ $(document).ready(function() {
dest.val(type);
});
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
let type_gift = $("#type_gift"); // Default mode
type_gift.removeAttr('checked');
$("#type_transfer").removeAttr('checked');
// Ensure we begin in transfer mode. Removing these lines may cause problems when reloading.
let type_transfer = $("#type_transfer"); // Default mode
type_transfer.removeAttr('checked');
$("#type_credit").removeAttr('checked');
$("#type_debit").removeAttr('checked');
$("label[for='type_gift']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
@ -145,70 +158,81 @@ $(document).ready(function() {
if (location.hash)
$("#type_" + location.hash.substr(1)).click();
else
type_gift.click();
type_transfer.click();
location.hash = "";
$("#source_me").click(function() {
// Shortcut to set the current user as the only emitter
sources_notes_display.length = 0;
sources.length = 0;
$("#source_note_list").html("");
let source_note = $("#source_note");
source_note.focus();
source_note.val("");
let event = jQuery.Event("keyup");
event.originalEvent = {charCode: 97};
source_note.trigger(event);
source_note.val(username);
event = jQuery.Event("keyup");
event.originalEvent = {charCode: 97};
source_note.trigger(event);
let fill_note = function() {
if (sources.length === 0) {
setTimeout(fill_note, 100);
return;
}
event = jQuery.Event("keypress");
event.originalEvent = {charCode: 13};
source_note.trigger(event);
source_note.tooltip('hide');
source_note.val('');
$("#dest_note").focus();
};
fill_note();
});
});
$("#btn_transfer").click(function() {
if ($("#type_gift").is(':checked')) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": user_id,
"destination": dest.note.id,
"destination_alias": dest.name
}).done(function () {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
let error = false;
reset();
}).fail(function () { // do it again but valid = false
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": false,
"invalidity_reason": "Solde insuffisant",
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": user_id,
"destination": dest.note.id,
"destination_alias": dest.name
}).done(function () {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger");
let amount_field = $("#amount");
amount_field.removeClass('is-invalid');
$("#amount-required").html("");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
let reason_field = $("#reason");
reason_field.removeClass('is-invalid');
$("#reason-required").html("");
reset();
});
});
});
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
amount_field.addClass('is-invalid');
$("#amount-required").html("<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>");
error = true;
}
else if ($("#type_transfer").is(':checked')) {
sources_notes_display.forEach(function (source) {
dests_notes_display.forEach(function (dest) {
if (!reason_field.val()) {
reason_field.addClass('is-invalid');
$("#reason-required").html("<strong>Ce champ est requis.</strong>");
error = true;
}
if (error)
return;
let amount = 100 * amount_field.val();
let reason = reason_field.val();
if ($("#type_transfer").is(':checked')) {
// We copy the arrays to ensure that transactions are well-processed even if the form is reset
[...sources_notes_display].forEach(function (source) {
[...dests_notes_display].forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
@ -217,18 +241,37 @@ $("#btn_transfer").click(function() {
"destination": dest.note.id,
"destination_alias": dest.name
}).done(function () {
if (!isNaN(source.note.balance)) {
let newBalance = source.note.balance - source.quantity * dest.quantity * amount;
if (newBalance <= -5000) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note "
+ source.name + " vers la note " + dest.name + " a été fait avec succès, " +
"mais la note émettrice passe en négatif sévère.", "danger", 10000);
reset();
return;
}
else if (newBalance < 0) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note "
+ source.name + " vers la note " + dest.name + " a été fait avec succès, " +
"mais la note émettrice passe en négatif.", "warning", 10000);
reset();
return;
}
}
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success", 10000);
reset();
}).fail(function (err) { // do it again but valid = false
}).fail(function () { // do it again but valid = false
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"amount": amount,
"reason": reason,
"valid": false,
"invalidity_reason": "Solde insuffisant",
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
@ -239,16 +282,12 @@ $("#btn_transfer").click(function() {
"destination_alias": dest.name
}).done(function () {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger");
reset();
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000);
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
});
@ -256,20 +295,30 @@ $("#btn_transfer").click(function() {
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val();
let user_note;
let given_reason = $("#reason").val();
let source, dest, reason;
let given_reason = reason;
let source_id, dest_id;
if ($("#type_credit").is(':checked')) {
if (!dests_notes_display.length) {
$("#dest_note").addClass('is-invalid');
return;
}
user_note = dests_notes_display[0].note.id;
source = special_note;
dest = user_note;
source_id = special_note;
dest_id = user_note;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
else {
if (!sources_notes_display.length) {
$("#source_note").addClass('is-invalid');
return;
}
user_note = sources_notes_display[0].note.id;
source = user_note;
dest = special_note;
source_id = user_note;
dest_id = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
@ -278,24 +327,24 @@ $("#btn_transfer").click(function() {
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": 100 * $("#amount").val(),
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction",
"source": source,
"source_alias": source.name,
"destination": dest,
"destination_alias": dest.name,
"source": source_id,
"source_alias": sources_notes_display[0].name,
"destination": dest_id,
"destination_alias": dests_notes_display[0].name,
"last_name": $("#last_name").val(),
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}).done(function () {
addMsg("Le crédit/retrait a bien été effectué !", "success");
addMsg("Le crédit/retrait a bien été effectué !", "success", 10000);
reset();
}).fail(function (err) {
addMsg("Le crédit/retrait a échoué : " + err.responseText, "danger");
reset();
addMsg("Le crédit/retrait a échoué : " + JSON.parse(err.responseText)["detail"],
"danger", 10000);
});
}
});

View File

@ -92,7 +92,7 @@
})
.done(function() {
addMsg('Invité supprimé','success');
$("#guests_table").load(location.href + " #guests_table");
$("#guests_table").load(location.pathname + " #guests_table");
})
.fail(function(xhr, textStatus, error) {
errMsg(xhr.responseJSON);

View File

@ -51,7 +51,7 @@
if ((pattern === old_pattern || pattern === "") && !force)
return;
$("#entry_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init);
$("#entry_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init);
refreshBalance();
}

View File

@ -77,6 +77,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block extracss %}{% endblock %}
</head>
<body class="d-flex w-100 h-100 flex-column">
{% if debug %}<div style="background:#ffeeba;text-align:center">Mode DEBUG activé.</div>{% endif %}
<main class="mb-auto">
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
<a class="navbar-brand" href="/">{{ request.site.name }}</a>
@ -98,7 +99,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fas fa-exchange-alt"></i>{% trans 'Transfer' %} </a>
</li>
{% endif %}
{% if "auth.user"|model_list|length >= 2 %}
{% if "auth.user"|model_list_length >= 2 %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a>
</li>
@ -108,13 +109,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fas fa-users"></i> {% trans 'Clubs' %}</a>
</li>
{% endif %}
{% if "member.change_profile_registration_valid"|has_perm:user %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'registration:future_user_list' %}">
<i class="fas fa-user-plus"></i> {% trans "Registrations" %}
</a>
</li>
{% endif %}
{% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fas fa-calendar"></i> {% trans 'Activities' %}</a>
@ -130,12 +124,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="{% url 'wei:current_wei_detail' %}"><i class="fas fa-bus"></i> {% trans 'WEI' %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a>
</li>
{% if user.is_staff %}
{% if user.is_authenticated %}
<li class="nav-item active">
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Administration' %}</a>
<a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a>
</li>
{% endif %}
{% if user.is_staff and ""|has_perm:user %}
<li class="nav-item active">
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Admin' %}</a>
</li>
{% endif %}
</ul>
@ -178,6 +174,17 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
{% endif %}
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div class="alert alert-warning alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button>
Attention : la Note Kfet 2020 est en phase de beta. Des fonctionnalités pourront être rajoutées d'ici à la version
finale, et des bugs peuvent survenir. Pour tout problème, merci d'envoyer un mail à l'adresse
<a href="mailto:&#110;&#111;&#116;&#101;&#107;&#102;&#101;&#116;&#50;&#48;&#50;&#48;&commat;&#108;&#105;&#115;&#116;&#115;&period;&#99;&#114;&#97;&#110;&#115;&period;&#111;&#114;&#103;">
&#110;&#111;&#116;&#101;&#107;&#102;&#101;&#116;&#50;&#48;&#50;&#48;&commat;&#108;&#105;&#115;&#116;&#115;&period;&#99;&#114;&#97;&#110;&#115;&period;&#111;&#114;&#103;</a>,
ou bien levez une issue sur le dépôt <a href="https://gitlab.crans.org/bde/nk20/-/issues">Gitlab</a>,
ou encore posez un commentaire sur le <a href="https://pad.crans.org/p/todoNK20">pad</a>.<br><br>
Certaines données ont été anonymisées afin de limiter les fuites de données, et peuvent ne pas correspondre avec vos données réelles.
</div>
<div id="messages"></div>
{% block content %}
<p>Default content...</p>

View File

@ -11,7 +11,7 @@
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.name}}</dd>
<dd class="col-xl-6">{{ club.name }}</dd>
{% if club.parent_club %}
<dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>

View File

@ -4,9 +4,6 @@
{% block content %}
<div class="row justify-content-center mb-4">
<div class="col-md-10 text-center">
<h4>
{% trans "search clubs" %}
</h4>
<input class="form-control mx-auto w-25" type="text" id="search_field"/>
<hr>
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
@ -36,7 +33,7 @@
function reloadTable() {
let pattern = searchbar_obj.val();
$("#club_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #club_table", init);
$("#club_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #club_table", init);
}
searchbar_obj.keyup(function() {

View File

@ -0,0 +1,69 @@
{% extends "member/noteowner_detail.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block profile_info %}
{% include "member/club_info.html" %}
{% endblock %}
{% block profile_content %}
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
<div class="form-group">
<div class="form-check">
<label class="form-check-label" for="only_active">
<input type="checkbox" class="checkboxinput form-check-input" id="only_active"
{% if only_active %}checked{% endif %}>
{% trans "Display only active memberships" %}
</label>
</div>
</div>
<div id="div_id_roles" class="form-group">
<label for="id_roles" class="col-form-label">{% trans "Filter roles:" %}</label>
<div class="">
<select name="roles" class="selectmultiple form-control" id="roles" multiple="">
{% for role in applicable_roles %}
<option value="{{ role.id }}" selected>{{ role.name }}</option>
{% endfor %}
</select>
</div>
</div>
<hr>
<div id="memberships_table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no membership found with this pattern." %}
</div>
{% endif %}
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function () {
let searchbar_obj = $("#searchbar");
let only_active_obj = $("#only_active");
let roles_obj = $("#roles");
function reloadTable() {
let pattern = searchbar_obj.val();
let roles = [];
$("#roles option:selected").each(function() {
roles.push($(this).val());
});
let roles_str = roles.join(',');
$("#memberships_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20")
+ "&only_active=" + (only_active_obj.is(':checked') ? '1' : '0')
+ "&roles=" + roles_str + " #memberships_table");
}
searchbar_obj.keyup(reloadTable);
only_active_obj.change(reloadTable);
roles_obj.change(reloadTable);
});
</script>
{% endblock %}

View File

@ -1,10 +1,23 @@
{% load render_table from django_tables2 %}
{% load i18n %}
{% if managers.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold">
<i class="fa fa-users"></i> {% trans "Club managers" %}
</a>
</div>
{% render_table managers %}
</div>
<hr>
{% endif %}
{% if member_list.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-users"></i> {% trans "Member of the Club" %}
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Club members" %}
</a>
</div>
{% render_table member_list %}
@ -16,7 +29,7 @@
{% if history_list.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>

View File

@ -11,7 +11,7 @@
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-users"></i> {% trans "View my memberships" %}
</a>
</div>
@ -22,7 +22,7 @@
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link collapsed font-weight-bold"
<a class="collapsed font-weight-bold"
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="true" aria-controls="historyListCollapse">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load perms %}
{% block content %}
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
@ -18,6 +19,13 @@
{% endif %}
</div>
<hr>
{% if "member.change_profile_registration_valid"|has_perm:user %}
<a class="btn btn-block btn-secondary" href="{% url 'registration:future_user_list' %}">
<i class="fas fa-user-plus"></i> {% trans "Registrations" %}
</a>
{% endif %}
{% endblock %}
{% block extrajavascript %}
@ -34,7 +42,7 @@
if (pattern === old_pattern || pattern === "")
return;
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$("#user_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
}
searchbar_obj.keyup(function() {

View File

@ -8,4 +8,5 @@
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
<p id="amount-required" class="invalid-feedback"></p>
</div>

View File

@ -68,15 +68,15 @@
<div class="card shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Most used buttons" %}
{% trans "Highlighted buttons" %}
</p>
</div>
<div class="card-body text-nowrap" style="overflow:auto hidden">
<div class="d-inline-flex flex-wrap justify-content-center" id="most_used">
{% for button in most_used %}
<div class="d-inline-flex flex-wrap justify-content-center" id="highlighted">
{% for button in highlighted %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
id="most_used_button{{ button.id }}" name="button" value="{{ button.name }}">
id="highlighted_button{{ button.id }}" name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
@ -86,7 +86,7 @@
</div>
{# Regroup buttons under categories #}
{% regroup transaction_templates by category as categories %}
{# {% regroup transaction_templates by category as categories %} #}
<div class="card border-primary text-center shadow mb-4">
{# Tabs for button categories #}
@ -94,8 +94,8 @@
<ul class="nav nav-tabs nav-fill card-header-tabs">
{% for category in categories %}
<li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.grouper|slugify }}">
{{ category.grouper }}
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.name|slugify }}">
{{ category.name }}
</a>
</li>
{% endfor %}
@ -106,9 +106,9 @@
<div class="card-body">
<div class="tab-content">
{% for category in categories %}
<div class="tab-pane" id="{{ category.grouper|slugify }}">
<div class="tab-pane" id="{{ category.name|slugify }}">
<div class="d-inline-flex flex-wrap justify-content-center">
{% for button in category.list %}
{% for button in category.templates_filtered %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
id="button{{ button.id }}" name="button" value="{{ button.name }}">
@ -157,24 +157,26 @@
{% block extrajavascript %}
<script type="text/javascript" src="{% static "js/consos.js" %}"></script>
<script type="text/javascript">
{% for button in most_used %}
{% for button in highlighted %}
{% if button.display %}
$("#most_used_button{{ button.id }}").click(function() {
addConso({{ button.destination.id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
{{ button.id }}, "{{ button.name }}");
$("#highlighted_button{{ button.id }}").click(function() {
addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}");
});
{% endif %}
{% endfor %}
{% for button in transaction_templates %}
{% for category in categories %}
{% for button in category.templates_filtered %}
{% if button.display %}
$("#button{{ button.id }}").click(function() {
addConso({{ button.destination.id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
{{ button.id }}, "{{ button.name }}");
addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}");
});
{% endif %}
{% endfor %}
{% endfor %}
</script>
{% endblock %}

View File

@ -10,11 +10,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
<label for="type_gift" class="btn btn-sm btn-outline-primary active">
<input type="radio" name="transaction_type" id="type_gift" checked>
{% trans "Gift" %}
</label>
<label for="type_transfer" class="btn btn-sm btn-outline-primary">
<label for="type_transfer" class="btn btn-sm btn-outline-primary active">
<input type="radio" name="transaction_type" id="type_transfer">
{% trans "Transfer" %}
</label>
@ -59,6 +55,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="source_note" placeholder="{% trans "Name or alias..." %}" />
<div id="source_me_div">
<hr>
<span class="form-control mx-auto d-block btn btn-secondary" id="source_me">
{% trans "I am the emitter" %}
</span>
</div>
</div>
</div>
</div>
@ -100,7 +102,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="form-row">
<div class="col-md-12">
<label for="reason">{% trans "Reason" %} :</label>
<input class="form-control mx-auto d-block" type="text" id="reason" required />
<input class="form-control mx-auto d-block" type="text" id="reason" />
<p id="reason-required" class="invalid-feedback"></p>
</div>
</div>
@ -160,7 +163,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }};
SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }};
user_id = {{ user.note.pk }};
username = "{{ user.username }}";
username = "{{ user.username|escapejs }}";
</script>
<script src="/static/js/transfer.js"></script>
{% endblock %}

View File

@ -5,18 +5,7 @@
{% block content %}
<div class="row justify-content-center mb-4">
<div class="col-md-10 text-center">
<h4>
{% trans "Search button" %}
</h4>
<input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}">
<div class="form-group">
<div id="div_active_only" class="form-check">
<label for="active_only" class="form-check-label">
<input type="checkbox" name="active_only" class="checkboxinput form-check-input" checked="" id="active_only">
{% trans "Display visible buttons only" %}
</label>
</div>
</div>
<hr>
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a>
</div>
@ -36,66 +25,39 @@
{% endblock %}
{% block extrajavascript %}
<script>
/* fonction appelée à la fin du timer */
function getInfo() {
var asked = $("#search_field").val();
/* on ne fait la requête que si on a au moins un caractère pour chercher */
if (asked.length >= 1) {
$.getJSON("/api/note/transaction/template/?format=json&search=" + asked + ($("#active_only").is(":checked") ? "&display=true" : ""), function(buttons) {
console.log(buttons);
let selected_id = buttons.results.map((a => "#row-" + a.id));
console.log(".table-row " + selected_id.join());
$(".table-row " + selected_id.join()).removeClass('d-none');
$(".table-row").not(selected_id.join()).addClass('d-none');
});
}
else {
if ($("#active_only").is(":checked")) {
$('.table-success').removeClass('d-none');
$('.table-danger').addClass('d-none');
}
else {
// show everything
$('table tr').removeClass('d-none');
}
}
}
<script type="text/javascript">
$(document).ready(function() {
let searchbar_obj = $("#search_field");
var timer_on = false;
var timer;
var timer;
var timer_on;
/* Fontion appelée quand le texte change (délenche le timer) */
function search_field_moved() {
if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur.
clearTimeout(timer);
timer = setTimeout(getInfo, 300);
function reloadTable() {
let pattern = searchbar_obj.val();
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table");
}
else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
timer = setTimeout(getInfo, 300);
searchbar_obj.keyup(function() {
if (timer_on)
clearTimeout(timer);
timer_on = true;
}
}
setTimeout(reloadTable, 0);
});
});
// on click of button "delete" , call the API
function delete_button(button_id) {
$.ajax({
url:"/api/note/transaction/template/"+button_id+"/",
url:"/api/note/transaction/template/" + button_id + "/",
method:"DELETE",
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
})
.done(function(){
.done(function() {
addMsg('{% trans "button successfully deleted "%}','success');
$("#buttons_table").load("{% url 'note:template_list' %} #buttons_table");
$("#buttons_table").load(location.pathname + "?search=" + $("#search_field").val().replace(" ", "%20") + " #buttons_table");
})
.fail(function(){
addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' )
.fail(function() {
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
});
}
$(document).ready(function() {
$("#search_field").keyup(search_field_moved);
$("#active_only").change(search_field_moved);
search_field_moved();
});
</script>
{% endblock %}

View File

@ -15,15 +15,15 @@
{% regroup active_memberships by roles as memberships_per_role %}
{% for role in roles %}
<li class="{% if not role.clubs %}no-club{% endif %}">
{{ role }} {% if role.weirole %}(<em>Pour le WEI</em>){% endif %}
{{ role }} {% if role.weirole %}(<em>Pour le WEI</em>){% endif %} {% if role.for_club %}(<em>Pour le club {{ role.for_club }} uniquement</em>){% endif %}
{% if role.clubs %}
<div class="alert alert-success">
{% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }}
</div>
{% endif %}
<ul>
{% for permission in role.permissions.permissions.all %}
<li data-toggle="tooltip" title="{% trans "Query:" %} {{ permission.query }}">{{ permission }} ({{ permission.type }} {{ permission.model }}{% if permission.permanent %}, {% trans "permanent" %}{% endif %})</li>
{% for permission in role.permissions.all %}
<li data-toggle="tooltip" title="{% trans "Mask:" %} {{ permission.mask }}, {% trans "Query:" %} {{ permission.query }}">{{ permission }} ({{ permission.get_type_display }} {{ permission.model }}{% if permission.permanent %}, {% trans "permanent" %}{% endif %})</li>
{% empty %}
<em>{% trans "No associated permission" %}</em>
{% endfor %}

View File

@ -32,7 +32,7 @@
if (pattern === old_pattern || pattern === "")
return;
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$("#user_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$(".table-row").click(function() {
window.document.location = $(this).data("href");

View File

@ -10,12 +10,22 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% block content %}
{% if user.is_authenticated %}
<p class="errornote">
{% blocktrans trimmed %}
{% blocktrans trimmed with username=request.user.username %}
You are authenticated as {{ username }}, but are not authorized to
access this page. Would you like to login to a different account?
access this page. Would you like to login to a different account,
or with a higher permission mask?
{% endblocktrans %}
</p>
{% endif %}
{% if request.resolver_match.view_name == 'admin:login' %}
<div class="alert alert-info">
{% blocktrans trimmed %}
You must be logged with a staff account with the higher mask to access Django Admin.
{% endblocktrans %}
</div>
{% endif %}
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
{{ form | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">

View File

@ -55,5 +55,11 @@
<hr>
<h2>{% trans "Closed remittances" %}</h2>
{% render_table closed_remittances %}
{% if closed_remittances.data %}
{% render_table closed_remittances %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no closed remittance yet." %}
</div>
{% endif %}
{% endblock %}

View File

@ -22,7 +22,7 @@
{% if teams.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %}
</a>
</div>
@ -35,7 +35,7 @@
{% if memberships.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Members" %}
</a>
</div>

View File

@ -39,7 +39,7 @@
{% if memberships.data or True %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %}
</a>
</div>

View File

@ -4,12 +4,11 @@
{% block content %}
<div class="row justify-content-center mb-4">
<div class="col-md-10 text-center">
<h4>
{% trans "search WEI" %}
</h4>
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved()" id="search_field"/>
<hr>
<a class="btn btn-primary text-center my-4" href="{% url 'wei:wei_create' %}">{% trans "Create WEI" %}</a>
{% if can_create_wei %}
<hr>
<a class="btn btn-primary text-center my-4" href="{% url 'wei:wei_create' %}">{% trans "Create WEI" %}</a>
{% endif %}
</div>
</div>
<div class="row justify-content-center">

View File

@ -75,7 +75,7 @@
{% if buses.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Buses" %}
</a>
</div>
@ -88,7 +88,7 @@
{% if member_list.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold" href="{% url "wei:wei_memberships" pk=club.pk %}">
<a class="stretched-link font-weight-bold" href="{% url "wei:wei_memberships" pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Members of the WEI" %}
</a>
</div>
@ -101,7 +101,7 @@
{% if history_list.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<a class="font-weight-bold">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
@ -116,7 +116,7 @@
{% if pre_registrations.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link font-weight-bold" href="{% url 'wei:wei_registrations' pk=club.pk %}">
<a class="stretched-link font-weight-bold" href="{% url 'wei:wei_registrations' pk=club.pk %}">
<i class="fa fa-user-plus"></i> {% trans "Unvalidated registrations" %}
</a>
</div>

View File

@ -5,6 +5,7 @@
\usepackage[french]{babel}
\usepackage[margin=1.5cm]{geometry}
\usepackage{lmodern}
\usepackage{ltablex}
\usepackage{tabularx}
@ -13,11 +14,11 @@
\huge{Liste des inscrits \og {{ wei.name }} \fg{}}
{% if bus %}
\LARGE{Bus {{ bus.name }}}
\LARGE{Bus {{ bus.name|safe }}}
{% if team %}
\Large{Équipe {{ team.name }}}
\Large{Équipe {{ team.name|safe }}}
{% endif %}
{% endif %}
\end{center}

View File

@ -59,6 +59,12 @@
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.gender }}</dd>
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.clothing_cut }}</dd>
<dt class="col-xl-6">{% trans 'clothing size'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.clothing_size }}</dd>
<dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.birth_date }}</dd>

View File

@ -2,6 +2,7 @@
envlist =
py36-django22
py37-django22
py38-django22
linters
skipsdist = True