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 # Local data
secrets.py secrets.py
.env
map.json
*.log *.log
media/ media/
# Virtualenv # Virtualenv
env/ env/
venv/ venv/

View File

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

View File

@ -11,11 +11,15 @@
git: git:
repo: https://gitlab.crans.org/bde/nk20.git repo: https://gitlab.crans.org/bde/nk20.git
dest: /var/www/note_kfet dest: /var/www/note_kfet
version: beta-soon version: master
force: true force: true
- name: Use default env vars (should be updated!) - 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 - name: Update permissions for note_kfet dir
file: file:

View File

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

View File

@ -15,6 +15,11 @@
group: www-data group: www-data
state: link state: link
- name: Disable default Nginx site
file:
dest: /etc/nginx/sites-enabled/default
state: absent
- name: Copy conf of UWSGI - name: Copy conf of UWSGI
file: file:
src: /var/www/note_kfet/uwsgi_note.ini 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, amount=self.activity.activity_type.guest_entry_fee,
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name, reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
valid=True, valid=True,
guest=self.guest, entry=self,
).save() ).save()
return ret return ret
@ -240,8 +240,8 @@ class Guest(models.Model):
class GuestTransaction(Transaction): class GuestTransaction(Transaction):
guest = models.OneToOneField( entry = models.OneToOneField(
Guest, Entry,
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )

View File

@ -23,6 +23,7 @@ from .tables import ActivityTable, GuestTable, EntryTable
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Activity model = Activity
form_class = ActivityForm form_class = ActivityForm
extra_context = {"title": _("Create new activity")}
def form_valid(self, form): def form_valid(self, form):
form.instance.creater = self.request.user form.instance.creater = self.request.user
@ -37,12 +38,14 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
model = Activity model = Activity
table_class = ActivityTable table_class = ActivityTable
ordering = ('-date_start',) ordering = ('-date_start',)
extra_context = {"title": _("Activities")}
def get_queryset(self):
return super().get_queryset().distinct()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _("Activities")
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now()) upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
context['upcoming'] = ActivityTable( context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")), 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): class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Activity model = Activity
context_object_name = "activity" context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data() context = super().get_context_data()
@ -71,6 +75,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Activity model = Activity
form_class = ActivityForm form_class = ActivityForm
extra_context = {"title": _("Update activity")}
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
@ -81,6 +86,12 @@ class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
form_class = GuestForm form_class = GuestForm
template_name = "activity/activity_invite.html" 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): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ 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): def __init__(self, *args, **kwargs):
super().__init__(*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() 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): class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
@ -26,6 +28,8 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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() 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 import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User 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 .forms import ProfileForm
from .models import Club, Membership, Profile, Role from .models import Club, Membership, Profile
class ProfileInline(admin.StackedInline): class ProfileInline(admin.StackedInline):
@ -17,6 +20,7 @@ class ProfileInline(admin.StackedInline):
can_delete = False can_delete = False
@admin.register(User, site=admin_site)
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,) inlines = (ProfileInline,)
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
@ -32,11 +36,33 @@ class CustomUserAdmin(UserAdmin):
return super().get_inline_instances(request, obj) return super().get_inline_instances(request, obj)
# Update Django User with profile @admin.register(Club, site=admin_site)
admin.site.unregister(User) class ClubAdmin(admin.ModelAdmin):
admin.site.register(User, CustomUserAdmin) 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 def pretty_fee_paid(self, obj):
admin.site.register(Club) return pretty_money(obj.membership_fee_paid)
admin.site.register(Membership)
admin.site.register(Role) 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 rest_framework import serializers
from ..models import Profile, Club, Role, Membership from ..models import Profile, Club, Membership
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
@ -29,17 +29,6 @@ class ClubSerializer(serializers.ModelSerializer):
fields = '__all__' 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): class MembershipSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Memberships. REST API Serializer for Memberships.

View File

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

View File

@ -4,8 +4,8 @@
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
from ..models import Profile, Club, Role, Membership from ..models import Profile, Club, Membership
class ProfileViewSet(ReadProtectedModelViewSet): class ProfileViewSet(ReadProtectedModelViewSet):
@ -30,18 +30,6 @@ class ClubViewSet(ReadProtectedModelViewSet):
search_fields = ['$name', ] 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): class MembershipViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. 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.forms import AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _ 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 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): 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): class ProfileForm(forms.ModelForm):
""" """
A form for the extras field provided by the :model:`member.Profile` model. 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): 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: class Meta:
model = Club model = Club
fields = '__all__' fields = '__all__'
@ -56,8 +77,6 @@ class ClubForm(forms.ModelForm):
class MembershipForm(forms.ModelForm): class MembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
soge = forms.BooleanField( soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"), label=_("Inscription paid by Société Générale"),
required=False, required=False,
@ -96,7 +115,7 @@ class MembershipForm(forms.ModelForm):
class Meta: class Meta:
model = Membership model = Membership
fields = ('user', 'roles', 'date_start') fields = ('user', 'date_start')
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion. # 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 # 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 # et récupère les noms d'utilisateur valides
@ -112,3 +131,28 @@ class MembershipForm(forms.ModelForm):
), ),
'date_start': DatePickerInput(), '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 import hashlib
from django.conf import settings
from django.contrib.auth.hashers import PBKDF2PasswordHasher from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import constant_time_compare from django.utils.crypto import constant_time_compare
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
class CustomNK15Hasher(PBKDF2PasswordHasher): class CustomNK15Hasher(PBKDF2PasswordHasher):
@ -20,8 +22,37 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
""" """
algorithm = "custom_nk15" 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): 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: if '|' in encoded:
salt, db_hashed_pass = encoded.split('$')[2].split('|') salt, db_hashed_pass = encoded.split('$')[2].split('|')
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass) return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
return super().verify(password, encoded) 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,)) return reverse('user_detail', args=(self.pk,))
def send_email_validation_link(self): 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', message = loader.render_to_string('registration/mails/email_validation_email.html',
{ {
'user': self.user, 'user': self.user,
@ -247,24 +247,6 @@ class Club(models.Model):
return reverse_lazy('member:club_detail', args=(self.pk,)) 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): class Membership(models.Model):
""" """
Register the membership of a user to a club, including roles and membership duration. 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( roles = models.ManyToManyField(
Role, "permission.Role",
verbose_name=_("roles"), verbose_name=_("roles"),
) )
@ -302,6 +284,7 @@ class Membership(models.Model):
verbose_name=_('fee'), verbose_name=_('fee'),
) )
@property
def valid(self): def valid(self):
""" """
A membership is valid if today is between the start and the end date. 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(): 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) 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 created = not self.pk
if created: if created:
if Membership.objects.filter( if Membership.objects.filter(

View File

@ -131,3 +131,31 @@ class MembershipTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', ) fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
model = Membership 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/', 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>/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>/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/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"), 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 PIL import Image
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.db.models import Q from django.db.models import Q
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin 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 import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable from note.tables import HistoryTable, AliasTable
from note_kfet.middlewares import _set_current_user_and_ip
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm
from .models import Club, Membership, Role from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
@ -36,6 +40,8 @@ class CustomLoginView(LoginView):
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
def form_valid(self, form): 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 self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form) return super().form_valid(form)
@ -45,9 +51,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
Update the user information. Update the user information.
""" """
model = User model = User
fields = ['first_name', 'last_name', 'username', 'email'] form_class = UserForm
template_name = 'member/profile_update.html' template_name = 'member/profile_update.html'
context_object_name = 'user_object' context_object_name = 'user_object'
extra_context = {"title": _("Update Profile")}
profile_form = ProfileForm profile_form = ProfileForm
def get_context_data(self, **kwargs): 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.") form.fields['email'].help_text = _("This address must be valid.")
context['profile_form'] = self.profile_form(instance=context['user_object'].profile) context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
context['title'] = _("Update Profile")
return context return context
def form_valid(self, form): def form_valid(self, form):
@ -101,6 +108,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
if olduser.email != user.email: if olduser.email != user.email:
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent. # If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
user.profile.email_confirmed = False user.profile.email_confirmed = False
user.profile.save()
user.profile.send_email_validation_link() user.profile.send_email_validation_link()
return super().form_valid(form) return super().form_valid(form)
@ -117,6 +125,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = User model = User
context_object_name = "user_object" context_object_name = "user_object"
template_name = "member/profile_detail.html" template_name = "member/profile_detail.html"
extra_context = {"title": _("Profile detail")}
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
""" """
@ -129,7 +138,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
user = context['user_object'] user = context['user_object']
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\ 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")) .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-') history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) 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 model = User
table_class = UserTable table_class = UserTable
template_name = 'member/user_list.html' template_name = 'member/user_list.html'
extra_context = {"title": _("Search user")}
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
""" """
Filter the user list with the given pattern. 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: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
@ -175,13 +185,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return qs[:20] 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): class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
@ -190,6 +193,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = User model = User
template_name = 'member/profile_alias.html' template_name = 'member/profile_alias.html'
context_object_name = 'user_object' context_object_name = 'user_object'
extra_context = {"title": _("Note aliases")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -203,6 +207,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
Update profile picture of the user note. Update profile picture of the user note.
""" """
form_class = ImageForm form_class = ImageForm
extra_context = {"title": _("Update note picture")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -260,6 +265,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
""" """
model = Token model = Token
template_name = "member/manage_auth_tokens.html" template_name = "member/manage_auth_tokens.html"
extra_context = {"title": _("Manage auth token")}
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
@ -287,6 +293,7 @@ class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Club model = Club
form_class = ClubForm form_class = ClubForm
success_url = reverse_lazy('member:club_list') success_url = reverse_lazy('member:club_list')
extra_context = {"title": _("Create new club")}
def form_valid(self, form): def form_valid(self, form):
return super().form_valid(form) return super().form_valid(form)
@ -298,12 +305,13 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
model = Club model = Club
table_class = ClubTable table_class = ClubTable
extra_context = {"title": _("Search club")}
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
""" """
Filter the user list with the given pattern. Filter the user list with the given pattern.
""" """
qs = super().get_queryset().filter() qs = super().get_queryset().distinct()
if "search" in self.request.GET: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
@ -322,6 +330,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
model = Club model = Club
context_object_name = "club" context_object_name = "club"
extra_context = {"title": _("Club detail")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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): if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates() 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))\ club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\ .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 = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table context['history_list'] = history_table
@ -342,7 +355,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
membership_table = MembershipTable(data=club_member, prefix="membership-") 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 context['member_list'] = membership_table
# Check if the user has the right to create a membership, to display the button. # 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 model = Club
template_name = 'member/club_alias.html' template_name = 'member/club_alias.html'
context_object_name = 'club' context_object_name = 'club'
extra_context = {"title": _("Note aliases")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -382,6 +396,7 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
context_object_name = "club" context_object_name = "club"
form_class = ClubForm form_class = ClubForm
template_name = "member/club_form.html" template_name = "member/club_form.html"
extra_context = {"title": _("Update club")}
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs) qs = super().get_queryset(**kwargs)
@ -415,6 +430,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Membership model = Membership
form_class = MembershipForm form_class = MembershipForm
template_name = 'member/add_members.html' template_name = 'member/add_members.html'
extra_context = {"title": _("Add new member to the club")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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"))\ club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None) .get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid 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 the concerned club is the BDE, then we add the option that Société générale pays the membership.
if club.name != "BDE": if club.name != "BDE":
@ -444,7 +459,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
user = old_membership.user user = old_membership.user
form.fields['user'].initial = user form.fields['user'].initial = user
form.fields['user'].disabled = True 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['date_start'].initial = old_membership.date_end + timedelta(days=1)
form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \ form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \
else club.membership_fee_unpaid else club.membership_fee_unpaid
@ -560,7 +574,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
form.add_error('bank', _("This field is required.")) form.add_error('bank', _("This field is required."))
return self.form_invalid(form) return self.form_invalid(form)
SpecialTransaction.objects.create( transaction = SpecialTransaction(
source=credit_type, source=credit_type,
destination=user.note, destination=user.note,
quantity=1, quantity=1,
@ -571,9 +585,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
bank=bank, bank=bank,
valid=True, valid=True,
) )
transaction._force_save = True
transaction.save()
ret = super().form_valid(form) 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 # If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
# Kfet membership. # Kfet membership.
if soge: if soge:
@ -595,6 +616,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
date_start=old_membership.get().date_end + timedelta(days=1) date_start=old_membership.get().date_end + timedelta(days=1)
if old_membership.exists() else form.instance.date_start, if old_membership.exists() else form.instance.date_start,
) )
membership._force_save = True
membership._soge = True membership._soge = True
membership.save() membership.save()
membership.refresh_from_db() membership.refresh_from_db()
@ -615,8 +637,9 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
Manage the roles of a user in a club Manage the roles of a user in a club
""" """
model = Membership model = Membership
form_class = MembershipForm form_class = MembershipRolesForm
template_name = 'member/add_members.html' template_name = 'member/add_members.html'
extra_context = {"title": _("Manage roles of an user in the club")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -626,15 +649,61 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
# We don't create a full membership, we only update one field
form.fields['user'].disabled = True club = self.object.club
del form.fields['date_start'] form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
del form.fields['credit_type'] & (Q(for_club__isnull=True) | Q(for_club=club))).all()
del form.fields['credit_amount']
del form.fields['last_name']
del form.fields['first_name']
del form.fields['bank']
return form return form
def get_success_url(self): 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 django.utils.translation import gettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, \ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction, SpecialTransaction RecurrentTransaction, MembershipTransaction, SpecialTransaction
from .templatetags.pretty_money import pretty_money
class AliasInlines(admin.TabularInline): class AliasInlines(admin.TabularInline):
@ -19,7 +21,7 @@ class AliasInlines(admin.TabularInline):
model = Alias model = Alias
@admin.register(Note) @admin.register(Note, site=admin_site)
class NoteAdmin(PolymorphicParentModelAdmin): class NoteAdmin(PolymorphicParentModelAdmin):
""" """
Parent regrouping all note types as children Parent regrouping all note types as children
@ -36,13 +38,12 @@ class NoteAdmin(PolymorphicParentModelAdmin):
# Organize notes by registration date # Organize notes by registration date
date_hierarchy = 'created_at' date_hierarchy = 'created_at'
ordering = ['-created_at']
# Search by aliases # Search by aliases
search_fields = ['alias__name'] search_fields = ['alias__name']
@admin.register(NoteClub) @admin.register(NoteClub, site=admin_site)
class NoteClubAdmin(PolymorphicChildModelAdmin): class NoteClubAdmin(PolymorphicChildModelAdmin):
""" """
Child for a club note, see NoteAdmin Child for a club note, see NoteAdmin
@ -66,15 +67,27 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
return False return False
@admin.register(NoteSpecial) @admin.register(NoteSpecial, site=admin_site)
class NoteSpecialAdmin(PolymorphicChildModelAdmin): class NoteSpecialAdmin(PolymorphicChildModelAdmin):
""" """
Child for a special note, see NoteAdmin Child for a special note, see NoteAdmin
""" """
readonly_fields = ('balance',) 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): class NoteUserAdmin(PolymorphicChildModelAdmin):
""" """
Child for an user note, see NoteAdmin Child for an user note, see NoteAdmin
@ -97,16 +110,16 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
return False return False
@admin.register(Transaction) @admin.register(Transaction, site=admin_site)
class TransactionAdmin(PolymorphicParentModelAdmin): class TransactionAdmin(PolymorphicParentModelAdmin):
""" """
Admin customisation for Transaction Admin customisation for Transaction
""" """
child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction) child_models = (Transaction, RecurrentTransaction, MembershipTransaction, SpecialTransaction)
list_display = ('created_at', 'poly_source', 'poly_destination', list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'valid') 'quantity', 'amount', 'valid')
list_filter = ('valid',) list_filter = ('valid',)
autocomplete_fields = ( readonly_fields = (
'source', 'source',
'destination', 'destination',
) )
@ -138,27 +151,35 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
return [] return []
@admin.register(MembershipTransaction) @admin.register(MembershipTransaction, site=admin_site)
class MembershipTransactionAdmin(PolymorphicChildModelAdmin): class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
""" """
Admin customisation for MembershipTransaction 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): class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
""" """
Admin customisation for SpecialTransaction Admin customisation for SpecialTransaction
""" """
@admin.register(TransactionTemplate) @admin.register(TransactionTemplate, site=admin_site)
class TransactionTemplateAdmin(admin.ModelAdmin): class TransactionTemplateAdmin(admin.ModelAdmin):
""" """
Admin customisation for TransactionTemplate Admin customisation for TransactionTemplate
""" """
list_display = ('name', 'poly_destination', 'amount', 'category', 'display',) list_display = ('name', 'poly_destination', 'pretty_amount', 'category', 'display', 'highlighted',)
list_filter = ('category', 'display') list_filter = ('category', 'display', 'highlighted',)
search_fields = ('name', 'destination__club__name', 'amount',)
autocomplete_fields = ('destination',) autocomplete_fields = ('destination',)
def poly_destination(self, obj): def poly_destination(self, obj):
@ -169,11 +190,15 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
poly_destination.short_description = _('destination') 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): class TemplateCategoryAdmin(admin.ModelAdmin):
""" """
Admin customisation for TransactionTemplate Admin customisation for TransactionTemplate
""" """
list_display = ('name',) 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 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): if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
print(obj.pk)
return NotePolymorphicSerializer().to_representation(obj.note) 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): def get_email_confirmed(self, obj):
if isinstance(obj.note, NoteUser): if isinstance(obj.note, NoteUser):

View File

@ -109,7 +109,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = queryset.filter( queryset = queryset.filter(
Q(name__regex="^" + alias) Q(name__regex="^" + alias)
| Q(normalized_name__regex="^" + Alias.normalize(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 return queryset

View File

@ -62,6 +62,7 @@ class TransactionTemplate(models.Model):
category = models.ForeignKey( category = models.ForeignKey(
TemplateCategory, TemplateCategory,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='templates',
verbose_name=_('type'), verbose_name=_('type'),
max_length=31, max_length=31,
) )
@ -71,6 +72,11 @@ class TransactionTemplate(models.Model):
verbose_name=_("display"), verbose_name=_("display"),
) )
highlighted = models.BooleanField(
default=False,
verbose_name=_("highlighted"),
)
description = models.CharField( description = models.CharField(
verbose_name=_('description'), verbose_name=_('description'),
max_length=255, max_length=255,
@ -202,7 +208,9 @@ class Transaction(PolymorphicModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
# Save notes # Save notes
self.source._force_save = True
self.source.save() self.source.save()
self.destination._force_save = True
self.destination.save() self.destination.save()
def delete(self, **kwargs): 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.utils.html import format_html
from django_tables2.utils import A from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _ 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.notes import Alias
from .models.transactions import Transaction, TransactionTemplate from .models.transactions import Transaction, TransactionTemplate
@ -52,14 +54,26 @@ class HistoryTable(tables.Table):
attrs={ attrs={
"td": { "td": {
"id": lambda record: "validate_" + str(record.id), "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", "data-toggle": "tooltip",
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"), "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() + ')', 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_' "onmouseover": lambda record: '$("#invalidity_reason_'
+ str(record.id) + '").show();$("#invalidity_reason_' + str(record.id) + '").show();$("#invalidity_reason_'
+ str(record.id) + '").focus();', + str(record.id) + '").focus();'
"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,
"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 When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
""" """
val = "" if value else "" 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) \ val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
+ "' value='" + (html.escape(record.invalidity_reason) + "' value='" + (html.escape(record.invalidity_reason)
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \ if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
@ -131,12 +149,10 @@ class ButtonTable(tables.Table):
row_attrs = { row_attrs = {
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'), 'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
'id': lambda record: "row-" + str(record.pk), 'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: record.pk
} }
model = TransactionTemplate model = TransactionTemplate
exclude = ('id',) exclude = ('id',)
order_by = ('type', '-display', 'destination__name', 'name',)
edit = tables.LinkColumn('note:template_update', edit = tables.LinkColumn('note:template_update',
args=[A('pk')], args=[A('pk')],

View File

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

View File

@ -2,11 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-lateré # SPDX-License-Identifier: GPL-3.0-or-lateré
from django.contrib import admin 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): class PermissionMaskAdmin(admin.ModelAdmin):
""" """
Admin customisation for PermissionMask Admin customisation for PermissionMask
@ -14,17 +15,19 @@ class PermissionMaskAdmin(admin.ModelAdmin):
list_display = ('description', 'rank', ) list_display = ('description', 'rank', )
@admin.register(Permission) @admin.register(Permission, site=admin_site)
class PermissionAdmin(admin.ModelAdmin): class PermissionAdmin(admin.ModelAdmin):
""" """
Admin customisation for Permission 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) @admin.register(Role, site=admin_site)
class RolePermissionsAdmin(admin.ModelAdmin): 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 rest_framework import serializers
from ..models import Permission, RolePermissions from ..models import Permission, Role
class PermissionSerializer(serializers.ModelSerializer): class PermissionSerializer(serializers.ModelSerializer):
@ -17,12 +17,12 @@ class PermissionSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class RolePermissionsSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for RolePermissions types. REST API Serializer for Role types.
The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API. The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
""" """
class Meta: class Meta:
model = RolePermissions model = Role
fields = '__all__' fields = '__all__'

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import PermissionViewSet, RolePermissionsViewSet from .views import PermissionViewSet, RoleViewSet
def register_permission_urls(router, path): def register_permission_urls(router, path):
@ -9,4 +9,4 @@ def register_permission_urls(router, path):
Configure router for permission REST API. Configure router for permission REST API.
""" """
router.register(path + "/permission", PermissionViewSet) 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 django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer, RolePermissionsSerializer from .serializers import PermissionSerializer, RoleSerializer
from ..models import Permission, RolePermissions from ..models import Permission, Role
class PermissionViewSet(ReadOnlyProtectedModelViewSet): class PermissionViewSet(ReadOnlyProtectedModelViewSet):
@ -20,13 +20,13 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
filterset_fields = ['model', 'type', ] filterset_fields = ['model', 'type', ]
class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet): class RoleViewSet(ReadOnlyProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
then render it on /api/permission/roles/ then render it on /api/permission/roles/
""" """
queryset = RolePermissions.objects.all() queryset = Role.objects.all()
serializer_class = RolePermissionsSerializer serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filterset_fields = ['role', ] filterset_fields = ['role', ]

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -19,8 +19,8 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
'OPTIONS': [], 'OPTIONS': [],
'HEAD': [], 'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'], 'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'], 'PUT': [], # ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'], 'PATCH': [], # ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(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 # In the other case, we check if he/she has the right to change one field
previous = qs.get() previous = qs.get()
for field in instance._meta.fields: for field in instance._meta.fields:
field_name = field.name field_name = field.name
old_value = getattr(previous, 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: if instance._meta.label_lower in EXCLUDED:
return 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 return
user = get_current_authenticated_user() user = get_current_authenticated_user()

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django import template from django import template
@ -16,9 +17,9 @@ def not_empty_model_list(model_name):
""" """
user = get_current_authenticated_user() user = get_current_authenticated_user()
session = get_current_session() session = get_current_session()
if user is None: if user is None or isinstance(user, AnonymousUser):
return False 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 return True
qs = model_list(model_name) qs = model_list(model_name)
return qs.exists() return qs.exists()
@ -31,28 +32,38 @@ def not_empty_model_change_list(model_name):
""" """
user = get_current_authenticated_user() user = get_current_authenticated_user()
session = get_current_session() session = get_current_session()
if user is None: if user is None or isinstance(user, AnonymousUser):
return False 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 return True
qs = model_list(model_name, "change") qs = model_list(model_name, "change")
return qs.exists() return qs.exists()
@stringfilter @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. Return the queryset of all visible instances of the given model.
""" """
user = get_current_authenticated_user() user = get_current_authenticated_user()
if user is None:
return False
spl = model_name.split(".") spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) 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 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): def has_perm(perm, obj):
return PermissionBackend.check_perm(get_current_authenticated_user(), 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() user = get_current_authenticated_user()
session = get_current_session() session = get_current_session()
if user is None: if user is None or isinstance(user, AnonymousUser):
return False 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 return True
if session.get("can_create_transaction", None): if session.get("can_create_transaction", None):
return session.get("can_create_transaction", None) == 1 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_list', not_empty_model_list)
register.filter('not_empty_model_change_list', not_empty_model_change_list) register.filter('not_empty_model_change_list', not_empty_model_change_list)
register.filter('model_list', model_list) register.filter('model_list', model_list)
register.filter('model_list_length', model_list_length)
register.filter('has_perm', has_perm) register.filter('has_perm', has_perm)

View File

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

View File

@ -5,9 +5,10 @@ from datetime import date
from django.forms import HiddenInput from django.forms import HiddenInput
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView, TemplateView from django.views.generic import UpdateView, TemplateView
from member.models import Role, Membership from member.models import Membership
from .backends import PermissionBackend from .backends import PermissionBackend
from .models import Role
class ProtectQuerysetMixin: class ProtectQuerysetMixin:
@ -19,7 +20,7 @@ class ProtectQuerysetMixin:
""" """
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset(**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): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
@ -40,6 +41,7 @@ class ProtectQuerysetMixin:
class RightsView(TemplateView): class RightsView(TemplateView):
template_name = "permission/all_rights.html" template_name = "permission/all_rights.html"
extra_context = {"title": _("Rights")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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 # Truncate microseconds so that tokens are consistent even if the
# database doesn't support microseconds. # database doesn't support microseconds.
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None) 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() 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.views.generic.edit import FormMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from member.forms import ProfileForm 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.models import SpecialTransaction
from note.templatetags.pretty_money import pretty_money from note.templatetags.pretty_money import pretty_money
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from .forms import SignUpForm, ValidationForm from .forms import SignUpForm, ValidationForm
@ -34,6 +35,7 @@ class UserCreateView(CreateView):
form_class = SignUpForm form_class = SignUpForm
template_name = 'registration/signup.html' template_name = 'registration/signup.html'
second_form = ProfileForm second_form = ProfileForm
extra_context = {"title": _("Register new user")}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -77,6 +79,7 @@ class UserValidateView(TemplateView):
""" """
title = _("Email validation") title = _("Email validation")
template_name = 'registration/email_validation_complete.html' template_name = 'registration/email_validation_complete.html'
extra_context = {"title": _("Validate email")}
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
""" """
@ -90,15 +93,12 @@ class UserValidateView(TemplateView):
# Validate the token # Validate the token
if user is not None and email_validation_token.check_token(user, 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. # 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.is_active = user.profile.registration_valid or user.is_superuser
user.profile.email_confirmed = True user.profile.email_confirmed = True
user.save() user.save()
user.profile.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): def get_user(self, uidb64):
@ -132,7 +132,7 @@ class UserValidationEmailSentView(TemplateView):
Display the information that the validation link has been sent. Display the information that the validation link has been sent.
""" """
template_name = 'registration/email_validation_email_sent.html' 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): class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
@ -140,6 +140,7 @@ class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, De
Rensend the email validation link. Rensend the email validation link.
""" """
model = User model = User
extra_context = {"title": _("Resend email validation link")}
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
user = self.get_object() user = self.get_object()
@ -157,6 +158,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
model = User model = User
table_class = FutureUserTable table_class = FutureUserTable
template_name = 'registration/future_user_list.html' template_name = 'registration/future_user_list.html'
extra_context = {"title": _("Pre-registered users list")}
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
""" """
@ -164,7 +166,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
:param kwargs: :param kwargs:
:return: :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: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
@ -198,6 +200,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
form_class = ValidationForm form_class = ValidationForm
context_object_name = "user_object" context_object_name = "user_object"
template_name = "registration/future_profile_detail.html" template_name = "registration/future_profile_detail.html"
extra_context = {"title": _("Registration detail")}
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
@ -354,6 +357,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
""" """
Delete a pre-registered user. Delete a pre-registered user.
""" """
extra_context = {"title": _("Invalidate pre-registration")}
def get(self, request, *args, **kwargs): 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é # SPDX-License-Identifier: GPL-3.0-or-lateré
from django.contrib import admin from django.contrib import admin
from note_kfet.admin import admin_site
from .models import RemittanceType, Remittance, SogeCredit from .models import RemittanceType, Remittance, SogeCredit
@admin.register(RemittanceType) @admin.register(RemittanceType, site=admin_site)
class RemittanceTypeAdmin(admin.ModelAdmin): class RemittanceTypeAdmin(admin.ModelAdmin):
""" """
Admin customisation for RemiitanceType Admin customisation for RemiitanceType
@ -14,7 +15,7 @@ class RemittanceTypeAdmin(admin.ModelAdmin):
list_display = ('note', ) list_display = ('note', )
@admin.register(Remittance) @admin.register(Remittance, site=admin_site)
class RemittanceAdmin(admin.ModelAdmin): class RemittanceAdmin(admin.ModelAdmin):
""" """
Admin customisation for Remittance Admin customisation for Remittance
@ -27,4 +28,14 @@ class RemittanceAdmin(admin.ModelAdmin):
return not obj.closed and super().has_change_permission(request, obj) 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 import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import DatePickerInput, AmountInput from note_kfet.inputs import DatePickerInput, AmountInput
from permission.backends import PermissionBackend
from .models import Invoice, Product, Remittance, SpecialTransactionProxy from .models import Invoice, Product, Remittance, SpecialTransactionProxy
@ -132,8 +131,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
# Add submit button # Add submit button
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\ self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
def clean_last_name(self): def clean_last_name(self):
""" """

View File

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

View File

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

View File

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

View File

@ -8,8 +8,9 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ 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 note.models import MembershipTransaction
from permission.models import Role
class WEIClub(Club): class WEIClub(Club):
@ -113,6 +114,7 @@ class BusTeam(models.Model):
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
verbose_name=_("name"),
) )
color = models.PositiveIntegerField( # Use a color picker to get the hexa code color = models.PositiveIntegerField( # Use a color picker to get the hexa code
@ -188,6 +190,28 @@ class WEIRegistration(models.Model):
verbose_name=_("gender"), 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( health_issues = models.TextField(
blank=True, blank=True,
default="", default="",

View File

@ -103,7 +103,7 @@ class WEIMembershipTable(tables.Table):
team = tables.LinkColumn( team = tables.LinkColumn(
'wei:manage_bus_team', 'wei:manage_bus_team',
args=[A('bus.pk')], args=[A('team.pk')],
) )
def render_year(self, record): def render_year(self, record):
@ -144,10 +144,10 @@ class BusTable(tables.Table):
) )
def render_teams(self, value): 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): 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: class Meta:
attrs = { attrs = {
@ -178,7 +178,7 @@ class BusTeamTable(tables.Table):
) )
def render_count(self, value): 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( count = tables.Column(
verbose_name=_("Members count"), verbose_name=_("Members count"),

View File

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

View File

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

View File

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

View File

@ -3,13 +3,14 @@
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
from django.views.generic import RedirectView from django.views.generic import RedirectView
from member.views import CustomLoginView from member.views import CustomLoginView
from .admin import admin_site
urlpatterns = [ urlpatterns = [
# Dev so redirect to something random # Dev so redirect to something random
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'), path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
@ -25,7 +26,7 @@ urlpatterns = [
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
path('admin/doc/', include('django.contrib.admindocs.urls')), 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/login/', CustomLoginView.as_view()),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('api/', include('api.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/ Copyright JS Foundation and other contributors, https://js.foundation/
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
====
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the 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 "note": note_id
} }
).done(function(){ ).done(function(){
$("#alias_table").load(location.href+ " #alias_table"); $("#alias_table").load(location.pathname+ " #alias_table");
addMsg("Alias ajouté","success"); addMsg("Alias ajouté","success");
}) })
.fail(function(xhr, textStatus, error){ .fail(function(xhr, textStatus, error){
@ -29,7 +29,7 @@
}) })
.done(function(){ .done(function(){
addMsg('Alias supprimé','success'); addMsg('Alias supprimé','success');
$("#alias_table").load(location.href + " #alias_table"); $("#alias_table").load(location.pathname + " #alias_table");
}) })
.fail(function(xhr,textStatus, error){ .fail(function(xhr,textStatus, error){
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON);

View File

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

View File

@ -156,7 +156,7 @@ function reset() {
function consumeAll() { function consumeAll() {
notes_display.forEach(function(note_display) { notes_display.forEach(function(note_display) {
buttons.forEach(function(button) { 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); 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. * 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 source_alias The alias used for the source (type: str)
* @param dest The note that sold the item (type: int) * @param dest The note that sold the item (type: int)
* @param quantity The quantity sold (type: int) * @param quantity The quantity sold (type: int)
@ -184,12 +184,24 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
"valid": true, "valid": true,
"polymorphic_ctype": type, "polymorphic_ctype": type,
"resourcetype": "RecurrentTransaction", "resourcetype": "RecurrentTransaction",
"source": source, "source": source.id,
"source_alias": source_alias, "source_alias": source_alias,
"destination": dest, "destination": dest,
"category": category, "category": category,
"template": template "template": template
}, reset).fail(function (e) { })
.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/", $.post("/api/note/transaction/transaction/",
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, "csrfmiddlewaretoken": CSRF_TOKEN,
@ -207,10 +219,10 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
"template": template "template": template
}).done(function() { }).done(function() {
reset(); 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 () { }).fail(function () {
reset(); reset();
errMsg(e.responseJSON); errMsg(e.responseJSON, 10000);
}); });
}); });
} }

View File

@ -7,22 +7,38 @@ function refreshHistory() {
$("#history").load("/note/transfer/ #history"); $("#history").load("/note/transfer/ #history");
} }
function reset() { function reset(refresh=true) {
sources_notes_display.length = 0; sources_notes_display.length = 0;
sources.length = 0; sources.length = 0;
dests_notes_display.length = 0; dests_notes_display.length = 0;
dests.length = 0; dests.length = 0;
$("#source_note_list").html(""); $("#source_note_list").html("");
$("#dest_note_list").html(""); $("#dest_note_list").html("");
$("#amount").val(""); let source_field = $("#source_note");
$("#reason").val(""); 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(""); $("#last_name").val("");
$("#first_name").val(""); $("#first_name").val("");
$("#bank").val(""); $("#bank").val("");
$("#user_note").val(""); $("#user_note").val("");
$("#profile_pic").attr("src", "/media/pic/default.png"); $("#profile_pic").attr("src", "/media/pic/default.png");
if (refresh) {
refreshBalance(); refreshBalance();
refreshHistory(); refreshHistory();
}
} }
$(document).ready(function() { $(document).ready(function() {
@ -71,17 +87,10 @@ $(document).ready(function() {
let source = $("#source_note"); let source = $("#source_note");
let dest = $("#dest_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() { $("#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'); $("#special_transaction_div").addClass('d-none');
source.attr('disabled', false); source.attr('disabled', false);
$("#source_note_list").removeClass('d-none'); $("#source_note_list").removeClass('d-none');
@ -90,6 +99,9 @@ $(document).ready(function() {
}); });
$("#type_credit").click(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'); $("#special_transaction_div").removeClass('d-none');
$("#source_note_list").addClass('d-none'); $("#source_note_list").addClass('d-none');
$("#dest_note_list").removeClass('d-none'); $("#dest_note_list").removeClass('d-none');
@ -107,6 +119,9 @@ $(document).ready(function() {
}); });
$("#type_debit").click(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'); $("#special_transaction_div").removeClass('d-none');
$("#source_note_list").removeClass('d-none'); $("#source_note_list").removeClass('d-none');
$("#dest_note_list").addClass('d-none'); $("#dest_note_list").addClass('d-none');
@ -131,13 +146,11 @@ $(document).ready(function() {
dest.val(type); dest.val(type);
}); });
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading. // Ensure we begin in transfer mode. Removing these lines may cause problems when reloading.
let type_gift = $("#type_gift"); // Default mode let type_transfer = $("#type_transfer"); // Default mode
type_gift.removeAttr('checked'); type_transfer.removeAttr('checked');
$("#type_transfer").removeAttr('checked');
$("#type_credit").removeAttr('checked'); $("#type_credit").removeAttr('checked');
$("#type_debit").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_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").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'); $("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
@ -145,90 +158,120 @@ $(document).ready(function() {
if (location.hash) if (location.hash)
$("#type_" + location.hash.substr(1)).click(); $("#type_" + location.hash.substr(1)).click();
else else
type_gift.click(); type_transfer.click();
location.hash = ""; 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() { $("#btn_transfer").click(function() {
if ($("#type_gift").is(':checked')) { let error = false;
dests_notes_display.forEach(function (dest) {
let amount_field = $("#amount");
amount_field.removeClass('is-invalid');
$("#amount-required").html("");
let reason_field = $("#reason");
reason_field.removeClass('is-invalid');
$("#reason-required").html("");
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;
}
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/", $.post("/api/note/transaction/transaction/",
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, "csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": dest.quantity, "quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(), "amount": amount,
"reason": $("#reason").val(), "reason": reason,
"valid": true, "valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction", "resourcetype": "Transaction",
"source": user_id, "source": source.note.id,
"source_alias": source.name,
"destination": dest.note.id, "destination": dest.note.id,
"destination_alias": dest.name "destination_alias": dest.name
}).done(function () { }).done(function () {
if (!isNaN(source.note.balance)) {
let newBalance = source.note.balance - source.quantity * dest.quantity * amount;
if (newBalance <= -5000) {
addMsg("Le transfert de " addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note " + pretty_money(source.quantity * dest.quantity * amount) + " de la note "
+ " vers la note " + dest.name + " a été fait avec succès !", "success"); + 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 * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success", 10000);
reset(); reset();
}).fail(function () { // do it again but valid = false }).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");
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");
reset();
});
});
});
}
else if ($("#type_transfer").is(':checked')) {
sources_notes_display.forEach(function (source) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/", $.post("/api/note/transaction/transaction/",
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, "csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity, "quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(), "amount": amount,
"reason": $("#reason").val(), "reason": reason,
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": source.note.id,
"source_alias": source.name,
"destination": dest.note.id,
"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 été fait avec succès !", "success");
reset();
}).fail(function (err) { // 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(),
"valid": false, "valid": false,
"invalidity_reason": "Solde insuffisant", "invalidity_reason": "Solde insuffisant",
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
@ -239,16 +282,12 @@ $("#btn_transfer").click(function() {
"destination_alias": dest.name "destination_alias": dest.name
}).done(function () { }).done(function () {
addMsg("Le transfert de " 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é : Solde insuffisant", "danger"); + " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000);
reset();
}).fail(function (err) { }).fail(function (err) {
addMsg("Le transfert de " 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"); + " 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')) { } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val(); let special_note = $("#credit_type").val();
let user_note; let user_note;
let given_reason = $("#reason").val(); let given_reason = reason;
let source, dest, reason; let source_id, dest_id;
if ($("#type_credit").is(':checked')) { if ($("#type_credit").is(':checked')) {
if (!dests_notes_display.length) {
$("#dest_note").addClass('is-invalid');
return;
}
user_note = dests_notes_display[0].note.id; user_note = dests_notes_display[0].note.id;
source = special_note; source_id = special_note;
dest = user_note; dest_id = user_note;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase(); reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0) if (given_reason.length > 0)
reason += " (" + given_reason + ")"; reason += " (" + given_reason + ")";
} }
else { else {
if (!sources_notes_display.length) {
$("#source_note").addClass('is-invalid');
return;
}
user_note = sources_notes_display[0].note.id; user_note = sources_notes_display[0].note.id;
source = user_note; source_id = user_note;
dest = special_note; dest_id = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase(); reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0) if (given_reason.length > 0)
reason += " (" + given_reason + ")"; reason += " (" + given_reason + ")";
@ -278,24 +327,24 @@ $("#btn_transfer").click(function() {
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, "csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1, "quantity": 1,
"amount": 100 * $("#amount").val(), "amount": amount,
"reason": reason, "reason": reason,
"valid": true, "valid": true,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction", "resourcetype": "SpecialTransaction",
"source": source, "source": source_id,
"source_alias": source.name, "source_alias": sources_notes_display[0].name,
"destination": dest, "destination": dest_id,
"destination_alias": dest.name, "destination_alias": dests_notes_display[0].name,
"last_name": $("#last_name").val(), "last_name": $("#last_name").val(),
"first_name": $("#first_name").val(), "first_name": $("#first_name").val(),
"bank": $("#bank").val() "bank": $("#bank").val()
}).done(function () { }).done(function () {
addMsg("Le crédit/retrait a bien été effectué !", "success"); addMsg("Le crédit/retrait a bien été effectué !", "success", 10000);
reset(); reset();
}).fail(function (err) { }).fail(function (err) {
addMsg("Le crédit/retrait a échoué : " + err.responseText, "danger"); addMsg("Le crédit/retrait a échoué : " + JSON.parse(err.responseText)["detail"],
reset(); "danger", 10000);
}); });
} }
}); });

View File

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

View File

@ -51,7 +51,7 @@
if ((pattern === old_pattern || pattern === "") && !force) if ((pattern === old_pattern || pattern === "") && !force)
return; 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(); refreshBalance();
} }

View File

@ -77,6 +77,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block extracss %}{% endblock %} {% block extracss %}{% endblock %}
</head> </head>
<body class="d-flex w-100 h-100 flex-column"> <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"> <main class="mb-auto">
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm"> <nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
<a class="navbar-brand" href="/">{{ request.site.name }}</a> <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> <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fas fa-exchange-alt"></i>{% trans 'Transfer' %} </a>
</li> </li>
{% endif %} {% endif %}
{% if "auth.user"|model_list|length >= 2 %} {% if "auth.user"|model_list_length >= 2 %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a> <a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a>
</li> </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> <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fas fa-users"></i> {% trans 'Clubs' %}</a>
</li> </li>
{% endif %} {% 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 %} {% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fas fa-calendar"></i> {% trans 'Activities' %}</a> <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> <a class="nav-link" href="{% url 'wei:current_wei_detail' %}"><i class="fas fa-bus"></i> {% trans 'WEI' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if user.is_authenticated %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a> <a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a>
</li> </li>
{% if user.is_staff %} {% endif %}
{% if user.is_staff and ""|has_perm:user %}
<li class="nav-item active"> <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 data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Admin' %}</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -178,6 +174,17 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} {% 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> <div id="messages"></div>
{% block content %} {% block content %}
<p>Default content...</p> <p>Default content...</p>

View File

@ -11,7 +11,7 @@
<div class="card-body" id="profile_infos"> <div class="card-body" id="profile_infos">
<dl class="row"> <dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt> <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 %} {% 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> <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 %} {% block content %}
<div class="row justify-content-center mb-4"> <div class="row justify-content-center mb-4">
<div class="col-md-10 text-center"> <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"/> <input class="form-control mx-auto w-25" type="text" id="search_field"/>
<hr> <hr>
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a> <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
@ -36,7 +33,7 @@
function reloadTable() { function reloadTable() {
let pattern = searchbar_obj.val(); 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() { 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 render_table from django_tables2 %}
{% load i18n %} {% 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 %} {% if member_list.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold"> <a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Member of the Club" %} <i class="fa fa-users"></i> {% trans "Club members" %}
</a> </a>
</div> </div>
{% render_table member_list %} {% render_table member_list %}
@ -16,7 +29,7 @@
{% if history_list.data %} {% if history_list.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="historyListHeading"> <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" %} <i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a> </a>
</div> </div>

View File

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

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% load perms %}
{% block content %} {% block content %}
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ..."> <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
@ -18,6 +19,13 @@
{% endif %} {% endif %}
</div> </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 %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
@ -34,7 +42,7 @@
if (pattern === old_pattern || pattern === "") if (pattern === old_pattern || pattern === "")
return; 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() { searchbar_obj.keyup(function() {

View File

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

View File

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

View File

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

View File

@ -5,18 +5,7 @@
{% block content %} {% block content %}
<div class="row justify-content-center mb-4"> <div class="row justify-content-center mb-4">
<div class="col-md-10 text-center"> <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..." %}"> <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> <hr>
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a> <a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a>
</div> </div>
@ -36,66 +25,39 @@
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script> <script type="text/javascript">
/* fonction appelée à la fin du timer */ $(document).ready(function() {
function getInfo() { let searchbar_obj = $("#search_field");
var asked = $("#search_field").val(); var timer_on = false;
/* on ne fait la requête que si on a au moins un caractère pour chercher */ var timer;
if (asked.length >= 1) {
$.getJSON("/api/note/transaction/template/?format=json&search=" + asked + ($("#active_only").is(":checked") ? "&display=true" : ""), function(buttons) { function reloadTable() {
console.log(buttons); let pattern = searchbar_obj.val();
let selected_id = buttons.results.map((a => "#row-" + a.id)); $("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table");
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');
}
}
} }
var timer; searchbar_obj.keyup(function() {
var timer_on; if (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); clearTimeout(timer);
timer = setTimeout(getInfo, 300);
}
else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
timer = setTimeout(getInfo, 300);
timer_on = true; timer_on = true;
} setTimeout(reloadTable, 0);
} });
});
// on click of button "delete" , call the API // on click of button "delete" , call the API
function delete_button(button_id) { function delete_button(button_id) {
$.ajax({ $.ajax({
url:"/api/note/transaction/template/"+button_id+"/", url:"/api/note/transaction/template/" + button_id + "/",
method:"DELETE", method:"DELETE",
headers: {"X-CSRFTOKEN": CSRF_TOKEN} headers: {"X-CSRFTOKEN": CSRF_TOKEN}
}) })
.done(function(){ .done(function() {
addMsg('{% trans "button successfully deleted "%}','success'); 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(){ .fail(function() {
addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' ) 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> </script>
{% endblock %} {% endblock %}

View File

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

View File

@ -32,7 +32,7 @@
if (pattern === old_pattern || pattern === "") if (pattern === old_pattern || pattern === "")
return; 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() { $(".table-row").click(function() {
window.document.location = $(this).data("href"); window.document.location = $(this).data("href");

View File

@ -10,12 +10,22 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% block content %} {% block content %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p class="errornote"> <p class="errornote">
{% blocktrans trimmed %} {% blocktrans trimmed with username=request.user.username %}
You are authenticated as {{ username }}, but are not authorized to 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 %} {% endblocktrans %}
</p> </p>
{% endif %} {% 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 action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
{{ form | crispy }} {{ form | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary"> <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">

View File

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

View File

@ -22,7 +22,7 @@
{% if teams.data %} {% if teams.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <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" %} <i class="fa fa-bus"></i> {% trans "Teams" %}
</a> </a>
</div> </div>
@ -35,7 +35,7 @@
{% if memberships.data %} {% if memberships.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <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" %} <i class="fa fa-bus"></i> {% trans "Members" %}
</a> </a>
</div> </div>

View File

@ -39,7 +39,7 @@
{% if memberships.data or True %} {% if memberships.data or True %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <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" %} <i class="fa fa-bus"></i> {% trans "Teams" %}
</a> </a>
</div> </div>

View File

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

View File

@ -75,7 +75,7 @@
{% if buses.data %} {% if buses.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <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" %} <i class="fa fa-bus"></i> {% trans "Buses" %}
</a> </a>
</div> </div>
@ -88,7 +88,7 @@
{% if member_list.data %} {% if member_list.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <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" %} <i class="fa fa-users"></i> {% trans "Members of the WEI" %}
</a> </a>
</div> </div>
@ -101,7 +101,7 @@
{% if history_list.data %} {% if history_list.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="historyListHeading"> <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" %} <i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a> </a>
</div> </div>
@ -116,7 +116,7 @@
{% if pre_registrations.data %} {% if pre_registrations.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="historyListHeading"> <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" %} <i class="fa fa-user-plus"></i> {% trans "Unvalidated registrations" %}
</a> </a>
</div> </div>

View File

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

View File

@ -59,6 +59,12 @@
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.gender }}</dd> <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> <dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.birth_date }}</dd> <dd class="col-xl-6">{{ registration.birth_date }}</dd>

View File

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