diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e8bd9b44..80d85791 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,15 +40,7 @@ linters: stage: quality-assurance image: debian:buster-backports before_script: - - > - apt-get update && - apt-get install --no-install-recommends -t buster-backports -y - python3-django python3-django-crispy-forms - python3-django-extensions python3-django-filters python3-django-polymorphic - python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil - python3-babel python3-lockfile python3-pip python3-phonenumbers - python3-bs4 python3-setuptools tox - texlive-latex-extra texlive-lang-french lmodern texlive-fonts-recommended + - apt-get update && apt-get install -y tox script: tox -e linters # Be nice to new contributors, but please use `tox` diff --git a/README.md b/README.md index fb3cad6e..43c5a8a5 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,82 @@ [![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/master) [![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master) -## Installation sur un serveur +## Table des matières -On supposera pour la suite que vous utilisez une installation de Debian Buster ou Ubuntu 20.04 fraîche ou bien configuré. + - [Installation d'une instance de développement](#installation-dune-instance-de-développement) + - [Installation d'une instance de production](#installation-dune-instance-de-production) + +## Installation d'une instance de développement + +L'instance de développement installe la majorité des dépendances dans un environnement Python isolé. +Bien que cela permette de créer une instance sur toutes les distributions, +**cela veut dire que vos dépendances ne seront pas mises à jour automatiquement.** + +1. **Installation des dépendances de la distribution.** + Il y a quelques dépendances qui ne sont pas trouvable dans PyPI. + On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre. + + ```bash + $ sudo apt update + $ sudo apt install --no-install-recommends -y \ + ipython3 python3-setuptools python3-venv python3-dev \ + texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended \ + gettext libjs-bootstrap4 fonts-font-awesome git + ``` + +2. **Clonage du dépot** là où vous voulez : + + ```bash + $ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20 + ``` + +3. **Création d'un environment de travail Python décorrélé du système.** + On n'utilise pas `--system-site-packages` ici pour ne pas avoir des clashs de versions de modules avec le système. + + ```bash + $ python3 -m venv env + $ source env/bin/activate # entrer dans l'environnement + (env)$ pip3 install -r requirements.txt + (env)$ deactivate # sortir de l'environnement + ``` + +4. **Variable d'environnement.** + Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour + ce qu'il faut. + +5. **Migrations et chargement des données initiales.** + Pour initialiser la base de données avec de quoi travailler. + + ```bash + (env)$ ./manage.py collectstatic --noinput + (env)$ ./manage.py compilemessages + (env)$ ./manage.py makemigrations + (env)$ ./manage.py migrate + (env)$ ./manage.py loaddata initial + (env)$ ./manage.py createsuperuser # Création d'un utilisateur initial + ``` + +6. Enjoy : + + ```bash + (env)$ ./manage.py runserver 0.0.0.0:8000 + ``` + +En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django +accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu +de la note sur un téléphone ! + +## Installation d'une instance de production + +**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.** +Cela permet de mettre à jour facilement les dépendances critiques telles que Django. + +L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**. Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant. -Sinon vous pouvez suivre les étapes ici. +Sinon vous pouvez suivre les étapes décrites ci-dessous. -### Installation avec Debian/Ubuntu - -0. **Activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports. +0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports. ```bash $ echo "deb http://deb.debian.org/debian buster-backports main" | sudo tee /etc/apt/sources.list.d/deb_debian_org_debian.list @@ -192,43 +258,6 @@ nk20: - "traefik.http.services.nk20.loadbalancer.server.port=8080" ``` -### Lancer un serveur de développement - -Avec `./manage.py runserver` il est très rapide de mettre en place -un serveur de développement par exemple sur son ordinateur. - -1. Cloner le dépôt là où vous voulez : - - $ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20 - -2. Créer un environnement Python isolé - pour ne pas interférer avec les versions de paquets systèmes : - - $ python3 -m venv venv - $ source venv/bin/activate - (env)$ pip install -r requirements.txt - -3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour - ce qu'il faut - -4. Migrations et chargement des données initiales : - - (env)$ ./manage.py makemigrations - (env)$ ./manage.py migrate - (env)$ ./manage.py loaddata initial - -5. Créer un super-utilisateur : - - (env)$ ./manage.py createsuperuser - -6. Enjoy : - - (env)$ ./manage.py runserver 0.0.0.0:8000 - -En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django -accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu -de la note sur un téléphone ! - ## Documentation Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC). diff --git a/apps/activity/models.py b/apps/activity/models.py index 131cd725..0aefaf59 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -130,7 +130,7 @@ class Activity(models.Model): raise ValidationError(_("The end date must be after the start date.")) ret = super().save(*args, **kwargs) - if settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS: + if not settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS: def refresh_activities(): from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name) diff --git a/apps/activity/templates/activity/activity_entry.html b/apps/activity/templates/activity/activity_entry.html index d59a4c48..d778490f 100644 --- a/apps/activity/templates/activity/activity_entry.html +++ b/apps/activity/templates/activity/activity_entry.html @@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later

{{ title }}

-
+
{% trans "Transfer" %} diff --git a/apps/activity/tests/test_activities.py b/apps/activity/tests/test_activities.py new file mode 100644 index 00000000..db83fb0e --- /dev/null +++ b/apps/activity/tests/test_activities.py @@ -0,0 +1,176 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from datetime import timedelta + +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone + +from activity.models import Activity, ActivityType, Guest, Entry +from member.models import Club + + +class TestActivities(TestCase): + """ + Test activities + """ + fixtures = ('initial',) + + def setUp(self): + self.user = User.objects.create_superuser( + username="admintoto", + password="tototototo", + email="toto@example.com" + ) + self.client.force_login(self.user) + + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + self.activity = Activity.objects.create( + name="Activity", + description="This is a test activity\non two very very long lines\nbecause this is very important.", + location="Earth", + activity_type=ActivityType.objects.get(name="Pot"), + creater=self.user, + organizer=Club.objects.get(name="Kfet"), + attendees_club=Club.objects.get(name="Kfet"), + date_start=timezone.now(), + date_end=timezone.now() + timedelta(days=2), + valid=True, + ) + + self.guest = Guest.objects.create( + activity=self.activity, + inviter=self.user.note, + last_name="GUEST", + first_name="Guest", + ) + + def test_activity_list(self): + """ + Display the list of all activities + """ + response = self.client.get(reverse("activity:activity_list")) + self.assertEqual(response.status_code, 200) + + def test_activity_create(self): + """ + Create a new activity + """ + response = self.client.get(reverse("activity:activity_create")) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("activity:activity_create"), data=dict( + name="Activity created", + description="This activity was successfully created.", + location="Earth", + activity_type=ActivityType.objects.get(name="Soirée de club").id, + creater=self.user.id, + organizer=Club.objects.get(name="Kfet").id, + attendees_club=Club.objects.get(name="Kfet").id, + date_start="{:%Y-%m-%d %H:%M}".format(timezone.now()), + date_end="{:%Y-%m-%d %H:%M}".format(timezone.now() + timedelta(days=2)), + valid=True, + )) + self.assertTrue(Activity.objects.filter(name="Activity created").exists()) + activity = Activity.objects.get(name="Activity created") + self.assertRedirects(response, reverse("activity:activity_detail", args=(activity.pk,)), 302, 200) + + def test_activity_detail(self): + """ + Display the detail of an activity + """ + response = self.client.get(reverse("activity:activity_detail", args=(self.activity.pk,))) + self.assertEqual(response.status_code, 200) + + def test_activity_update(self): + """ + Update an activity + """ + response = self.client.get(reverse("activity:activity_update", args=(self.activity.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("activity:activity_update", args=(self.activity.pk,)), data=dict( + name=str(self.activity) + " updated", + description="This activity was successfully updated.", + location="Earth", + activity_type=ActivityType.objects.get(name="Autre").id, + creater=self.user.id, + organizer=Club.objects.get(name="Kfet").id, + attendees_club=Club.objects.get(name="Kfet").id, + date_start="{:%Y-%m-%d %H:%M}".format(timezone.now()), + date_end="{:%Y-%m-%d %H:%M}".format(timezone.now() + timedelta(days=2)), + valid=True, + )) + self.assertTrue(Activity.objects.filter(name="Activity updated").exists()) + self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200) + + def test_activity_entry(self): + """ + Create some entries + """ + self.activity.open = True + self.activity.save() + + response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,))) + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)) + "?search=guest") + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)) + "?search=admin") + self.assertEqual(response.status_code, 200) + + # User entry + response = self.client.post("/api/activity/entry/", data=dict( + activity=self.activity.id, + note=self.user.note.id, + guest="", + )) + self.assertEqual(response.status_code, 201) # 201 = Created + self.assertTrue(Entry.objects.filter(note=self.user.note, guest=None, activity=self.activity).exists()) + + # Guest entry + response = self.client.post("/api/activity/entry/", data=dict( + activity=self.activity.id, + note=self.user.note.id, + guest=self.guest.id, + )) + self.assertEqual(response.status_code, 201) # 201 = Created + self.assertTrue(Entry.objects.filter(note=self.user.note, guest=self.guest.id, activity=self.activity).exists()) + + def test_activity_invite(self): + """ + Try to invite people to an activity + """ + response = self.client.get(reverse("activity:activity_invite", args=(self.activity.pk,))) + self.assertEqual(response.status_code, 200) + + # The activity is started, can't invite + response = self.client.post(reverse("activity:activity_invite", args=(self.activity.pk,)), data=dict( + activity=self.activity.id, + inviter=self.user.note.id, + last_name="GUEST2", + first_name="Guest", + )) + self.assertEqual(response.status_code, 200) + + self.activity.date_start += timedelta(days=1) + self.activity.save() + + response = self.client.post(reverse("activity:activity_invite", args=(self.activity.pk,)), data=dict( + activity=self.activity.id, + inviter=self.user.note.id, + last_name="GUEST2", + first_name="Guest", + )) + self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200) + + def test_activity_ics(self): + """ + Render the ICS calendar + """ + response = self.client.get(reverse("activity:calendar_ics")) + self.assertEqual(response.status_code, 200) diff --git a/apps/activity/urls.py b/apps/activity/urls.py index f074e8f7..155229d4 100644 --- a/apps/activity/urls.py +++ b/apps/activity/urls.py @@ -14,4 +14,5 @@ urlpatterns = [ path('/entry/', views.ActivityEntryView.as_view(), name='activity_entry'), path('/update/', views.ActivityUpdateView.as_view(), name='activity_update'), path('new/', views.ActivityCreateView.as_view(), name='activity_create'), + path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'), ] diff --git a/apps/activity/views.py b/apps/activity/views.py index fd218db5..7de31b0c 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -1,14 +1,18 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from hashlib import md5 + from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.db.models import F, Q +from django.http import HttpResponse from django.urls import reverse_lazy from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from django.views import View from django.views.generic import DetailView, TemplateView, UpdateView from django_tables2.views import SingleTableView from note.models import Alias, NoteSpecial, NoteUser @@ -190,10 +194,10 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): if pattern[0] != "^": pattern = "^" + pattern guest_qs = guest_qs.filter( - Q(first_name__regex=pattern) - | Q(last_name__regex=pattern) - | Q(inviter__alias__name__regex=pattern) - | Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern)) + Q(first_name__iregex=pattern) + | Q(last_name__iregex=pattern) + | Q(inviter__alias__name__iregex=pattern) + | Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern)) ) else: guest_qs = guest_qs.none() @@ -226,21 +230,19 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): if "search" in self.request.GET and self.request.GET["search"]: pattern = self.request.GET["search"] note_qs = note_qs.filter( - Q(note__noteuser__user__first_name__regex=pattern) - | Q(note__noteuser__user__last_name__regex=pattern) - | Q(name__regex=pattern) - | Q(normalized_name__regex=Alias.normalize(pattern)) + Q(note__noteuser__user__first_name__iregex=pattern) + | Q(note__noteuser__user__last_name__iregex=pattern) + | Q(name__iregex=pattern) + | Q(normalized_name__iregex=Alias.normalize(pattern)) ) else: note_qs = note_qs.none() - if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql': - note_qs = note_qs.distinct('note__pk')[:20] - else: - # SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only - # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page. - # In production mode, please use PostgreSQL. - note_qs = note_qs.distinct()[:20] + # SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only + # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page. + # In production mode, please use PostgreSQL. + note_qs = note_qs.distinct('note__pk')[:20]\ + if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20] return note_qs def get_context_data(self, **kwargs): @@ -281,3 +283,60 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): Entry(activity=a, note=self.request.user.note,))] return context + + +class CalendarView(View): + """ + Render an ICS calendar with all valid activities. + """ + + def multilines(self, string, maxlength, offset=0): + newstring = string[:maxlength - offset] + string = string[maxlength - offset:] + while string: + newstring += "\r\n " + newstring += string[:maxlength - 1] + string = string[maxlength - 1:] + return newstring + + def get(self, request, *args, **kwargs): + ics = """BEGIN:VCALENDAR +VERSION: 2.0 +PRODID:Note Kfet 2020 +X-WR-CALNAME:Kfet Calendar +NAME:Kfet Calendar +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +""" + for activity in Activity.objects.filter(valid=True).order_by("-date_start").all(): + ics += f"""BEGIN:VEVENT +DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z +UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()} +SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)} +DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)} +DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)} +LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"} +DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """ + -- {activity.organizer.name} +END:VEVENT +""" + ics += "END:VCALENDAR" + ics = ics.replace("\r", "").replace("\n", "\r\n") + return HttpResponse(ics, content_type="text/calendar; charset=UTF-8") diff --git a/apps/api/serializers.py b/apps/api/serializers.py new file mode 100644 index 00000000..d59bdc43 --- /dev/null +++ b/apps/api/serializers.py @@ -0,0 +1,33 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + + +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User +from rest_framework.serializers import ModelSerializer + + +class UserSerializer(ModelSerializer): + """ + REST API Serializer for Users. + The djangorestframework plugin will analyse the model `User` and parse all fields in the API. + """ + + class Meta: + model = User + exclude = ( + 'password', + 'groups', + 'user_permissions', + ) + + +class ContentTypeSerializer(ModelSerializer): + """ + REST API Serializer for Users. + The djangorestframework plugin will analyse the model `User` and parse all fields in the API. + """ + + class Meta: + model = ContentType + fields = '__all__' diff --git a/apps/api/urls.py b/apps/api/urls.py index 9b4d44de..7131c657 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,103 +3,9 @@ from django.conf import settings from django.conf.urls import url, include -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.db.models import Q -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import routers, serializers -from rest_framework.viewsets import ReadOnlyModelViewSet -from api.viewsets import ReadProtectedModelViewSet -from note.models import Alias - - -class UserSerializer(serializers.ModelSerializer): - """ - REST API Serializer for Users. - The djangorestframework plugin will analyse the model `User` and parse all fields in the API. - """ - - class Meta: - model = User - exclude = ( - 'password', - 'groups', - 'user_permissions', - ) - - -class ContentTypeSerializer(serializers.ModelSerializer): - """ - REST API Serializer for Users. - The djangorestframework plugin will analyse the model `User` and parse all fields in the API. - """ - - class Meta: - model = ContentType - fields = '__all__' - - -class UserViewSet(ReadProtectedModelViewSet): - """ - REST API View set. - The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, - then render it on /api/users/ - """ - queryset = User.objects.all() - serializer_class = UserSerializer - filter_backends = [DjangoFilterBackend] - filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] - - def get_queryset(self): - queryset = super().get_queryset().order_by("username") - - if "search" in self.request.GET: - pattern = self.request.GET["search"] - - # We match first a user by its username, then if an alias is matched without normalization - # And finally if the normalized pattern matches a normalized alias. - queryset = queryset.filter( - username__iregex="^" + pattern - ).union( - queryset.filter( - Q(note__alias__name__iregex="^" + pattern) - & ~Q(username__iregex="^" + pattern) - ), all=True).union( - queryset.filter( - Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) - & ~Q(note__alias__name__iregex="^" + pattern) - & ~Q(username__iregex="^" + pattern) - ), - all=True).union( - queryset.filter( - Q(note__alias__normalized_name__iregex="^" + pattern.lower()) - & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) - & ~Q(note__alias__name__iregex="^" + pattern) - & ~Q(username__iregex="^" + pattern) - ), - all=True).union( - queryset.filter( - (Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern)) - & ~Q(note__alias__normalized_name__iregex="^" + pattern.lower()) - & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) - & ~Q(note__alias__name__iregex="^" + pattern) - & ~Q(username__iregex="^" + pattern) - ), - all=True) - - return queryset - - -# This ViewSet is the only one that is accessible from all authenticated users! -class ContentTypeViewSet(ReadOnlyModelViewSet): - """ - REST API View set. - The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, - then render it on /api/users/ - """ - queryset = ContentType.objects.all() - serializer_class = ContentTypeSerializer +from rest_framework import routers +from .viewsets import ContentTypeViewSet, UserViewSet # Routers provide an easy way of automatically determining the URL conf. # Register each app API router and user viewset diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py index 01fc7998..fa2fc941 100644 --- a/apps/api/viewsets.py +++ b/apps/api/viewsets.py @@ -2,12 +2,19 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib.contenttypes.models import ContentType +from django_filters.rest_framework import DjangoFilterBackend +from django.db.models import Q +from django.conf import settings +from django.contrib.auth.models import User +from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet from permission.backends import PermissionBackend -from rest_framework import viewsets from note_kfet.middlewares import get_current_session +from note.models import Alias + +from .serializers import UserSerializer, ContentTypeSerializer -class ReadProtectedModelViewSet(viewsets.ModelViewSet): +class ReadProtectedModelViewSet(ModelViewSet): """ Protect a ModelViewSet by filtering the objects that the user cannot see. """ @@ -19,10 +26,10 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet): def get_queryset(self): user = self.request.user get_current_session().setdefault("permission_mask", 42) - return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct() + return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct() -class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): +class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet): """ Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see. """ @@ -34,4 +41,72 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): user = self.request.user get_current_session().setdefault("permission_mask", 42) - return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct() + return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct() + + +class UserViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, + then render it on /api/users/ + """ + queryset = User.objects.all() + serializer_class = UserSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] + + def get_queryset(self): + queryset = super().get_queryset() + # Sqlite doesn't support ORDER BY in subqueries + queryset = queryset.order_by("username") \ + if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset + + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + # Filter with different rules + # We use union-all to keep each filter rule sorted in result + queryset = queryset.filter( + # Match without normalization + note__alias__name__iregex="^" + pattern + ).union( + queryset.filter( + # Match with normalization + Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + & ~Q(note__alias__name__iregex="^" + pattern) + ), + all=True, + ).union( + queryset.filter( + # Match on lower pattern + Q(note__alias__normalized_name__iregex="^" + pattern.lower()) + & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + & ~Q(note__alias__name__iregex="^" + pattern) + ), + all=True, + ).union( + queryset.filter( + # Match on firstname or lastname + (Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern)) + & ~Q(note__alias__normalized_name__iregex="^" + pattern.lower()) + & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + & ~Q(note__alias__name__iregex="^" + pattern) + ), + all=True, + ) + + queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ + else queryset.order_by("username") + + return queryset + + +# This ViewSet is the only one that is accessible from all authenticated users! +class ContentTypeViewSet(ReadOnlyModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, + then render it on /api/users/ + """ + queryset = ContentType.objects.all() + serializer_class = ContentTypeSerializer diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 2d443d13..e58ba7c1 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -50,10 +50,7 @@ def save_object(sender, instance, **kwargs): in order to store each modification made """ # noinspection PyProtectedMember - if instance._meta.label_lower in EXCLUDED: - return - - if hasattr(instance, "_no_log"): + if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"): return # noinspection PyProtectedMember @@ -120,10 +117,7 @@ def delete_object(sender, instance, **kwargs): Each time a model is deleted, an entry in the table `Changelog` is added in the database """ # noinspection PyProtectedMember - if instance._meta.label_lower in EXCLUDED: - return - - if hasattr(instance, "_no_log"): + if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"): return # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP diff --git a/apps/member/admin.py b/apps/member/admin.py index 4cc2d0bf..7936f564 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -31,9 +31,7 @@ class CustomUserAdmin(UserAdmin): """ When creating a new user don't show profile one the first step """ - if not obj: - return list() - return super().get_inline_instances(request, obj) + return super().get_inline_instances(request, obj) if obj else [] @admin.register(Club, site=admin_site) diff --git a/apps/member/models.py b/apps/member/models.py index b17f1f09..d1218e94 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -172,19 +172,21 @@ class Profile(models.Model): def send_email_validation_link(self): subject = "[Note Kfet] " + str(_("Activate your Note Kfet account")) + token = email_validation_token.make_token(self.user) + uid = urlsafe_base64_encode(force_bytes(self.user_id)) message = loader.render_to_string('registration/mails/email_validation_email.txt', { 'user': self.user, 'domain': os.getenv("NOTE_URL", "note.example.com"), - 'token': email_validation_token.make_token(self.user), - 'uid': urlsafe_base64_encode(force_bytes(self.user.pk)), + 'token': token, + 'uid': uid, }) html = loader.render_to_string('registration/mails/email_validation_email.html', { 'user': self.user, 'domain': os.getenv("NOTE_URL", "note.example.com"), - 'token': email_validation_token.make_token(self.user), - 'uid': urlsafe_base64_encode(force_bytes(self.user.pk)), + 'token': token, + 'uid': uid, }) self.user.email_user(subject, message, html_message=html) @@ -339,43 +341,40 @@ class Membership(models.Model): return self.date_start.toordinal() <= datetime.datetime.now().toordinal() def renew(self): - if Membership.objects.filter( + if not Membership.objects.filter( user=self.user, club=self.club, date_start__gte=self.club.membership_start, ).exists(): - # Membership is already renewed - return - new_membership = Membership( - user=self.user, - club=self.club, - date_start=max(self.date_end + datetime.timedelta(days=1), self.club.membership_start), - ) - if hasattr(self, '_force_renew_parent') and self._force_renew_parent: - new_membership._force_renew_parent = True - if hasattr(self, '_soge') and self._soge: - new_membership._soge = True - if hasattr(self, '_force_save') and self._force_save: - new_membership._force_save = True - new_membership.save() - new_membership.roles.set(self.roles.all()) - new_membership.save() + # Membership is not renewed yet + new_membership = Membership( + user=self.user, + club=self.club, + date_start=max(self.date_end + datetime.timedelta(days=1), self.club.membership_start), + ) + if hasattr(self, '_force_renew_parent') and self._force_renew_parent: + new_membership._force_renew_parent = True + if hasattr(self, '_soge') and self._soge: + new_membership._soge = True + if hasattr(self, '_force_save') and self._force_save: + new_membership._force_save = True + new_membership.save() + new_membership.roles.set(self.roles.all()) + new_membership.save() def save(self, *args, **kwargs): """ Calculate fee and end date before saving the membership and creating the transaction if needed. """ - - if self.pk: + created = not self.pk + if not created: for role in self.roles.all(): club = role.for_club if club is not None: if club.pk != self.club_id: raise ValidationError(_('The role {role} does not apply to the club {club}.') .format(role=role.name, club=club.name)) - - created = not self.pk - if created: + else: if Membership.objects.filter( user=self.user, club=self.club, @@ -384,7 +383,7 @@ class Membership(models.Model): ).exists(): raise ValidationError(_('User is already a member of the club')) - if self.club.parent_club is not None and not self.pk: + if self.club.parent_club is not None: # Check that the user is already a member of the parent club if the membership is created if not Membership.objects.filter( user=self.user, @@ -433,15 +432,10 @@ class Membership(models.Model): raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name) - if self.user.profile.paid: - self.fee = self.club.membership_fee_paid - else: - self.fee = self.club.membership_fee_unpaid + self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid - if self.club.membership_duration is not None: - self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) - else: - self.date_end = self.date_start + datetime.timedelta(days=424242) + self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \ + if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242) if self.club.membership_end is not None and self.date_end > self.club.membership_end: self.date_end = self.club.membership_end diff --git a/apps/member/signals.py b/apps/member/signals.py index fbb66c1f..43659b01 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -6,11 +6,10 @@ def save_user_profile(instance, created, raw, **_kwargs): """ Hook to create and save a profile when an user is updated if it is not registered with the signup form """ - if raw: - # When provisionning data, do not try to autocreate - return - - if created and instance.is_active: + if not raw and created and instance.is_active: from .models import Profile Profile.objects.get_or_create(user=instance) + if instance.is_superuser: + instance.profile.email_confirmed = True + instance.profile.registration_valid = True instance.profile.save() diff --git a/media/pic/default.png b/apps/member/static/member/img/default_picture.png similarity index 100% rename from media/pic/default.png rename to apps/member/static/member/img/default_picture.png diff --git a/apps/member/templates/member/includes/profile_info.html b/apps/member/templates/member/includes/profile_info.html index 04fc6742..372592d5 100644 --- a/apps/member/templates/member/includes/profile_info.html +++ b/apps/member/templates/member/includes/profile_info.html @@ -48,7 +48,9 @@ {% if user_object.pk == user_object.pk %} - - {% trans 'Manage auth token' %} - -{% endif %} \ No newline at end of file + +{% endif %} diff --git a/apps/member/templates/member/manage_auth_tokens.html b/apps/member/templates/member/manage_auth_tokens.html index 473286c1..014686f1 100644 --- a/apps/member/templates/member/manage_auth_tokens.html +++ b/apps/member/templates/member/manage_auth_tokens.html @@ -1,4 +1,4 @@ -{% extends "member/base.html" %} +{% extends "base.html" %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} diff --git a/apps/member/tests/test_login.py b/apps/member/tests/test_login.py index 51a4ab94..e022c4ea 100644 --- a/apps/member/tests/test_login.py +++ b/apps/member/tests/test_login.py @@ -1,9 +1,9 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - +from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase -from note.models import TransactionTemplate, TemplateCategory +from django.urls import reverse """ Test that login page still works @@ -31,7 +31,20 @@ class TemplateLoggedInTests(TestCase): sess.save() def test_login_page(self): - response = self.client.get('/accounts/login/') + response = self.client.get(reverse("login")) + self.assertEqual(response.status_code, 200) + + self.client.logout() + + response = self.client.post('/accounts/login/', data=dict( + username="admin", + password="adminadmin", + permission_mask=3, + )) + self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 200) + + def test_logout(self): + response = self.client.get(reverse("logout")) self.assertEqual(response.status_code, 200) def test_admin_index(self): @@ -41,22 +54,3 @@ class TemplateLoggedInTests(TestCase): def test_accounts_password_reset(self): response = self.client.get('/accounts/password_reset/') self.assertEqual(response.status_code, 200) - - def test_logout_page(self): - response = self.client.get('/accounts/logout/') - self.assertEqual(response.status_code, 200) - - def test_transfer_page(self): - response = self.client.get('/note/transfer/') - self.assertEqual(response.status_code, 200) - - def test_consos_page(self): - # Create one button and ensure that it is visible - cat = TemplateCategory.objects.create() - TransactionTemplate.objects.create( - destination_id=5, - category=cat, - amount=0, - ) - response = self.client.get('/note/consos/') - self.assertEqual(response.status_code, 200) diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py new file mode 100644 index 00000000..ef8b8209 --- /dev/null +++ b/apps/member/tests/test_memberships.py @@ -0,0 +1,405 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import hashlib +import os +from datetime import date, timedelta + +from django.contrib.auth.models import User +from django.core.files.uploadedfile import SimpleUploadedFile +from django.db.models import Q +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from member.models import Club, Membership, Profile +from note.models import Alias, NoteSpecial +from permission.models import Role +from treasury.models import SogeCredit + +""" +Create some users and clubs and test that all pages are rendering properly +and that memberships are working. +""" + + +class TestMemberships(TestCase): + fixtures = ('initial', ) + + def setUp(self) -> None: + """ + Create a sample superuser, a club and a membership for all tests. + """ + self.user = User.objects.create_superuser( + username="toto", + email="toto@example.com", + password="toto", + ) + self.user.profile.registration_valid = True + self.user.profile.email_confirmed = True + self.user.profile.save() + self.client.force_login(self.user) + + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + self.club = Club.objects.create(name="totoclub", parent_club=Club.objects.get(name="BDE")) + self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE")) + self.membership = Membership.objects.create(user=self.user, club=self.club) + self.membership.roles.add(Role.objects.get(name="Bureau de club")) + self.membership.save() + + def test_admin_pages(self): + """ + Check that Django Admin pages for the member app are loading successfully. + """ + response = self.client.get(reverse("admin:index") + "member/membership/") + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("admin:index") + "member/club/") + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("admin:index") + "auth/user/") + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("admin:index") + "auth/user/" + str(self.user.pk) + "/change/") + self.assertEqual(response.status_code, 200) + + def test_render_club_list(self): + """ + Render the list of all clubs, with a search. + """ + response = self.client.get(reverse("member:club_list")) + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("member:club_list") + "?search=toto") + self.assertEqual(response.status_code, 200) + + def test_render_club_create(self): + """ + Try to create a new club. + """ + response = self.client.get(reverse("member:club_create")) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("member:club_create"), data=dict( + name="Club toto", + email="clubtoto@example.com", + parent_club=self.club.pk, + require_memberships=False, + membership_fee_paid=0, + membership_fee_unpaid=0, + )) + self.assertTrue(Club.objects.filter(name="Club toto").exists()) + club = Club.objects.get(name="Club toto") + self.assertRedirects(response, club.get_absolute_url(), 302, 200) + + def test_render_club_detail(self): + """ + Display the detail of a club. + """ + response = self.client.get(reverse("member:club_detail", args=(self.club.pk,))) + self.assertEqual(response.status_code, 200) + + def test_render_club_update(self): + """ + Try to update the information about a club. + """ + response = self.client.get(reverse("member:club_update", args=(self.club.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("member:club_update", args=(self.club.pk, )), data=dict( + name="Toto club updated", + email="clubtoto@example.com", + require_memberships=True, + membership_fee_paid=0, + membership_fee_unpaid=0, + )) + self.assertRedirects(response, self.club.get_absolute_url(), 302, 200) + self.assertTrue(Club.objects.exclude(name="Toto club updated")) + + def test_render_club_update_picture(self): + """ + Try to update the picture of the note of a club. + """ + response = self.client.get(reverse("member:club_update_pic", args=(self.club.pk,))) + self.assertEqual(response.status_code, 200) + + old_pic = self.club.note.display_image + + with open("apps/member/static/member/img/default_picture.png", "rb") as f: + image = SimpleUploadedFile("image.png", f.read(), "image/png") + response = self.client.post(reverse("member:club_update_pic", args=(self.club.pk,)), dict( + image=image, + x=0, + y=0, + width=200, + height=200, + )) + self.assertRedirects(response, self.club.get_absolute_url(), 302, 200) + + self.club.note.refresh_from_db() + self.assertTrue(os.path.exists(self.club.note.display_image.path)) + os.remove(self.club.note.display_image.path) + + self.club.note.display_image = old_pic + self.club.note.save() + + def test_render_club_aliases(self): + """ + Display the list of the aliases of a club. + """ + # Alias creation and deletion is already tested in the note app + response = self.client.get(reverse("member:club_alias", args=(self.club.pk,))) + self.assertEqual(response.status_code, 200) + + def test_render_club_members(self): + """ + Display the list of the members of a club. + """ + response = self.client.get(reverse("member:club_members", args=(self.club.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse("member:club_members", args=(self.club.pk,)) + "?search=toto&roles=" + + ",".join([str(role.pk) for role in + Role.objects.filter(weirole__isnull=True).all()])) + self.assertEqual(response.status_code, 200) + + def test_render_club_add_member(self): + """ + Try to add memberships and renew them. + """ + response = self.client.get(reverse("member:club_add_member", args=(Club.objects.get(name="BDE").pk,))) + self.assertEqual(response.status_code, 200) + + user = User.objects.create(username="totototo") + user.profile.registration_valid = True + user.profile.email_confirmed = True + user.profile.save() + user.save() + + # We create a club without any parent and one club with parent BDE (that is the club Kfet) + for bde_parent in False, True: + if bde_parent: + club = Club.objects.get(name="Kfet") + else: + club = Club.objects.create( + name="Second club " + ("with BDE" if bde_parent else "without BDE"), + parent_club=None, + email="newclub@example.com", + require_memberships=True, + membership_fee_paid=1000, + membership_fee_unpaid=500, + membership_start=date.today(), + membership_end=date.today() + timedelta(days=366), + membership_duration=366, + ) + + response = self.client.get(reverse("member:club_add_member", args=(club.pk,))) + self.assertEqual(response.status_code, 200) + + # Create a new membership + response = self.client.post(reverse("member:club_add_member", args=(club.pk,)), data=dict( + user=user.pk, + date_start="{:%Y-%m-%d}".format(timezone.now().date()), + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=4200, + last_name="TOTO", + first_name="Toto", + bank="Le matelas", + )) + self.assertRedirects(response, club.get_absolute_url(), 302, 200) + + self.assertTrue(Membership.objects.filter(user=user, club=club).exists()) + + # Membership is sent to the past to check renewals + membership = Membership.objects.get(user=user, club=club) + self.assertTrue(membership.valid) + membership.date_start = date(year=2000, month=1, day=1) + membership.date_end = date(year=2000, month=12, day=31) + membership.save() + self.assertFalse(membership.valid) + + response = self.client.get(reverse("member:club_members", args=(club.pk,)) + "?only_active=0") + self.assertEqual(response.status_code, 200) + + bde_membership = self.bde_membership + if bde_parent: + bde_membership = Membership.objects.get(club__name="BDE", user=user) + bde_membership.date_start = date(year=2000, month=1, day=1) + bde_membership.date_end = date(year=2000, month=12, day=31) + bde_membership.save() + + response = self.client.get(reverse("member:club_renew_membership", args=(bde_membership.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse("member:club_renew_membership", args=(membership.pk,))) + self.assertEqual(response.status_code, 200) + + # Renew membership + response = self.client.post(reverse("member:club_renew_membership", args=(membership.pk,)), data=dict( + user=user.pk, + date_start="{:%Y-%m-%d}".format(timezone.now().date()), + soge=bde_parent, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=14242, + last_name="TOTO", + first_name="Toto", + bank="Bank", + )) + self.assertRedirects(response, club.get_absolute_url(), 302, 200) + + response = self.client.get(user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_auto_join_kfet_when_join_bde_with_soge(self): + """ + When we join the BDE club with a Soge registration, a Kfet membership is automatically created. + We check that it is the case. + """ + user = User.objects.create(username="new1A") + user.profile.registration_valid = True + user.profile.email_confirmed = True + user.profile.save() + user.save() + + bde = Club.objects.get(name="BDE") + kfet = Club.objects.get(name="Kfet") + + response = self.client.post(reverse("member:club_add_member", args=(bde.pk,)), data=dict( + user=user.pk, + date_start="{:%Y-%m-%d}".format(timezone.now().date()), + soge=True, + credit_type=NoteSpecial.objects.get(special_type="Virement bancaire").id, + credit_amount=(bde.membership_fee_paid + kfet.membership_fee_paid) / 100, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + )) + self.assertRedirects(response, bde.get_absolute_url(), 302, 200) + + self.assertTrue(Membership.objects.filter(user=user, club=bde).exists()) + self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists()) + self.assertTrue(SogeCredit.objects.filter(user=user).exists()) + + def test_change_roles(self): + """ + Check to change the roles of a membership. + """ + response = self.client.get(reverse("member:club_manage_roles", args=(self.membership.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict( + roles=[role.id for role in Role.objects.filter( + Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()], + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.membership.refresh_from_db() + self.assertEqual(self.membership.roles.count(), 3) + + def test_render_user_list(self): + """ + Display the user search page. + """ + response = self.client.get(reverse("member:user_list")) + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("member:user_list") + "?search=toto") + self.assertEqual(response.status_code, 200) + + def test_render_user_detail(self): + """ + Display the user detail page. + """ + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_render_user_update(self): + """ + Update some data about the user. + """ + response = self.client.get(reverse("member:user_update_profile", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("member:user_update_profile", args=(self.user.pk,)), data=dict( + first_name="Toto", + last_name="Toto", + username="toto changed", + email="updated@example.com", + phone_number="+33600000000", + section="", + department="A0", + promotion=timezone.now().year, + address="Earth", + paid=True, + ml_events_registration="en", + ml_sports_registration=True, + ml_art_registration=True, + report_frequency=7, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.assertTrue(User.objects.filter(username="toto changed").exists()) + self.assertTrue(Profile.objects.filter(address="Earth").exists()) + self.assertTrue(Alias.objects.filter(normalized_name="totochanged").exists()) + + def test_render_user_update_picture(self): + """ + Update the note picture of the user. + """ + response = self.client.get(reverse("member:user_update_pic", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + old_pic = self.user.note.display_image + + with open("apps/member/static/member/img/default_picture.png", "rb") as f: + image = SimpleUploadedFile("image.png", f.read(), "image/png") + response = self.client.post(reverse("member:user_update_pic", args=(self.user.pk,)), dict( + image=image, + x=0, + y=0, + width=200, + height=200, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + + self.user.note.refresh_from_db() + self.assertTrue(os.path.exists(self.user.note.display_image.path)) + os.remove(self.user.note.display_image.path) + + self.user.note.display_image = old_pic + self.user.note.save() + + def test_render_user_aliases(self): + """ + Display the list of aliases of the user. + """ + # Alias creation and deletion is already tested in the note app + response = self.client.get(reverse("member:user_alias", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + def test_manage_auth_token(self): + """ + Display the page to see the API authentication token, see it and regenerate it. + :return: + """ + response = self.client.get(reverse("member:auth_token")) + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("member:auth_token") + "?view") + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("member:auth_token") + "?regenerate") + self.assertRedirects(response, reverse("member:auth_token") + "?view", 302, 200) + + def test_random_coverage(self): + # Useless, only for coverage + self.assertEqual(str(self.user), str(self.user.profile)) + self.user.profile.promotion = None + self.assertEqual(self.user.profile.ens_year, 0) + self.membership.date_end = None + self.assertTrue(self.membership.valid) + + def test_nk15_hasher(self): + """ + Test that NK15 passwords are successfully imported. + """ + salt = "42" + password = "strongpassword42" + hashed = hashlib.sha256((salt + password).encode("utf-8")).hexdigest() + self.user.password = "custom_nk15$1$" + salt + "|" + hashed + self.user.save() + self.assertTrue(self.user.check_password(password)) diff --git a/apps/member/views.py b/apps/member/views.py index cccffc4a..4534c9e8 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -97,8 +97,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): note = NoteUser.objects.filter( alias__normalized_name=Alias.normalize(new_username)) if note.exists() and note.get().user != self.object: - form.add_error('username', - _("An alias with a similar name already exists.")) + form.add_error('username', _("An alias with a similar name already exists.")) return super().form_invalid(form) # Check if the username is one of user's aliases. alias = Alias.objects.filter(name=new_username) @@ -141,10 +140,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ We can't display information of a not registered user. """ - qs = super().get_queryset() - if self.request.user.is_superuser and self.request.session.get("permission_mask", -1) >= 42: - return qs - return qs.filter(profile__registration_valid=True) + return super().get_queryset().filter(profile__registration_valid=True) def get_context_data(self, **kwargs): """ @@ -204,14 +200,16 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Filter the user list with the given pattern. """ - qs = super().get_queryset().distinct("username").annotate(alias=F("note__alias__name"))\ + qs = super().get_queryset().annotate(alias=F("note__alias__name"))\ .annotate(normalized_alias=F("note__alias__normalized_name"))\ - .filter(profile__registration_valid=True).order_by("username") - if "search" in self.request.GET: - pattern = self.request.GET["search"] + .filter(profile__registration_valid=True) - if not pattern: - return qs.none() + # Sqlite doesn't support order by in subqueries + qs = qs.order_by("username").distinct("username")\ + if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql' else qs.distinct() + + if "search" in self.request.GET and self.request.GET["search"]: + pattern = self.request.GET["search"] qs = qs.filter( username__iregex="^" + pattern @@ -270,12 +268,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det def post(self, request, *args, **kwargs): form = self.get_form() self.object = self.get_object() - if form.is_valid(): - return self.form_valid(form) - else: - print('is_invalid') - print(form) - return self.form_invalid(form) + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) def form_valid(self, form): image_field = form.cleaned_data['image'] @@ -320,8 +313,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): def get(self, request, *args, **kwargs): if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): Token.objects.get(user=self.request.user).delete() - return redirect(reverse_lazy('member:auth_token') + "?show", - permanent=True) + return redirect(reverse_lazy('member:auth_token') + "?show") return super().get(request, *args, **kwargs) @@ -351,8 +343,9 @@ class ClubCreateView(ProtectQuerysetMixin, ProtectedCreateView): email="", ) - def form_valid(self, form): - return super().form_valid(form) + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk}) class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): @@ -655,7 +648,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid c = c.parent_club - if user.note.balance + credit_amount < fee and not Membership.objects.filter( + if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter( club__name="Kfet", user=user, date_start__lte=date.today(), @@ -683,7 +676,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): if club.membership_end and form.instance.date_start > club.membership_end: form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") - .format(form.instance.club.membership_start)) + .format(form.instance.club.membership_end)) return super().form_invalid(form) # Now, all is fine, the membership can be created. @@ -719,46 +712,38 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): transaction._force_save = True transaction.save() + # Parent club memberships are automatically renewed / created. + # For example, a Kfet membership creates a BDE membership if it does not exist. form.instance._force_renew_parent = True ret = super().form_valid(form) - if club.name == "BDE": - member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() - elif club.name == "Kfet": - member_role = Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() - else: - member_role = Role.objects.filter(name="Membre de club").all() + member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \ + if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \ + if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all() form.instance.roles.set(member_role) form.instance._force_save = True form.instance.save() # If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the # Kfet membership. - if soge: - # If not already done, create BDE and Kfet memberships - bde = Club.objects.get(name="BDE") + if soge and club.name == "BDE": kfet = Club.objects.get(name="Kfet") + fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid - soge_clubs = [bde, kfet] - for club in soge_clubs: - fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid - - # Get current membership, to get the end date - old_membership = Membership.objects.filter( - club=club, - user=user, - ).order_by("-date_start") - - if old_membership.filter(date_start__gte=club.membership_start).exists(): - # Membership is already renewed - continue + # Get current membership, to get the end date + old_membership = Membership.objects.filter( + club=kfet, + user=user, + ).order_by("-date_start") + if not old_membership.filter(date_start__gte=kfet.membership_start).exists(): + # If the membership is not already renewed membership = Membership( - club=club, + club=kfet, user=user, fee=fee, - date_start=max(old_membership.first().date_end + timedelta(days=1), club.membership_start) + date_start=max(old_membership.first().date_end + timedelta(days=1), kfet.membership_start) if old_membership.exists() else form.instance.date_start, ) membership._force_save = True @@ -767,10 +752,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): membership.refresh_from_db() if old_membership.exists(): membership.roles.set(old_membership.get().roles.all()) - elif c.name == "BDE": - membership.roles.set(Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all()) - elif c.name == "Kfet": - membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all()) + membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all()) membership.save() return ret @@ -830,9 +812,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV 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_str = self.request.GET["roles"].replace(' ', '').split(',') if self.request.GET["roles"] else ['0'] roles_int = map(int, roles_str) qs = qs.filter(roles__in=roles_int) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 9b213025..a478e7ff 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -1,6 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - +from django.conf import settings from django.db.models import Q from django.core.exceptions import ValidationError from django_filters.rest_framework import DjangoFilterBackend @@ -117,6 +117,9 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): """ queryset = super().get_queryset() + # Sqlite doesn't support ORDER BY in subqueries + queryset = queryset.order_by("name") \ + if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset alias = self.request.query_params.get("alias", ".*") queryset = queryset.prefetch_related('note') @@ -137,7 +140,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): ), all=True) - return queryset.order_by('name').distinct() + queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ + else queryset.order_by("name") + + return queryset.distinct() class TemplateCategoryViewSet(ReadProtectedModelViewSet): diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index d88be5a6..a4f220bd 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -356,4 +356,4 @@ class MembershipTransaction(Transaction): @property def type(self): - return _('membership transaction') + return _('membership').capitalize() diff --git a/apps/note/tables.py b/apps/note/tables.py index b1d434ae..0ca50306 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -29,6 +29,7 @@ class HistoryTable(tables.Table): source = tables.Column( attrs={ "td": { + "class": "text-nowrap", "data-toggle": "tooltip", "title": lambda record: _("used alias").capitalize() + " : " + record.source_alias, } @@ -38,15 +39,46 @@ class HistoryTable(tables.Table): destination = tables.Column( attrs={ "td": { + "class": "text-nowrap", "data-toggle": "tooltip", "title": lambda record: _("used alias").capitalize() + " : " + record.destination_alias, } } ) + created_at = tables.DateTimeColumn(format='Y-m-d H:i:s', + attrs={ + "td": { + "class": "text-nowrap", + }, + } + ) + + amount = tables.Column( + attrs={ + "td": { + "class": "text-nowrap", + }, + } + ) + + reason = tables.Column( + attrs={ + "td": { + "class": "text-break", + }, + } + ) + type = tables.Column() - total = tables.Column() # will use Transaction.total() !! + total = tables.Column( # will use Transaction.total() !! + attrs={ + "td": { + "class": "text-nowrap", + }, + } + ) valid = tables.Column( attrs={ diff --git a/apps/note/templates/note/conso_form.html b/apps/note/templates/note/conso_form.html index 07c63488..d5914055 100644 --- a/apps/note/templates/note/conso_form.html +++ b/apps/note/templates/note/conso_form.html @@ -15,7 +15,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
-
diff --git a/apps/note/templates/note/transaction_form.html b/apps/note/templates/note/transaction_form.html index acb09beb..15478219 100644 --- a/apps/note/templates/note/transaction_form.html +++ b/apps/note/templates/note/transaction_form.html @@ -38,7 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later {# Preview note profile (picture, username and balance) #}
-
diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 120bb1ee..5f266788 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -2567,6 +2567,70 @@ "description": "(Dé)bloquer sa propre note et modifier la raison" } }, + { + "model": "permission.permission", + "pk": 165, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{}", + "type": "change", + "mask": 1, + "field": "password", + "permanent": true, + "description": "Changer son mot de passe" + } + }, + { + "model": "permission.permission", + "pk": 166, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]", + "type": "add", + "mask": 2, + "field": "", + "permanent": false, + "description": "Créer une transaction quelconque tant que la source reste au-dessus de -50 €" + } + }, + { + "model": "permission.permission", + "pk": 167, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]", + "type": "change", + "mask": 2, + "field": "valid", + "permanent": false, + "description": "Modifier le statut de validation d'une transaction si c'est possible" + } + }, + { + "model": "permission.permission", + "pk": 168, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]", + "type": "change", + "mask": 2, + "field": "invalidity_reason", + "permanent": false, + "description": "Modifier la raison d'invalidité d'une transaction si c'est possible" + } + }, { "model": "permission.role", "pk": 1, @@ -2591,7 +2655,8 @@ 52, 126, 161, - 162 + 162, + 165 ] } }, @@ -2697,7 +2762,11 @@ 127, 133, 141, - 142 + 142, + 150, + 166, + 167, + 168 ] } }, @@ -2711,8 +2780,7 @@ 24, 25, 26, - 27, - 33 + 27 ] } }, @@ -2932,7 +3000,8 @@ 161, 162, 163, - 164 + 164, + 165 ] } }, @@ -2944,7 +3013,6 @@ "name": "GC Kfet", "permissions": [ 32, - 33, 56, 58, 55, @@ -2959,7 +3027,10 @@ 29, 30, 31, - 143 + 143, + 166, + 167, + 168 ] } }, diff --git a/apps/permission/tests/test_permission_queries.py b/apps/permission/tests/test_permission_queries.py index e0af9cf0..4d73ae11 100644 --- a/apps/permission/tests/test_permission_queries.py +++ b/apps/permission/tests/test_permission_queries.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from datetime import date +from json.decoder import JSONDecodeError from django.contrib.auth.models import User from django.core.exceptions import FieldError @@ -56,29 +57,29 @@ class PermissionQueryTestCase(TestCase): We use a random user with a random WEIClub (to use permissions for the WEI) in a random team in a random bus. """ for perm in Permission.objects.all(): - instanced = perm.about( - user=User.objects.get(), - club=WEIClub.objects.get(), - membership=Membership.objects.get(), - User=User, - Club=Club, - Membership=Membership, - Note=Note, - NoteUser=NoteUser, - NoteClub=NoteClub, - NoteSpecial=NoteSpecial, - F=F, - Q=Q, - now=timezone.now(), - today=date.today(), - ) try: + instanced = perm.about( + user=User.objects.get(), + club=WEIClub.objects.get(), + membership=Membership.objects.get(), + User=User, + Club=Club, + Membership=Membership, + Note=Note, + NoteUser=NoteUser, + NoteClub=NoteClub, + NoteSpecial=NoteSpecial, + F=F, + Q=Q, + now=timezone.now(), + today=date.today(), + ) instanced.update_query() query = instanced.query model = perm.model.model_class() model.objects.filter(query).all() # print("Good query for permission", perm) - except (FieldError, AttributeError, ValueError, TypeError): + except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError): print("Query error for permission", perm) print("Query:", perm.query) if instanced.query: diff --git a/apps/registration/tests/__init__.py b/apps/registration/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/registration/tests/test_registration.py b/apps/registration/tests/test_registration.py new file mode 100644 index 00000000..e2191445 --- /dev/null +++ b/apps/registration/tests/test_registration.py @@ -0,0 +1,386 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib.auth.models import User +from django.db.models import Q +from django.test import TestCase +from django.urls import reverse +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from member.models import Club, Membership +from note.models import NoteUser, NoteSpecial, Transaction +from registration.tokens import email_validation_token +from treasury.models import SogeCredit + +""" +Check that pre-registrations and validations are working as well. +""" + + +class TestSignup(TestCase): + """ + Assume we are a new user. + Check that it can pre-register without any problem. + """ + + fixtures = ("initial", ) + + def test_signup(self): + """ + A first year member signs up and validates its email address. + """ + response = self.client.get(reverse("registration:signup")) + self.assertEqual(response.status_code, 200) + + # Signup + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="toto", + email="toto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="+33123456789", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200) + self.assertTrue(User.objects.filter(username="toto").exists()) + user = User.objects.get(username="toto") + # A preregistred user has no note + self.assertFalse(NoteUser.objects.filter(user=user).exists()) + self.assertFalse(user.profile.registration_valid) + self.assertFalse(user.profile.email_confirmed) + self.assertFalse(user.is_active) + + response = self.client.get(reverse("registration:email_validation_sent")) + self.assertEqual(response.status_code, 200) + + # Check that the email validation link is valid + token = email_validation_token.make_token(user) + uid = urlsafe_base64_encode(force_bytes(user.pk)) + response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=uid, token=token))) + self.assertEqual(response.status_code, 200) + user.profile.refresh_from_db() + self.assertTrue(user.profile.email_confirmed) + + # Token has expired + response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=uid, token=token))) + self.assertEqual(response.status_code, 400) + + # Uid does not exist + response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=0, token="toto"))) + self.assertEqual(response.status_code, 400) + + def test_invalid_signup(self): + """ + Send wrong data and check that it is not valid + """ + User.objects.create_superuser( + first_name="Toto", + last_name="TOTO", + username="toto", + email="toto@example.com", + password="toto1234", + ) + + # The email is already used + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="tôtö", + email="toto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="+33123456789", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertTrue(response.status_code, 200) + + # The username is similar to a known alias + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="tôtö", + email="othertoto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="+33123456789", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertTrue(response.status_code, 200) + + # The phone number is invalid + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="Ihaveanotherusername", + email="othertoto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="invalid phone number", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertTrue(response.status_code, 200) + + +class TestValidateRegistration(TestCase): + """ + Test the admin interface to validate users + """ + + fixtures = ('initial',) + + def setUp(self) -> None: + self.superuser = User.objects.create_superuser( + username="admintoto", + password="toto1234", + email="admin.toto@example.com", + ) + self.client.force_login(self.superuser) + + self.user = User.objects.create( + username="toto", + first_name="Toto", + last_name="TOTO", + email="toto@example.com", + ) + + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + def test_future_user_list(self): + """ + Display the list of pre-registered users + """ + response = self.client.get(reverse("registration:future_user_list")) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse("registration:future_user_list") + "?search=toto") + self.assertEqual(response.status_code, 200) + + def test_invalid_registrations(self): + """ + Send wrong data and check that errors are detected + """ + + # BDE Membership is mandatory + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=4200, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=False, + join_Kfet=False, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # Same + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type="", + credit_amount=0, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=False, + join_Kfet=True, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # The BDE membership is not free + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=0, + last_name="TOTO", + first_name="Toto", + bank="J'ai pas d'argent", + join_BDE=True, + join_Kfet=True, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # Last and first names are required for a credit + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=4000, + last_name="", + first_name="", + bank="", + join_BDE=True, + join_Kfet=True, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # The username admïntoto is too similar with the alias admintoto. + # Since the form is valid, the user must update its username. + self.user.username = "admïntoto" + self.user.save() + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=500, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=False, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + def test_validate_bde_registration(self): + """ + The user wants only to join the BDE. We validate the registration. + """ + response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 404) + + self.user.profile.email_confirmed = True + self.user.profile.save() + + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=500, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=False, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.user.profile.refresh_from_db() + self.assertTrue(self.user.profile.registration_valid) + self.assertTrue(NoteUser.objects.filter(user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists()) + self.assertFalse(Membership.objects.filter(club__name="Kfet", user=self.user).exists()) + self.assertFalse(SogeCredit.objects.filter(user=self.user).exists()) + self.assertEqual(Transaction.objects.filter( + Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_kfet_registration(self): + """ + The user joins the BDE and the Kfet. + """ + response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 404) + + self.user.profile.email_confirmed = True + self.user.profile.save() + + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=4000, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=True, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.user.profile.refresh_from_db() + self.assertTrue(self.user.profile.registration_valid) + self.assertTrue(NoteUser.objects.filter(user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists()) + self.assertFalse(SogeCredit.objects.filter(user=self.user).exists()) + self.assertEqual(Transaction.objects.filter( + Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_kfet_registration_with_soge(self): + """ + The user joins the BDE and the Kfet, but the membership is paid by the Société générale. + """ + response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 404) + + self.user.profile.email_confirmed = True + self.user.profile.save() + + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=True, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=4000, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=True, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.user.profile.refresh_from_db() + self.assertTrue(self.user.profile.registration_valid) + self.assertTrue(NoteUser.objects.filter(user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists()) + self.assertTrue(SogeCredit.objects.filter(user=self.user).exists()) + self.assertEqual(Transaction.objects.filter( + Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2) + self.assertFalse(Transaction.objects.filter(valid=True).exists()) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_invalidate_registration(self): + """ + Try to invalidate (= delete) pre-registration. + """ + response = self.client.get(reverse("registration:future_user_invalidate", args=(self.user.pk,))) + self.assertRedirects(response, reverse("registration:future_user_list"), 302, 200) + self.assertFalse(User.objects.filter(pk=self.user.pk).exists()) + + def test_resend_email_validation_link(self): + """ + Resend email validation linK. + """ + response = self.client.get(reverse("registration:email_validation_resend", args=(self.user.pk,))) + self.assertRedirects(response, reverse("registration:future_user_detail", args=(self.user.pk,)), 302, 200) diff --git a/apps/registration/views.py b/apps/registration/views.py index bf68a8ed..7a924591 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -16,7 +16,7 @@ from django.views.generic.edit import FormMixin from django_tables2 import SingleTableView from member.forms import ProfileForm from member.models import Membership, Club -from note.models import SpecialTransaction +from note.models import SpecialTransaction, Alias from note.templatetags.pretty_money import pretty_money from permission.backends import PermissionBackend from permission.models import Role @@ -101,7 +101,7 @@ class UserValidateView(TemplateView): user.profile.email_confirmed = True user.save() user.profile.save() - return self.render_to_response(self.get_context_data()) + return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400) def get_user(self, uidb64): """ @@ -169,12 +169,9 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi :return: """ qs = super().get_queryset().distinct().filter(profile__registration_valid=False) - if "search" in self.request.GET: + if "search" in self.request.GET and self.request.GET["search"]: pattern = self.request.GET["search"] - if not pattern: - return qs.none() - qs = qs.filter( Q(first_name__iregex=pattern) | Q(last_name__iregex=pattern) @@ -205,10 +202,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, def post(self, request, *args, **kwargs): form = self.get_form() self.object = self.get_object() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) def get_queryset(self, **kwargs): """ @@ -239,6 +233,10 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, def form_valid(self, form): user = self.get_object() + if Alias.objects.filter(normalized_name=Alias.normalize(user.username)).exists(): + form.add_error(None, _("An alias with a similar name already exists.")) + return self.form_invalid(form) + # Get form data soge = form.cleaned_data["soge"] credit_type = form.cleaned_data["credit_type"] @@ -276,9 +274,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, if credit_type is None: credit_amount = 0 - if join_Kfet and not join_BDE: - form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club.")) - if fee > credit_amount and not soge: # Check if the user credits enough money form.add_error('credit_type', diff --git a/apps/scripts b/apps/scripts index c1c0a879..4e1bcd18 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit c1c0a8797179d110ad919912378f05b030f44f61 +Subproject commit 4e1bcd1808a24b532aa27bf2a119f6f8155af534 diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py index 1db820b2..25b4f4cb 100644 --- a/apps/treasury/admin.py +++ b/apps/treasury/admin.py @@ -24,9 +24,7 @@ class RemittanceAdmin(admin.ModelAdmin): list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', ) def has_change_permission(self, request, obj=None): - if not obj: - return True - return not obj.closed and super().has_change_permission(request, obj) + return not obj or (not obj.closed and super().has_change_permission(request, obj)) @admin.register(SogeCredit, site=admin_site) diff --git a/apps/treasury/api/views.py b/apps/treasury/api/views.py index ee97e6ac..82a0ed1e 100644 --- a/apps/treasury/api/views.py +++ b/apps/treasury/api/views.py @@ -16,7 +16,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer, then render it on /api/treasury/invoice/ """ - queryset = Invoice.objects.all() + queryset = Invoice.objects.order_by("id").all() serializer_class = InvoiceSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['bde', ] @@ -28,7 +28,7 @@ class ProductViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer, then render it on /api/treasury/product/ """ - queryset = Product.objects.all() + queryset = Product.objects.order_by("invoice_id", "id").all() serializer_class = ProductSerializer filter_backends = [SearchFilter] search_fields = ['$designation', ] @@ -40,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer then render it on /api/treasury/remittance_type/ """ - queryset = RemittanceType.objects + queryset = RemittanceType.objects.order_by("id") serializer_class = RemittanceTypeSerializer @@ -50,7 +50,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer, then render it on /api/treasury/remittance/ """ - queryset = Remittance.objects + queryset = Remittance.objects.order_by("id") serializer_class = RemittanceSerializer @@ -60,5 +60,5 @@ class SogeCreditViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer, then render it on /api/treasury/soge_credit/ """ - queryset = SogeCredit.objects + queryset = SogeCredit.objects.order_by("id") serializer_class = SogeCreditSerializer diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index 38da324d..c2461f76 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -16,21 +16,15 @@ class InvoiceForm(forms.ModelForm): """ def clean(self): + # If the invoice is locked, it can't be updated. if self.instance and self.instance.locked: for field_name in self.fields: self.cleaned_data[field_name] = getattr(self.instance, field_name) self.errors.clear() + self.add_error(None, _('This invoice is locked and can no longer be edited.')) return self.cleaned_data return super().clean() - def save(self, commit=True): - """ - If the invoice is locked, don't save it - """ - if not self.instance.locked: - super().save(commit) - return self.instance - class Meta: model = Invoice exclude = ('bde', 'date', 'tex', ) diff --git a/apps/treasury/models.py b/apps/treasury/models.py index 6d5b4021..762c7bb5 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -85,7 +85,7 @@ class Invoice(models.Model): old_invoice = Invoice.objects.filter(id=self.id) if old_invoice.exists(): - if old_invoice.get().locked: + if old_invoice.get().locked and not self._force_save: raise ValidationError(_("This invoice is locked and can no longer be edited.")) products = self.products.all() @@ -224,7 +224,7 @@ class Remittance(models.Model): def save(self, force_insert=False, force_update=False, using=None, update_fields=None): # Check if all transactions have the right type. - if self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): + if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): raise ValidationError("All transactions in a remittance must have the same type") return super().save(force_insert, force_update, using, update_fields) diff --git a/note_kfet/static/img/Finalist.png b/apps/treasury/static/img/Finalist.png similarity index 100% rename from note_kfet/static/img/Finalist.png rename to apps/treasury/static/img/Finalist.png diff --git a/note_kfet/static/img/Kataclist.png b/apps/treasury/static/img/Kataclist.png similarity index 100% rename from note_kfet/static/img/Kataclist.png rename to apps/treasury/static/img/Kataclist.png diff --git a/note_kfet/static/img/Listorique.png b/apps/treasury/static/img/Listorique.png similarity index 100% rename from note_kfet/static/img/Listorique.png rename to apps/treasury/static/img/Listorique.png diff --git a/note_kfet/static/img/Monopolist.png b/apps/treasury/static/img/Monopolist.png similarity index 100% rename from note_kfet/static/img/Monopolist.png rename to apps/treasury/static/img/Monopolist.png diff --git a/note_kfet/static/img/Saperlistpopette.png b/apps/treasury/static/img/Saperlistpopette.png similarity index 100% rename from note_kfet/static/img/Saperlistpopette.png rename to apps/treasury/static/img/Saperlistpopette.png diff --git a/note_kfet/static/img/Satellist.png b/apps/treasury/static/img/Satellist.png similarity index 100% rename from note_kfet/static/img/Satellist.png rename to apps/treasury/static/img/Satellist.png diff --git a/apps/treasury/tables.py b/apps/treasury/tables.py index 14044f1c..9a72ecf3 100644 --- a/apps/treasury/tables.py +++ b/apps/treasury/tables.py @@ -34,7 +34,7 @@ class InvoiceTable(tables.Table): delete = tables.LinkColumn( 'treasury:invoice_delete', - args=[A('pk')], + args=[A('id')], verbose_name=_("delete"), text=_("Delete"), attrs={ diff --git a/apps/treasury/templates/treasury/invoice_list.html b/apps/treasury/templates/treasury/invoice_list.html index 32c1b1c1..d9cd8a3e 100644 --- a/apps/treasury/templates/treasury/invoice_list.html +++ b/apps/treasury/templates/treasury/invoice_list.html @@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% block content %}
-
+
{% trans "Invoice" %}s diff --git a/apps/treasury/templates/treasury/invoice_sample.tex b/apps/treasury/templates/treasury/invoice_sample.tex index 4e6342b0..d7ec7391 100644 --- a/apps/treasury/templates/treasury/invoice_sample.tex +++ b/apps/treasury/templates/treasury/invoice_sample.tex @@ -58,7 +58,7 @@ \parbox[b][\paperheight]{\paperwidth}{% \vfill \centering - {\transparent{0.1}\includegraphics[width=\textwidth]{../../static/img/{{ obj.bde }}}}% + {\transparent{0.1}\includegraphics[width=\textwidth]{../../apps/treasury/static/img/{{ obj.bde }}}}% \vfill } } diff --git a/apps/treasury/templates/treasury/remittance_list.html b/apps/treasury/templates/treasury/remittance_list.html index c400f18f..8ced1ad0 100644 --- a/apps/treasury/templates/treasury/remittance_list.html +++ b/apps/treasury/templates/treasury/remittance_list.html @@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% block content %}
-
+
{% trans "Invoice" %}s diff --git a/apps/treasury/templates/treasury/sogecredit_list.html b/apps/treasury/templates/treasury/sogecredit_list.html index c3862811..1eb1aba5 100644 --- a/apps/treasury/templates/treasury/sogecredit_list.html +++ b/apps/treasury/templates/treasury/sogecredit_list.html @@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% block content %}
-
+
{% trans "Invoice" %}s @@ -59,9 +59,6 @@ SPDX-License-Identifier: GPL-3.0-or-later function reloadTable() { let pattern = searchbar_obj.val(); - if (pattern === old_pattern || pattern === "") - return; - $("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + ( invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table"); diff --git a/apps/treasury/tests/__init__.py b/apps/treasury/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/treasury/tests/test_treasury.py b/apps/treasury/tests/test_treasury.py new file mode 100644 index 00000000..15d35cb3 --- /dev/null +++ b/apps/treasury/tests/test_treasury.py @@ -0,0 +1,403 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.test import TestCase +from django.urls import reverse + +from member.models import Membership, Club +from note.models import SpecialTransaction, NoteSpecial, Transaction +from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit + + +class TestInvoices(TestCase): + """ + Check that invoices can be created and rendered properly. + """ + def setUp(self) -> None: + self.user = User.objects.create_superuser( + username="admintoto", + password="totototo", + email="admin@example.com", + ) + self.client.force_login(self.user) + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + self.invoice = Invoice.objects.create( + id=1, + object="Object", + description="Description", + name="Me", + address="Earth", + acquitted=False, + ) + self.product = Product.objects.create( + invoice=self.invoice, + designation="Product", + quantity=3, + amount=3.14, + ) + + def test_admin_page(self): + """ + Display the invoice admin page. + """ + response = self.client.get(reverse("admin:index") + "treasury/invoice/") + self.assertEqual(response.status_code, 200) + + def test_invoices_list(self): + """ + Display the list of invoices. + """ + response = self.client.get(reverse("treasury:invoice_list")) + self.assertEqual(response.status_code, 200) + + def test_invoice_create(self): + """ + Try to create a new invoice. + """ + response = self.client.get(reverse("treasury:invoice_create")) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("treasury:invoice_create"), data={ + "id": 42, + "object": "Same object", + "description": "Longer description", + "name": "Me and others", + "address": "Alwways earth", + "acquitted": True, + "products-0-designation": "Designation", + "products-0-quantity": 1, + "products-0-amount": 42, + "products-TOTAL_FORMS": 1, + "products-INITIAL_FORMS": 0, + "products-MIN_NUM_FORMS": 0, + "products-MAX_NUM_FORMS": 1000, + }) + self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200) + self.assertTrue(Invoice.objects.filter(object="Same object", id=42).exists()) + self.assertTrue(Product.objects.filter(designation="Designation", invoice_id=42).exists()) + self.assertTrue(Invoice.objects.get(id=42).tex) + + def test_invoice_update(self): + """ + Try to update an invoice. + """ + response = self.client.get(reverse("treasury:invoice_update", args=(self.invoice.id,))) + self.assertEqual(response.status_code, 200) + + data = { + "object": "Same object", + "description": "Longer description", + "name": "Me and others", + "address": "Always earth", + "acquitted": True, + "locked": True, + "products-0-designation": "Designation", + "products-0-quantity": 1, + "products-0-amount": 4200, + "products-1-designation": "Second designation", + "products-1-quantity": 5, + "products-1-amount": -1800, + "products-TOTAL_FORMS": 2, + "products-INITIAL_FORMS": 0, + "products-MIN_NUM_FORMS": 0, + "products-MAX_NUM_FORMS": 1000, + } + + response = self.client.post(reverse("treasury:invoice_update", args=(self.invoice.id,)), data=data) + self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200) + self.invoice.refresh_from_db() + self.assertTrue(Invoice.objects.filter(pk=1, object="Same object", locked=True).exists()) + self.assertTrue(Product.objects.filter(designation="Second designation", invoice_id=1).exists()) + + # Resend the same data, but the invoice is locked. + response = self.client.get(reverse("treasury:invoice_update", args=(self.invoice.id,))) + self.assertTrue(response.status_code, 200) + response = self.client.post(reverse("treasury:invoice_update", args=(self.invoice.id,)), data=data) + self.assertTrue(response.status_code, 200) + + def test_delete_invoice(self): + """ + Try to delete an invoice. + """ + response = self.client.get(reverse("treasury:invoice_delete", args=(self.invoice.id,))) + self.assertEqual(response.status_code, 200) + + # Can't delete a locked invoice + self.invoice.locked = True + self.invoice.save() + response = self.client.delete(reverse("treasury:invoice_delete", args=(self.invoice.id,))) + self.assertEqual(response.status_code, 403) + self.assertTrue(Invoice.objects.filter(pk=self.invoice.id).exists()) + + # Unlock invoice and truly delete it. + self.invoice.locked = False + self.invoice._force_save = True + self.invoice.save() + response = self.client.delete(reverse("treasury:invoice_delete", args=(self.invoice.id,))) + self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200) + self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists()) + + def test_invoice_render_pdf(self): + """ + Generate the PDF file of an invoice. + """ + response = self.client.get(reverse("treasury:invoice_render", args=(self.invoice.id,))) + self.assertEqual(response.status_code, 200) + + def test_invoice_api(self): + """ + Load some API pages + """ + response = self.client.get("/api/treasury/invoice/") + self.assertEqual(response.status_code, 200) + response = self.client.get("/api/treasury/product/") + self.assertEqual(response.status_code, 200) + + +class TestRemittances(TestCase): + """ + Create some credits and close remittances. + """ + + fixtures = ('initial',) + + def setUp(self) -> None: + self.user = User.objects.create_superuser( + username="admintoto", + password="totototo", + email="admin@example.com", + ) + self.client.force_login(self.user) + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + self.credit = SpecialTransaction.objects.create( + source=NoteSpecial.objects.get(special_type="Chèque"), + destination=self.user.note, + amount=4200, + reason="Credit", + last_name="TOTO", + first_name="Toto", + bank="Société générale", + ) + + self.second_credit = SpecialTransaction.objects.create( + source=self.user.note, + destination=NoteSpecial.objects.get(special_type="Chèque"), + amount=424200, + reason="Second credit", + last_name="TOTO", + first_name="Toto", + bank="Société générale", + ) + + self.remittance = Remittance.objects.create( + remittance_type=RemittanceType.objects.get(), + comment="Test remittance", + closed=False, + ) + self.credit.specialtransactionproxy.remittance = self.remittance + self.credit.specialtransactionproxy.save() + + def test_admin_page(self): + """ + Load the admin page. + """ + response = self.client.get(reverse("admin:index") + "treasury/remittance/") + self.assertEqual(response.status_code, 200) + + def test_remittances_list(self): + """ + Display the remittance list. + :return: + """ + response = self.client.get(reverse("treasury:remittance_list")) + self.assertEqual(response.status_code, 200) + + def test_remittance_create(self): + """ + Create a new Remittance. + """ + response = self.client.get(reverse("treasury:remittance_create")) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("treasury:remittance_create"), data=dict( + remittance_type=RemittanceType.objects.get().pk, + comment="Created remittance", + )) + self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200) + self.assertTrue(Remittance.objects.filter(comment="Created remittance").exists()) + + def test_remittance_update(self): + """ + Update an existing remittance. + """ + response = self.client.get(reverse("treasury:remittance_update", args=(self.remittance.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("treasury:remittance_update", args=(self.remittance.pk,)), data=dict( + comment="Updated remittance", + )) + self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200) + self.assertTrue(Remittance.objects.filter(comment="Updated remittance").exists()) + + def test_remittance_close(self): + """ + Try to close an open remittance. + """ + response = self.client.get(reverse("treasury:remittance_update", args=(self.remittance.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("treasury:remittance_update", args=(self.remittance.pk,)), data=dict( + comment="Closed remittance", + close=True, + )) + self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200) + self.assertTrue(Remittance.objects.filter(comment="Closed remittance", closed=True).exists()) + + def test_remittance_link_transaction(self): + """ + Link a transaction to an open remittance. + """ + response = self.client.get(reverse("treasury:link_transaction", args=(self.credit.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("treasury:link_transaction", args=(self.credit.pk,)), data=dict( + remittance=self.remittance.pk, + last_name="Last Name", + first_name="First Name", + bank="Bank", + )) + self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200) + self.credit.refresh_from_db() + self.assertEqual(self.credit.last_name, "Last Name") + self.assertEqual(self.remittance.transactions.count(), 1) + + response = self.client.get(reverse("treasury:unlink_transaction", args=(self.credit.pk,))) + self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200) + + def test_invoice_api(self): + """ + Load some API pages + """ + response = self.client.get("/api/treasury/remittance_type/") + self.assertEqual(response.status_code, 200) + response = self.client.get("/api/treasury/remittance/") + self.assertEqual(response.status_code, 200) + + +class TestSogeCredits(TestCase): + """ + Check that credits from the Société générale are working correctly. + """ + + fixtures = ('initial',) + + def setUp(self) -> None: + self.user = User.objects.create_superuser( + username="admintoto", + password="totototo", + email="admin@example.com", + ) + self.client.force_login(self.user) + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + self.kfet = Club.objects.get(name="Kfet") + self.bde = self.kfet.parent_club + + self.kfet_membership = Membership( + user=self.user, + club=self.kfet, + ) + self.kfet_membership._force_renew_parent = True + self.kfet_membership._soge = True + self.kfet_membership.save() + + def test_admin_page(self): + """ + Render the admin page. + """ + response = self.client.get(reverse("admin:index") + "treasury/sogecredit/") + self.assertEqual(response.status_code, 200) + + def test_sogecredit_list(self): + """ + Display the list of all credits. + """ + response = self.client.get(reverse("treasury:soge_credits")) + self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("treasury:soge_credits") + "?search=toto&valid=") + self.assertEqual(response.status_code, 200) + + def test_validate_soge_credit(self): + """ + Try to validate a credit. + """ + soge_credit = SogeCredit.objects.get(user=self.user) + + response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict( + validate=True, + )) + self.assertRedirects(response, reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), 302, 200) + soge_credit.refresh_from_db() + self.assertTrue(soge_credit.valid) + self.user.note.refresh_from_db() + self.assertEqual(self.user.note.balance, 0) + self.assertEqual( + Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3) + self.assertTrue(self.user.profile.soge) + + def test_delete_soge_credit(self): + """ + Try to invalidate a credit. + """ + soge_credit = SogeCredit.objects.get(user=self.user) + + response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,))) + self.assertEqual(response.status_code, 200) + + try: + self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True)) + raise AssertionError("It is not possible to delete the soge credit until the note is not credited.") + except ValidationError: + pass + + SpecialTransaction.objects.create( + source=NoteSpecial.objects.get(special_type="Carte bancaire"), + destination=self.user.note, + amount=self.bde.membership_fee_paid + self.kfet.membership_fee_paid, + quantity=1, + reason="Registration is not complete, pliz pay", + last_name="TOTO", + first_name="Toto", + ) + + response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), + data=dict(delete=True)) + # 403 because no SogeCredit exists anymore, then a PermissionDenied is raised + self.assertRedirects(response, reverse("treasury:soge_credits"), 302, 403) + self.assertFalse(SogeCredit.objects.filter(pk=soge_credit.pk)) + self.user.note.refresh_from_db() + self.assertEqual(self.user.note.balance, 0) + self.assertEqual( + Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3) + self.assertFalse(self.user.profile.soge) + + def test_invoice_api(self): + """ + Load some API pages + """ + response = self.client.get("/api/treasury/soge_credit/") + self.assertEqual(response.status_code, 200) diff --git a/apps/treasury/views.py b/apps/treasury/views.py index c2265289..5889f8b5 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -60,6 +60,11 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView): return context + def get_form(self, form_class=None): + form = super().get_form(form_class) + del form.fields["locked"] + return form + def form_valid(self, form): ret = super().form_valid(form) @@ -134,6 +139,11 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): return context + def get_form(self, form_class=None): + form = super().get_form(form_class) + del form.fields["id"] + return form + def form_valid(self, form): ret = super().form_valid(form) @@ -165,6 +175,11 @@ class InvoiceDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): model = Invoice extra_context = {"title": _("Delete invoice")} + def delete(self, request, *args, **kwargs): + if self.get_object().locked: + raise PermissionDenied(_("This invoice is locked and can't be deleted.")) + return super().delete(request, *args, **kwargs) + def get_success_url(self): return reverse_lazy('treasury:invoice_list') @@ -387,7 +402,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi if not request.user.is_authenticated: return self.handle_no_permission() - if not self.get_queryset().exists(): + if not super().get_queryset().exists(): raise PermissionDenied(_("You are not able to see the treasury interface.")) return super().dispatch(request, *args, **kwargs) diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 3145e2ad..e27135fa 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -3,302 +3,305 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-09-01 10:28+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"POT-Creation-Date: 2020-09-04 07:44+0200\n" +"PO-Revision-Date: 2020-09-03 23:47+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.3\n" #: apps/activity/apps.py:10 apps/activity/models.py:145 #: apps/activity/models.py:161 msgid "activity" -msgstr "" +msgstr "Veranstaltung" #: apps/activity/forms.py:35 apps/activity/models.py:130 msgid "The end date must be after the start date." -msgstr "" +msgstr "Das Abschlussdatum muss nach das Anfangsdatum sein." #: apps/activity/forms.py:76 apps/activity/models.py:257 msgid "You can't invite someone once the activity is started." msgstr "" +"Sie dürfen nicht jemandem einladen wenn die Veranstaltung angefangen hat." #: apps/activity/forms.py:79 apps/activity/models.py:260 msgid "This activity is not validated yet." -msgstr "" +msgstr "Diese Veranstaltung ist noch nicht bestätigt." #: apps/activity/forms.py:89 apps/activity/models.py:268 msgid "This person has been already invited 5 times this year." -msgstr "" +msgstr "Diese Person wurde schon 5 mal dieses Jahr eingeladen." #: apps/activity/forms.py:93 apps/activity/models.py:272 msgid "This person is already invited." -msgstr "" +msgstr "Diese Person wurde schon eingeladen." #: apps/activity/forms.py:97 apps/activity/models.py:276 msgid "You can't invite more than 3 people to this activity." -msgstr "" +msgstr "Sie dürfen höchstens 3 Leute zu dieser Veranstaltung einladen." #: apps/activity/models.py:27 apps/activity/models.py:62 -#: apps/member/models.py:198 +#: apps/member/models.py:200 #: apps/member/templates/member/includes/club_info.html:4 #: apps/member/templates/member/includes/profile_info.html:4 -#: apps/note/models/notes.py:250 apps/note/models/transactions.py:26 -#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:297 +#: apps/note/models/notes.py:247 apps/note/models/transactions.py:26 +#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:302 #: apps/permission/models.py:329 #: apps/registration/templates/registration/future_profile_detail.html:16 #: apps/wei/models.py:66 apps/wei/models.py:118 #: apps/wei/templates/wei/base.html:26 #: apps/wei/templates/wei/weimembership_form.html:14 msgid "name" -msgstr "" +msgstr "Name" #: apps/activity/models.py:32 msgid "manage entries" -msgstr "" +msgstr "Einträge verwalten" #: apps/activity/models.py:33 msgid "Enable the support of entries for this activity." -msgstr "" +msgstr "Aktivieren Sie die Eintrittmöglichkeit für diese Aktivität." #: apps/activity/models.py:38 #: apps/activity/templates/activity/includes/activity_info.html:42 msgid "can invite" -msgstr "" +msgstr "Darf einladen" #: apps/activity/models.py:43 #: apps/activity/templates/activity/includes/activity_info.html:46 msgid "guest entry fee" -msgstr "" +msgstr "Gasteintrittspreis" #: apps/activity/models.py:48 msgid "activity type" -msgstr "" +msgstr "Veranstaltungart" #: apps/activity/models.py:49 msgid "activity types" -msgstr "" +msgstr "Vearnstaltungarte" #: apps/activity/models.py:67 #: apps/activity/templates/activity/includes/activity_info.html:19 #: apps/note/models/transactions.py:81 apps/permission/models.py:110 #: apps/permission/models.py:189 apps/wei/models.py:72 apps/wei/models.py:129 msgid "description" -msgstr "" +msgstr "Beschreibung" #: apps/activity/models.py:71 msgid "location" -msgstr "" +msgstr "Ort" #: apps/activity/models.py:75 msgid "Place where the activity is organized, eg. Kfet." -msgstr "" +msgstr "Wo findet die Veranstaltung statt ? (z.B Kfet)" #: apps/activity/models.py:82 #: apps/activity/templates/activity/includes/activity_info.html:22 -#: apps/note/models/notes.py:226 apps/note/models/transactions.py:66 +#: apps/note/models/notes.py:223 apps/note/models/transactions.py:66 #: apps/permission/models.py:164 msgid "type" -msgstr "" +msgstr "Type" -#: apps/activity/models.py:88 apps/logs/models.py:22 apps/member/models.py:303 -#: apps/note/models/notes.py:142 apps/treasury/models.py:267 +#: apps/activity/models.py:88 apps/logs/models.py:22 apps/member/models.py:305 +#: apps/note/models/notes.py:138 apps/treasury/models.py:267 #: apps/treasury/templates/treasury/sogecredit_detail.html:14 #: apps/wei/models.py:160 apps/wei/templates/wei/survey.html:15 msgid "user" -msgstr "" +msgstr "User" #: apps/activity/models.py:95 #: apps/activity/templates/activity/includes/activity_info.html:36 msgid "organizer" -msgstr "" +msgstr "Veranstalter" #: apps/activity/models.py:96 msgid "Club that organizes the activity. The entry fees will go to this club." msgstr "" +"Die Veranstaltung wurde von einem Club organisert. Die Eintrittsbeiträge " +"gehen an diesen Club." #: apps/activity/models.py:103 #: apps/activity/templates/activity/includes/activity_info.html:39 msgid "attendees club" -msgstr "" +msgstr "Teilnehmer" #: apps/activity/models.py:104 msgid "Club that is authorized to join the activity. Mostly the Kfet club." -msgstr "" +msgstr "Club die an die Veranstaltung teilnehmen können." #: apps/activity/models.py:108 #: apps/activity/templates/activity/includes/activity_info.html:25 msgid "start date" -msgstr "" +msgstr "Anfangsdatum" #: apps/activity/models.py:112 #: apps/activity/templates/activity/includes/activity_info.html:28 msgid "end date" -msgstr "" +msgstr "Abschlussdatum" #: apps/activity/models.py:117 #: apps/activity/templates/activity/includes/activity_info.html:50 #: apps/note/models/transactions.py:146 msgid "valid" -msgstr "" +msgstr "gültig" #: apps/activity/models.py:122 #: apps/activity/templates/activity/includes/activity_info.html:65 msgid "open" -msgstr "" +msgstr "geöffnet" #: apps/activity/models.py:146 msgid "activities" -msgstr "" +msgstr "Veranstaltungen" #: apps/activity/models.py:166 msgid "entry time" -msgstr "" +msgstr "Eintrittzeit" #: apps/activity/models.py:172 apps/note/apps.py:14 -#: apps/note/models/notes.py:76 +#: apps/note/models/notes.py:78 msgid "note" -msgstr "" +msgstr "Note" #: apps/activity/models.py:183 #: apps/activity/templates/activity/activity_entry.html:46 msgid "entry" -msgstr "" +msgstr "Eintritt" #: apps/activity/models.py:184 #: apps/activity/templates/activity/activity_entry.html:46 msgid "entries" -msgstr "" +msgstr "Eintritte" #: apps/activity/models.py:190 msgid "Already entered on " -msgstr "" +msgstr "Schon eingetretten " #: apps/activity/models.py:190 apps/activity/tables.py:54 msgid "{:%Y-%m-%d %H:%M:%S}" -msgstr "" +msgstr "{:%Y-%m-%d %H:%M:%S}" #: apps/activity/models.py:198 msgid "The balance is negative." -msgstr "" +msgstr "Kontostand ist im Rot." #: apps/activity/models.py:228 msgid "last name" -msgstr "" +msgstr "Nachname" #: apps/activity/models.py:233 #: apps/member/templates/member/includes/profile_info.html:4 #: apps/registration/templates/registration/future_profile_detail.html:16 #: apps/wei/templates/wei/weimembership_form.html:14 msgid "first name" -msgstr "" +msgstr "Vorname" #: apps/activity/models.py:240 msgid "inviter" -msgstr "" +msgstr "Einlader" #: apps/activity/models.py:284 msgid "guest" -msgstr "" +msgstr "Gast" #: apps/activity/models.py:285 msgid "guests" -msgstr "" +msgstr "Gäste" #: apps/activity/models.py:297 msgid "Invitation" -msgstr "" +msgstr "Einladung" #: apps/activity/tables.py:25 msgid "The activity is currently open." -msgstr "" +msgstr "Die Veranstaltung ist geöffnet." #: apps/activity/tables.py:26 msgid "The validation of the activity is pending." -msgstr "" +msgstr "Diese Veranstaltung ist noch nicht bestätigt." #: apps/activity/tables.py:41 apps/treasury/tables.py:107 msgid "Remove" -msgstr "" +msgstr "Entfernen" #: apps/activity/tables.py:54 msgid "Entered on " -msgstr "" +msgstr "Eingetreten um " #: apps/activity/tables.py:56 msgid "remove" -msgstr "" +msgstr "entfernen" -#: apps/activity/tables.py:80 apps/note/forms.py:66 apps/treasury/models.py:186 +#: apps/activity/tables.py:80 apps/note/forms.py:68 apps/treasury/models.py:186 msgid "Type" -msgstr "" +msgstr "Type" #: apps/activity/tables.py:82 apps/member/forms.py:131 #: apps/registration/forms.py:81 apps/treasury/forms.py:135 #: apps/wei/forms/registration.py:96 msgid "Last name" -msgstr "" +msgstr "Nachname" #: apps/activity/tables.py:84 apps/member/forms.py:136 #: apps/note/templates/note/transaction_form.html:134 #: apps/registration/forms.py:86 apps/treasury/forms.py:137 #: apps/wei/forms/registration.py:101 msgid "First name" -msgstr "" +msgstr "Vorname" -#: apps/activity/tables.py:86 apps/note/models/notes.py:85 +#: apps/activity/tables.py:86 apps/note/models/notes.py:87 msgid "Note" -msgstr "" +msgstr "Note" #: apps/activity/tables.py:88 apps/member/tables.py:46 msgid "Balance" -msgstr "" +msgstr "Kontostand" #: apps/activity/templates/activity/activity_detail.html:15 msgid "Guests list" -msgstr "" +msgstr "Gastliste" #: apps/activity/templates/activity/activity_entry.html:14 #: apps/note/models/transactions.py:259 #: apps/note/templates/note/transaction_form.html:16 #: apps/note/templates/note/transaction_form.html:148 -#: note_kfet/templates/base.html:78 +#: note_kfet/templates/base.html:69 msgid "Transfer" -msgstr "" +msgstr "Überweisen" #: apps/activity/templates/activity/activity_entry.html:18 -#: apps/note/models/transactions.py:313 +#: apps/note/models/transactions.py:318 #: apps/note/templates/note/transaction_form.html:21 msgid "Credit" -msgstr "" +msgstr "Kredit" #: apps/activity/templates/activity/activity_entry.html:21 -#: apps/note/models/transactions.py:313 +#: apps/note/models/transactions.py:318 #: apps/note/templates/note/transaction_form.html:25 msgid "Debit" -msgstr "" +msgstr "Soll" #: apps/activity/templates/activity/activity_entry.html:27 #: apps/note/templates/note/transaction_form.html:30 msgid "Entries" -msgstr "" +msgstr "Eintritte" #: apps/activity/templates/activity/activity_entry.html:37 msgid "Return to activity page" -msgstr "" +msgstr "Zurück zur Veranstaltungseite" #: apps/activity/templates/activity/activity_form.html:16 #: apps/member/templates/member/add_members.html:32 #: apps/member/templates/member/club_form.html:16 -#: apps/note/templates/note/transactiontemplate_form.html:15 +#: apps/note/templates/note/transactiontemplate_form.html:18 #: apps/treasury/forms.py:93 apps/treasury/forms.py:147 #: apps/treasury/templates/treasury/invoice_form.html:74 #: apps/wei/templates/wei/bus_form.html:17 @@ -306,503 +309,512 @@ msgstr "" #: apps/wei/templates/wei/weiclub_form.html:17 #: apps/wei/templates/wei/weiregistration_form.html:18 msgid "Submit" -msgstr "" +msgstr "Vorlegen" #: apps/activity/templates/activity/activity_list.html:12 msgid "Current activity" -msgstr "" +msgstr "Aktuelle Veranstaltung" #: apps/activity/templates/activity/activity_list.html:24 msgid "Upcoming activities" -msgstr "" +msgstr "Zukünftige Veranstaltungen" #: apps/activity/templates/activity/activity_list.html:31 msgid "There is no planned activity." -msgstr "" +msgstr "Es gibt keine geplante Veranstaltung." #: apps/activity/templates/activity/activity_list.html:38 msgid "New activity" -msgstr "" +msgstr "Neue Veranstaltung" #: apps/activity/templates/activity/activity_list.html:45 msgid "All activities" -msgstr "" +msgstr "Alle Veranstaltungen" #: apps/activity/templates/activity/includes/activity_info.html:32 msgid "creater" -msgstr "" +msgstr "Verantworter" #: apps/activity/templates/activity/includes/activity_info.html:53 msgid "opened" -msgstr "" +msgstr "geöffnet" #: apps/activity/templates/activity/includes/activity_info.html:60 msgid "Entry page" -msgstr "" +msgstr "Eintrittseite" #: apps/activity/templates/activity/includes/activity_info.html:65 msgid "close" -msgstr "" +msgstr "Schlusss" #: apps/activity/templates/activity/includes/activity_info.html:68 msgid "invalidate" -msgstr "" +msgstr "invalidate" #: apps/activity/templates/activity/includes/activity_info.html:68 msgid "validate" -msgstr "" +msgstr "validate" #: apps/activity/templates/activity/includes/activity_info.html:71 -#: apps/logs/models.py:62 apps/note/tables.py:169 +#: apps/logs/models.py:62 apps/note/tables.py:194 msgid "edit" -msgstr "" +msgstr "bearbeiten" #: apps/activity/templates/activity/includes/activity_info.html:74 msgid "Invite" -msgstr "" +msgstr "Einladen" #: apps/activity/views.py:29 msgid "Create new activity" -msgstr "" +msgstr "Neue Veranstaltung schaffen" -#: apps/activity/views.py:59 note_kfet/templates/base.html:96 +#: apps/activity/views.py:59 note_kfet/templates/base.html:87 msgid "Activities" -msgstr "" +msgstr "Veranstaltungen" #: apps/activity/views.py:87 msgid "Activity detail" -msgstr "" +msgstr "Veranstaltunginfo" #: apps/activity/views.py:107 msgid "Update activity" -msgstr "" +msgstr "Veranstaltung bearbeiten" #: apps/activity/views.py:134 msgid "Invite guest to the activity \"{}\"" -msgstr "" +msgstr "Gast zur Veranstaltung \"{}\" einladen" #: apps/activity/views.py:168 msgid "You are not allowed to display the entry interface for this activity." -msgstr "" +msgstr "Sie haben nicht das Recht diese Seite zu benuzten." #: apps/activity/views.py:171 msgid "This activity does not support activity entries." -msgstr "" +msgstr "Diese Veranstaltung braucht nicht Eintritt." #: apps/activity/views.py:174 msgid "This activity is closed." -msgstr "" +msgstr "Diese Veranstaltung ist geschlossen." #: apps/activity/views.py:272 msgid "Entry for activity \"{}\"" -msgstr "" +msgstr "Eintritt zur Veranstaltung \"{}\"" #: apps/api/apps.py:10 msgid "API" -msgstr "" +msgstr "API" #: apps/logs/apps.py:11 msgid "Logs" -msgstr "" +msgstr "Logs" #: apps/logs/models.py:28 msgid "IP Address" -msgstr "" +msgstr "IP Adresse" #: apps/logs/models.py:36 apps/permission/models.py:134 msgid "model" -msgstr "" +msgstr "Model" #: apps/logs/models.py:43 msgid "identifier" -msgstr "" +msgstr "Kennzeichnung" #: apps/logs/models.py:48 msgid "previous data" -msgstr "" +msgstr "ehemalige Daten" #: apps/logs/models.py:53 msgid "new data" -msgstr "" +msgstr "neue Daten" #: apps/logs/models.py:61 msgid "create" -msgstr "" +msgstr "schaffen" -#: apps/logs/models.py:63 apps/note/tables.py:139 apps/note/tables.py:175 +#: apps/logs/models.py:63 apps/note/tables.py:164 apps/note/tables.py:200 #: apps/permission/models.py:127 apps/treasury/tables.py:38 #: apps/wei/tables.py:75 msgid "delete" -msgstr "" +msgstr "entfernen" #: apps/logs/models.py:66 msgid "action" -msgstr "" +msgstr "Aktion" #: apps/logs/models.py:74 msgid "timestamp" -msgstr "" +msgstr "Zeitstempel" #: apps/logs/models.py:78 msgid "Logs cannot be destroyed." -msgstr "" +msgstr "Logs können nicht entfernen sein." #: apps/logs/models.py:81 msgid "changelog" -msgstr "" +msgstr "Changelog" #: apps/logs/models.py:82 msgid "changelogs" -msgstr "" +msgstr "Changelogs" -#: apps/member/admin.py:52 apps/member/models.py:225 +#: apps/member/admin.py:50 apps/member/models.py:227 #: apps/member/templates/member/includes/club_info.html:34 msgid "membership fee (paid students)" -msgstr "" +msgstr "Mitgliedschaftpreis (bezahlte Studenten)" -#: apps/member/admin.py:53 apps/member/models.py:230 +#: apps/member/admin.py:51 apps/member/models.py:232 #: apps/member/templates/member/includes/club_info.html:37 msgid "membership fee (unpaid students)" -msgstr "" +msgstr "Mitgliedschaftpreis (unbezahlte Studenten)" -#: apps/member/admin.py:67 apps/member/models.py:314 +#: apps/member/admin.py:65 apps/member/models.py:316 msgid "roles" -msgstr "" +msgstr "Rollen" -#: apps/member/admin.py:68 apps/member/models.py:328 +#: apps/member/admin.py:66 apps/member/models.py:330 msgid "fee" -msgstr "" +msgstr "Preis" #: apps/member/apps.py:14 apps/wei/tables.py:181 apps/wei/tables.py:212 msgid "member" -msgstr "" +msgstr "Mitglied" #: apps/member/forms.py:41 msgid "Report frequency" -msgstr "" +msgstr "Bericht Frequenz" #: apps/member/forms.py:43 msgid "Last report date" -msgstr "" +msgstr "Letzen Bericht Datum" #: apps/member/forms.py:48 msgid "You can't register to the note if you come from the future." -msgstr "" +msgstr "Sie dürfen nicht einloggen wenn sie aus der Zukunft kommen." #: apps/member/forms.py:73 msgid "select an image" -msgstr "" +msgstr "Wählen sie ein Bild aus" #: apps/member/forms.py:74 msgid "Maximal size: 2MB" -msgstr "" +msgstr "Maximal Größe: 2MB" -#: apps/member/forms.py:87 apps/member/views.py:101 -#: apps/registration/forms.py:33 +#: apps/member/forms.py:87 apps/member/views.py:100 +#: apps/registration/forms.py:33 apps/registration/views.py:237 msgid "An alias with a similar name already exists." -msgstr "" +msgstr "Ein ähnliches Alias ist schon benutzt." #: apps/member/forms.py:110 apps/registration/forms.py:61 msgid "Inscription paid by Société Générale" -msgstr "" +msgstr "Mitgliedschaft von der Société Générale bezahlt" #: apps/member/forms.py:112 apps/registration/forms.py:63 msgid "Check this case is the Société Générale paid the inscription." -msgstr "" +msgstr "Die Société Générale die Mitgliedschaft bezahlt." #: apps/member/forms.py:117 apps/registration/forms.py:68 #: apps/wei/forms/registration.py:83 msgid "Credit type" -msgstr "" +msgstr "Kredittype" #: apps/member/forms.py:118 apps/registration/forms.py:69 #: apps/wei/forms/registration.py:84 msgid "No credit" -msgstr "" +msgstr "Kein Kredit" #: apps/member/forms.py:120 msgid "You can credit the note of the user." -msgstr "" +msgstr "Sie dûrfen diese Note kreditieren." #: apps/member/forms.py:124 apps/registration/forms.py:74 #: apps/wei/forms/registration.py:89 msgid "Credit amount" -msgstr "" +msgstr "Kreditanzahl" #: apps/member/forms.py:141 apps/note/templates/note/transaction_form.html:140 #: apps/registration/forms.py:91 apps/treasury/forms.py:139 #: apps/wei/forms/registration.py:106 msgid "Bank" -msgstr "" +msgstr "Bank" #: apps/member/forms.py:168 msgid "User" -msgstr "" +msgstr "User" #: apps/member/forms.py:182 msgid "Roles" -msgstr "" +msgstr "Rollen" #: apps/member/models.py:38 #: apps/member/templates/member/includes/profile_info.html:34 #: apps/registration/templates/registration/future_profile_detail.html:40 #: apps/wei/templates/wei/weimembership_form.html:44 msgid "phone number" -msgstr "" +msgstr "Telefonnummer" #: apps/member/models.py:45 #: apps/member/templates/member/includes/profile_info.html:28 #: apps/registration/templates/registration/future_profile_detail.html:34 #: apps/wei/templates/wei/weimembership_form.html:38 msgid "section" -msgstr "" +msgstr "Section" #: apps/member/models.py:46 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -msgstr "" +msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" #: apps/member/models.py:54 apps/wei/templates/wei/weimembership_form.html:32 msgid "department" -msgstr "" +msgstr "Fachabteilung" #: apps/member/models.py:56 msgid "Informatics (A0)" -msgstr "" +msgstr "Informatik (A0)" #: apps/member/models.py:57 msgid "Mathematics (A1)" -msgstr "" +msgstr "Mathematik (A1)" #: apps/member/models.py:58 msgid "Physics (A2)" -msgstr "" +msgstr "Physik (A2)" #: apps/member/models.py:59 msgid "Applied physics (A'2)" -msgstr "" +msgstr "Angewandte Physik (A'2)" #: apps/member/models.py:60 msgid "Chemistry (A''2)" -msgstr "" +msgstr "Chemie (A''2)" #: apps/member/models.py:61 msgid "Biology (A3)" -msgstr "" +msgstr "Biologie (A3)" #: apps/member/models.py:62 msgid "SAPHIRE (B1234)" -msgstr "" +msgstr "SAPHIRE (B1234)" #: apps/member/models.py:63 msgid "Mechanics (B1)" -msgstr "" +msgstr "Mechanik (B1)" #: apps/member/models.py:64 msgid "Civil engineering (B2)" -msgstr "" +msgstr "Bauingenieur (B2)" #: apps/member/models.py:65 msgid "Mechanical engineering (B3)" -msgstr "" +msgstr "Mechanikingenieur (B3)" #: apps/member/models.py:66 msgid "EEA (B4)" -msgstr "" +msgstr "Electrotechnik (B4)" #: apps/member/models.py:67 msgid "Design (C)" -msgstr "" +msgstr "Design (C)" #: apps/member/models.py:68 msgid "Economy-management (D2)" -msgstr "" +msgstr "Wirtschaftingenieure (D2)" #: apps/member/models.py:69 msgid "Social sciences (D3)" -msgstr "" +msgstr "Sozialwissenschaften (D3)" #: apps/member/models.py:70 msgid "English (E)" -msgstr "" +msgstr "English (E)" #: apps/member/models.py:71 msgid "External (EXT)" -msgstr "" +msgstr "Extern (EXT)" #: apps/member/models.py:78 msgid "promotion" -msgstr "" +msgstr "Promotion" #: apps/member/models.py:79 msgid "Year of entry to the school (None if not ENS student)" -msgstr "" +msgstr "ENS Eintrittjahr (None wenn kein ENS Student)" #: apps/member/models.py:83 #: apps/member/templates/member/includes/profile_info.html:38 #: apps/registration/templates/registration/future_profile_detail.html:37 #: apps/wei/templates/wei/weimembership_form.html:41 msgid "address" -msgstr "" +msgstr "Adresse" #: apps/member/models.py:90 #: apps/member/templates/member/includes/profile_info.html:45 #: apps/registration/templates/registration/future_profile_detail.html:43 #: apps/wei/templates/wei/weimembership_form.html:47 msgid "paid" -msgstr "" +msgstr "bezahlt" #: apps/member/models.py:91 msgid "Tells if the user receive a salary." -msgstr "" +msgstr "User ist bezahlt." #: apps/member/models.py:101 apps/treasury/tables.py:146 msgid "No" -msgstr "" +msgstr "Nein" #: apps/member/models.py:102 msgid "Yes (receive them in french)" -msgstr "" +msgstr "Ja (auf Fränzosich)" #: apps/member/models.py:103 msgid "Yes (receive them in english)" -msgstr "" +msgstr "Ja (auf English)" #: apps/member/models.py:105 msgid "" "Register on the mailing list to stay informed of the events of the campus (1 " "mail/week)" msgstr "" +"Melden Sie sich auf der Mailingliste an, um über die Ereignisse des Campus " +"informiert zu bleiben (1 Mail / Woche)" #: apps/member/models.py:110 msgid "" "Register on the mailing list to stay informed of the sport events of the " "campus (1 mail/week)" msgstr "" +"Melden Sie sich auf der Mailingliste an, um über die Sportereignisse des " +"Campus informiert zu bleiben (1 Mail / Woche)" #: apps/member/models.py:115 msgid "" "Register on the mailing list to stay informed of the art events of the " "campus (1 mail/week)" msgstr "" +"Melden Sie sich auf der Mailingliste an, um über die Kunstereignisse des " +"Campus informiert zu bleiben (1 Mail / Woche)" #: apps/member/models.py:119 msgid "report frequency (in days)" -msgstr "" +msgstr "Bericht Frequenz (Tagen)" #: apps/member/models.py:124 msgid "last report date" -msgstr "" +msgstr "letzen Bericht Datum" #: apps/member/models.py:129 msgid "email confirmed" -msgstr "" +msgstr "email bestätigt" #: apps/member/models.py:134 msgid "registration valid" -msgstr "" +msgstr "Anmeldung gültig" #: apps/member/models.py:163 apps/member/models.py:164 msgid "user profile" -msgstr "" +msgstr "Userprofile" #: apps/member/models.py:174 msgid "Activate your Note Kfet account" -msgstr "" +msgstr "Ihre Note Kfet Konto bestätigen" -#: apps/member/models.py:203 +#: apps/member/models.py:205 #: apps/member/templates/member/includes/club_info.html:55 #: apps/member/templates/member/includes/profile_info.html:31 #: apps/registration/templates/registration/future_profile_detail.html:22 #: apps/wei/templates/wei/base.html:70 #: apps/wei/templates/wei/weimembership_form.html:20 msgid "email" -msgstr "" +msgstr "Email" -#: apps/member/models.py:210 +#: apps/member/models.py:212 msgid "parent club" -msgstr "" +msgstr "Urclub" -#: apps/member/models.py:219 +#: apps/member/models.py:221 msgid "require memberships" -msgstr "" +msgstr "erfordern Mitgliedschaft" -#: apps/member/models.py:220 +#: apps/member/models.py:222 msgid "Uncheck if this club don't require memberships." msgstr "" +"Deaktivieren Sie diese Option, wenn für diesen Club keine Mitgliedschaft " +"erforderlich ist." -#: apps/member/models.py:236 +#: apps/member/models.py:238 #: apps/member/templates/member/includes/club_info.html:26 msgid "membership duration" -msgstr "" +msgstr "Mitgliedscahftzeit" -#: apps/member/models.py:237 +#: apps/member/models.py:239 msgid "The longest time (in days) a membership can last (NULL = infinite)." -msgstr "" +msgstr "Wie lang am höchsten eine Mitgliedschaft dauern kann." -#: apps/member/models.py:244 +#: apps/member/models.py:246 #: apps/member/templates/member/includes/club_info.html:16 msgid "membership start" -msgstr "" +msgstr "Mitgliedschaftanfangsdatum" -#: apps/member/models.py:245 +#: apps/member/models.py:247 msgid "Date from which the members can renew their membership." -msgstr "" +msgstr "Ab wann kann man sein Mitgliedschaft erneuern." -#: apps/member/models.py:251 +#: apps/member/models.py:253 #: apps/member/templates/member/includes/club_info.html:21 msgid "membership end" -msgstr "" +msgstr "Mitgliedschaftenddatum" -#: apps/member/models.py:252 +#: apps/member/models.py:254 msgid "Maximal date of a membership, after which members must renew it." msgstr "" +"Maximales Datum einer Mitgliedschaft, nach dem Mitglieder es erneuern müssen." -#: apps/member/models.py:284 apps/member/models.py:309 -#: apps/note/models/notes.py:183 +#: apps/member/models.py:286 apps/member/models.py:311 +#: apps/note/models/notes.py:179 msgid "club" -msgstr "" +msgstr "Club" -#: apps/member/models.py:285 +#: apps/member/models.py:287 msgid "clubs" -msgstr "" +msgstr "Clubs" -#: apps/member/models.py:319 +#: apps/member/models.py:321 msgid "membership starts on" -msgstr "" +msgstr "Mitgliedschaft fängt an" -#: apps/member/models.py:323 +#: apps/member/models.py:325 msgid "membership ends on" -msgstr "" +msgstr "Mitgliedschaft endet am" -#: apps/member/models.py:374 +#: apps/member/models.py:375 #, python-brace-format msgid "The role {role} does not apply to the club {club}." -msgstr "" +msgstr "Die Rolle {role} ist nicht erlaubt für das Club {club}." -#: apps/member/models.py:385 apps/member/views.py:676 +#: apps/member/models.py:384 apps/member/views.py:669 msgid "User is already a member of the club" -msgstr "" +msgstr "User ist schon ein Mitglied dieser club" -#: apps/member/models.py:433 +#: apps/member/models.py:432 msgid "User is not a member of the parent club" -msgstr "" +msgstr "User ist noch nicht Mitglied des Urclubs" -#: apps/member/models.py:486 +#: apps/member/models.py:480 #, python-brace-format msgid "Membership of {user} for the club {club}" -msgstr "" +msgstr "Mitgliedschaft von {user} für das Club {club}" -#: apps/member/models.py:489 +#: apps/member/models.py:483 apps/note/models/transactions.py:359 msgid "membership" -msgstr "" +msgstr "Mitgliedschaft" -#: apps/member/models.py:490 +#: apps/member/models.py:484 msgid "memberships" -msgstr "" +msgstr "Mitgliedschaften" #: apps/member/tables.py:121 msgid "Renew" -msgstr "" +msgstr "Erneuern" #: apps/member/templates/member/add_members.html:16 #, python-format @@ -811,6 +823,9 @@ msgid "" "%(pretty_fee)s will be charged to renew automatically the membership in this/" "these club·s." msgstr "" +"Dieser User ist noch nicht Mitglied von den Urclub %(clubs)s. Ein extra " +"Beitrag von %(pretty_fee)s wurde bezahlt um die Mitgliedschaft von dieser/" +"diesen Club zu erneuern." #: apps/member/templates/member/add_members.html:21 #, python-format @@ -818,477 +833,498 @@ msgid "" "This club has parents %(clubs)s. An additional fee of %(pretty_fee)s will be " "charged to adhere automatically to this/these club·s." msgstr "" +"Dieses Club hat %(clubs)s als Urclub. Eine extra Beitrag von %(pretty_fee)s " +"wurde bezahlt um Mitglied von dieser/diesen Club zu werden." #: apps/member/templates/member/base.html:17 #: apps/registration/templates/registration/future_profile_detail.html:12 msgid "Account #" -msgstr "" +msgstr "Konto #" #: apps/member/templates/member/base.html:48 #: apps/member/templates/member/base.html:62 apps/member/views.py:59 #: apps/registration/templates/registration/future_profile_detail.html:48 #: apps/wei/templates/wei/weimembership_form.html:117 msgid "Update Profile" -msgstr "" +msgstr "Profile bearbeiten" #: apps/member/templates/member/base.html:52 #: apps/member/templates/member/base.html:67 msgid "View Profile" -msgstr "" +msgstr "Profile schauen" #: apps/member/templates/member/base.html:57 msgid "Add member" -msgstr "" +msgstr "Neue Mitglied" #: apps/member/templates/member/base.html:72 #: apps/member/templates/member/base.html:93 #: apps/member/templates/member/base.html:114 msgid "Lock note" -msgstr "" +msgstr "Note zuschliessen" #: apps/member/templates/member/base.html:76 #: apps/member/templates/member/base.html:126 #: apps/member/templates/member/base.html:138 msgid "Unlock note" -msgstr "" +msgstr "Note aufschliessen" #: apps/member/templates/member/base.html:99 msgid "" "Are you sure you want to lock this note? This will prevent any transaction " "that would be performed, until the note is unlocked." msgstr "" +"Seit Ihr sicher diese Note zuschliessen ? Keine Transaktion mit dieser Note " +"wurde erlaubt bis der Aufschluss." #: apps/member/templates/member/base.html:104 msgid "" "If you use the force mode, the user won't be able to unlock the note by " "itself." msgstr "" +"Wenn sie das stark mode benutzen, Der User wurde nicht erlaubt sich selbzt " +"zu aufschliessen." #: apps/member/templates/member/base.html:110 #: apps/member/templates/member/base.html:137 apps/treasury/forms.py:95 msgid "Close" -msgstr "" +msgstr "Schluss" #: apps/member/templates/member/base.html:112 msgid "Force mode" -msgstr "" +msgstr "Stark mode" #: apps/member/templates/member/base.html:132 msgid "" "Are you sure you want to unlock this note? Transactions will be re-enabled." msgstr "" +"Seit ihr sicher diese Note aufzuschliessen ? Transaktionen wurden wieder " +"erlaubt." #: apps/member/templates/member/club_alias.html:10 -#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:240 -#: apps/member/views.py:450 +#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:238 +#: apps/member/views.py:443 msgid "Note aliases" -msgstr "" +msgstr "Note Aliases" #: apps/member/templates/member/club_alias.html:20 #: apps/member/templates/member/profile_alias.html:19 #: apps/treasury/tables.py:99 msgid "Add" -msgstr "" +msgstr "Neue" #: apps/member/templates/member/club_detail.html:13 #: apps/permission/templates/permission/all_rights.html:32 msgid "Club managers" -msgstr "" +msgstr "Clubmanager" #: apps/member/templates/member/club_detail.html:26 msgid "Club members" -msgstr "" +msgstr "Mitglied" #: apps/member/templates/member/club_detail.html:40 #: apps/member/templates/member/profile_detail.html:32 #: apps/wei/templates/wei/weiclub_detail.html:75 msgid "Transaction history" -msgstr "" +msgstr "Transaktionvergangenheit" #: apps/member/templates/member/club_list.html:10 msgid "Create club" -msgstr "" +msgstr "Neue Club" #: apps/member/templates/member/club_members.html:19 msgid "Display only active memberships" -msgstr "" +msgstr "Schau nur aktive Mitgliedschaften" #: apps/member/templates/member/club_members.html:23 msgid "Filter roles:" -msgstr "" +msgstr "Rollen filter:" #: apps/member/templates/member/club_members.html:36 #: apps/wei/templates/wei/weimembership_list.html:17 msgid "There is no membership found with this pattern." -msgstr "" +msgstr "Keine Mitgliedschaft mit diesem pattern gefunden." #: apps/member/templates/member/includes/club_info.html:9 msgid "Club Parent" -msgstr "" +msgstr "Urclub" #: apps/member/templates/member/includes/club_info.html:27 msgid "days" -msgstr "" +msgstr "Tagen" #: apps/member/templates/member/includes/club_info.html:31 #: apps/wei/templates/wei/base.html:40 msgid "membership fee" -msgstr "" +msgstr "Mitgliedsachftpreis" #: apps/member/templates/member/includes/club_info.html:43 #: apps/member/templates/member/includes/profile_info.html:42 #: apps/treasury/templates/treasury/sogecredit_detail.html:18 #: apps/wei/templates/wei/base.html:60 msgid "balance" -msgstr "" +msgstr "Kontostand" #: apps/member/templates/member/includes/club_info.html:47 #: apps/member/templates/member/includes/profile_info.html:20 -#: apps/note/models/notes.py:273 apps/wei/templates/wei/base.html:66 +#: apps/note/models/notes.py:270 apps/wei/templates/wei/base.html:66 msgid "aliases" -msgstr "" +msgstr "Aliases" #: apps/member/templates/member/includes/club_info.html:51 #: apps/member/templates/member/includes/profile_info.html:24 msgid "Manage aliases" -msgstr "" +msgstr "Aliases bearbeiten" #: apps/member/templates/member/includes/profile_info.html:7 #: apps/registration/templates/registration/future_profile_detail.html:19 #: apps/wei/templates/wei/weimembership_form.html:17 msgid "username" -msgstr "" +msgstr "Username" #: apps/member/templates/member/includes/profile_info.html:11 msgid "password" -msgstr "" +msgstr "Kennwort" #: apps/member/templates/member/includes/profile_info.html:15 msgid "Change password" -msgstr "" +msgstr "Kennword ändern" -#: apps/member/templates/member/includes/profile_info.html:52 -#: apps/member/views.py:318 -msgid "Manage auth token" -msgstr "" +#: apps/member/templates/member/includes/profile_info.html:53 +msgid "API token" +msgstr "API token" #: apps/member/templates/member/manage_auth_tokens.html:19 msgid "Token" -msgstr "" +msgstr "Token" #: apps/member/templates/member/manage_auth_tokens.html:26 msgid "Created" -msgstr "" +msgstr "Neue" #: apps/member/templates/member/manage_auth_tokens.html:34 msgid "Regenerate token" -msgstr "" +msgstr "Token erneuern" #: apps/member/templates/member/profile_detail.html:11 #: apps/registration/templates/registration/future_profile_detail.html:28 #: apps/wei/templates/wei/weimembership_form.html:26 msgid "This user doesn't have confirmed his/her e-mail address." -msgstr "" +msgstr "Dieser User hat noch nicht sein email bestätigt." #: apps/member/templates/member/profile_detail.html:13 #: apps/registration/templates/registration/future_profile_detail.html:29 #: apps/wei/templates/wei/weimembership_form.html:27 msgid "Click here to resend a validation link." -msgstr "" +msgstr "Click hier um eine Bestätigunglinke zu schicken." #: apps/member/templates/member/profile_detail.html:21 msgid "View my memberships" -msgstr "" +msgstr "Meine Mitgliedschaften schauen" #: apps/member/templates/member/profile_update.html:18 msgid "Save Changes" -msgstr "" +msgstr "Speichern" #: apps/member/templates/member/user_list.html:10 msgid "Registrations" -msgstr "" +msgstr "Anmeldung" #: apps/member/views.py:72 apps/registration/forms.py:23 msgid "This address must be valid." -msgstr "" +msgstr "Diese Adresse muss gültig sein." -#: apps/member/views.py:138 +#: apps/member/views.py:137 msgid "Profile detail" -msgstr "" +msgstr "Profile detail" -#: apps/member/views.py:201 +#: apps/member/views.py:197 msgid "Search user" -msgstr "" +msgstr "User finden" -#: apps/member/views.py:260 +#: apps/member/views.py:258 msgid "Update note picture" -msgstr "" +msgstr "Notebild ändern" -#: apps/member/views.py:346 +#: apps/member/views.py:311 +msgid "Manage auth token" +msgstr "Auth token bearbeiten" + +#: apps/member/views.py:338 msgid "Create new club" -msgstr "" +msgstr "Neue Club" -#: apps/member/views.py:364 +#: apps/member/views.py:357 msgid "Search club" -msgstr "" +msgstr "Club finden" -#: apps/member/views.py:397 +#: apps/member/views.py:390 msgid "Club detail" -msgstr "" +msgstr "Club Details" -#: apps/member/views.py:473 +#: apps/member/views.py:466 msgid "Update club" -msgstr "" +msgstr "Club bearbeiten" -#: apps/member/views.py:507 +#: apps/member/views.py:500 msgid "Add new member to the club" -msgstr "" +msgstr "Neue Mitglieder" -#: apps/member/views.py:667 apps/wei/views.py:922 +#: apps/member/views.py:660 apps/wei/views.py:922 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." msgstr "" +"Diese User hat nicht genug Geld um Mitglied zu werden, und darf nich im Rot " +"sein." -#: apps/member/views.py:680 +#: apps/member/views.py:673 msgid "The membership must start after {:%m-%d-%Y}." -msgstr "" +msgstr "Die Mitgliedschaft muss nach {:%m-%d-Y} anfängen." -#: apps/member/views.py:685 +#: apps/member/views.py:678 msgid "The membership must begin before {:%m-%d-%Y}." -msgstr "" +msgstr "Die Mitgliedschaft muss vor {:%m-%d-Y} anfängen." -#: apps/member/views.py:701 apps/member/views.py:703 apps/member/views.py:705 -#: apps/registration/views.py:292 apps/registration/views.py:294 -#: apps/registration/views.py:296 apps/wei/views.py:927 apps/wei/views.py:931 +#: apps/member/views.py:694 apps/member/views.py:696 apps/member/views.py:698 +#: apps/registration/views.py:287 apps/registration/views.py:289 +#: apps/registration/views.py:291 apps/wei/views.py:927 apps/wei/views.py:931 msgid "This field is required." -msgstr "" +msgstr "Dies ist ein Pflichtfeld." -#: apps/member/views.py:789 +#: apps/member/views.py:771 msgid "Manage roles of an user in the club" -msgstr "" +msgstr "Rollen in diesen Club bearbeiten" -#: apps/member/views.py:814 +#: apps/member/views.py:796 msgid "Members of the club" -msgstr "" +msgstr "Mitlglieder dieses Club" -#: apps/note/admin.py:133 apps/note/models/transactions.py:106 +#: apps/note/admin.py:129 apps/note/models/transactions.py:106 msgid "source" -msgstr "" +msgstr "Sender" -#: apps/note/admin.py:141 apps/note/admin.py:191 +#: apps/note/admin.py:137 apps/note/admin.py:205 #: apps/note/models/transactions.py:56 apps/note/models/transactions.py:119 msgid "destination" -msgstr "" +msgstr "Empfänger" -#: apps/note/admin.py:196 apps/note/models/transactions.py:60 +#: apps/note/admin.py:210 apps/note/models/transactions.py:60 #: apps/note/models/transactions.py:137 msgid "amount" -msgstr "" +msgstr "Anzahl" -#: apps/note/api/serializers.py:177 apps/note/api/serializers.py:183 +#: apps/note/api/serializers.py:178 apps/note/api/serializers.py:184 #: apps/note/models/transactions.py:224 msgid "" "The transaction can't be saved since the source note or the destination note " "is not active." msgstr "" +"Diese Transaktion ist nicht möglich weil die Note des Sender oder des " +"Empfänger inaktiv ist." -#: apps/note/forms.py:37 +#: apps/note/forms.py:39 msgid "Source" -msgstr "" +msgstr "Sender" -#: apps/note/forms.py:51 +#: apps/note/forms.py:53 msgid "Destination" -msgstr "" +msgstr "Empfänger" -#: apps/note/forms.py:72 apps/note/templates/note/transaction_form.html:119 +#: apps/note/forms.py:74 apps/note/templates/note/transaction_form.html:119 msgid "Reason" -msgstr "" +msgstr "Grund" -#: apps/note/forms.py:77 apps/treasury/tables.py:139 +#: apps/note/forms.py:79 apps/treasury/tables.py:139 msgid "Valid" -msgstr "" +msgstr "Gültig" -#: apps/note/forms.py:83 +#: apps/note/forms.py:85 msgid "Total amount greater than" -msgstr "" +msgstr "Totalanzahl größer als" -#: apps/note/forms.py:91 +#: apps/note/forms.py:93 msgid "Total amount less than" -msgstr "" +msgstr "Totalanzahl kleiner als" -#: apps/note/forms.py:97 +#: apps/note/forms.py:99 msgid "Created after" -msgstr "" +msgstr "Erschafft nacht" -#: apps/note/forms.py:104 +#: apps/note/forms.py:106 msgid "Created before" -msgstr "" +msgstr "Erschafft vor" -#: apps/note/models/notes.py:30 +#: apps/note/models/notes.py:32 msgid "account balance" -msgstr "" +msgstr "Kontostand" -#: apps/note/models/notes.py:31 +#: apps/note/models/notes.py:33 msgid "in centimes, money credited for this instance" -msgstr "" +msgstr "In Cent, der Anzahl Geld für diese Konto" -#: apps/note/models/notes.py:36 +#: apps/note/models/notes.py:38 msgid "last negative date" -msgstr "" +msgstr "letztes mal im Rot" -#: apps/note/models/notes.py:37 +#: apps/note/models/notes.py:39 msgid "last time the balance was negative" -msgstr "" +msgstr "letztes mal im Rot" -#: apps/note/models/notes.py:43 +#: apps/note/models/notes.py:45 msgid "display image" -msgstr "" +msgstr "Bild" -#: apps/note/models/notes.py:52 apps/note/models/transactions.py:129 +#: apps/note/models/notes.py:54 apps/note/models/transactions.py:129 msgid "created at" -msgstr "" +msgstr "erschafft am" -#: apps/note/models/notes.py:57 +#: apps/note/models/notes.py:59 msgid "active" -msgstr "" +msgstr "Aktiv" -#: apps/note/models/notes.py:60 +#: apps/note/models/notes.py:62 msgid "" "Designates whether this note should be treated as active. Unselect this " "instead of deleting notes." msgstr "" +"Designates whether this note should be treated as active. Unselect this " +"instead of deleting notes." -#: apps/note/models/notes.py:67 +#: apps/note/models/notes.py:69 msgid "" "The user blocked his/her note manually, eg. when he/she left the school for " "holidays. It can be reactivated at any time." msgstr "" +"The user blocked his/her note manually, eg. when he/she left the school for " +"holidays. It can be reactivated at any time." -#: apps/note/models/notes.py:69 +#: apps/note/models/notes.py:71 msgid "The note is blocked by the the BDE and can't be manually reactivated." -msgstr "" +msgstr "The note is blocked by the the BDE and can't be manually reactivated." -#: apps/note/models/notes.py:77 +#: apps/note/models/notes.py:79 msgid "notes" -msgstr "" +msgstr "Notes" -#: apps/note/models/notes.py:102 apps/note/models/notes.py:126 +#: apps/note/models/notes.py:122 msgid "This alias is already taken." -msgstr "" +msgstr "Dieses Alias ist schon benutzt." -#: apps/note/models/notes.py:146 +#: apps/note/models/notes.py:142 msgid "one's note" -msgstr "" +msgstr "Jemand Note" -#: apps/note/models/notes.py:147 +#: apps/note/models/notes.py:143 msgid "users note" -msgstr "" +msgstr "User Note" -#: apps/note/models/notes.py:153 +#: apps/note/models/notes.py:149 #, python-format msgid "%(user)s's note" -msgstr "" +msgstr "%(user)s's note" -#: apps/note/models/notes.py:187 +#: apps/note/models/notes.py:183 msgid "club note" -msgstr "" +msgstr "Club Note" -#: apps/note/models/notes.py:188 +#: apps/note/models/notes.py:184 msgid "clubs notes" -msgstr "" +msgstr "Club Notes" -#: apps/note/models/notes.py:194 +#: apps/note/models/notes.py:190 #, python-format msgid "Note of %(club)s club" -msgstr "" +msgstr "%(club)s Note" -#: apps/note/models/notes.py:232 +#: apps/note/models/notes.py:229 msgid "special note" -msgstr "" +msgstr "Sondernote" -#: apps/note/models/notes.py:233 +#: apps/note/models/notes.py:230 msgid "special notes" -msgstr "" +msgstr "Sondernoten" -#: apps/note/models/notes.py:256 +#: apps/note/models/notes.py:253 msgid "Invalid alias" -msgstr "" +msgstr "Unerlaublt Alias" -#: apps/note/models/notes.py:272 +#: apps/note/models/notes.py:269 msgid "alias" -msgstr "" +msgstr "Alias" + +#: apps/note/models/notes.py:293 +msgid "Alias is too long." +msgstr "Alias ist zu lang." #: apps/note/models/notes.py:296 -msgid "Alias is too long." -msgstr "" - -#: apps/note/models/notes.py:299 msgid "" "This alias contains only complex character. Please use a more simple alias." msgstr "" +"Dieser Alias enthält nur komplexe Zeichen. Bitte verwenden Sie einen " +"einfacheren Alias." -#: apps/note/models/notes.py:303 +#: apps/note/models/notes.py:300 msgid "An alias with a similar name already exists: {} " -msgstr "" +msgstr "Ein Alias mit einem ähnlichen Namen existiert bereits: {} " -#: apps/note/models/notes.py:316 +#: apps/note/models/notes.py:313 msgid "You can't delete your main alias." -msgstr "" +msgstr "Sie können Ihren Hauptalias nicht löschen." #: apps/note/models/transactions.py:32 msgid "transaction category" -msgstr "" +msgstr "Transaktionkategorie" #: apps/note/models/transactions.py:33 msgid "transaction categories" -msgstr "" +msgstr "Transaktionkategorien" #: apps/note/models/transactions.py:49 msgid "A template with this name already exist" -msgstr "" +msgstr "Eine Vorlage mit diesem Namen ist bereits vorhanden" #: apps/note/models/transactions.py:72 msgid "display" -msgstr "" +msgstr "Schauen" #: apps/note/models/transactions.py:77 msgid "highlighted" -msgstr "" +msgstr "hervorgehoben" #: apps/note/models/transactions.py:87 msgid "transaction template" -msgstr "" +msgstr "Transaktionsvorlage" #: apps/note/models/transactions.py:88 msgid "transaction templates" -msgstr "" +msgstr "Transaktionsvorlagen" #: apps/note/models/transactions.py:112 apps/note/models/transactions.py:125 -#: apps/note/tables.py:34 apps/note/tables.py:43 +#: apps/note/tables.py:34 apps/note/tables.py:44 msgid "used alias" -msgstr "" +msgstr "benutzte Aliasen" #: apps/note/models/transactions.py:133 msgid "quantity" -msgstr "" +msgstr "Anzahl" #: apps/note/models/transactions.py:141 msgid "reason" -msgstr "" +msgstr "Grund" -#: apps/note/models/transactions.py:151 apps/note/tables.py:114 +#: apps/note/models/transactions.py:151 apps/note/tables.py:139 msgid "invalidity reason" -msgstr "" +msgstr "Ungültigkeit Grund" #: apps/note/models/transactions.py:159 msgid "transaction" -msgstr "" +msgstr "Transaktion" #: apps/note/models/transactions.py:160 #: apps/treasury/templates/treasury/sogecredit_detail.html:22 msgid "transactions" -msgstr "" +msgstr "Transaktionen" #: apps/note/models/transactions.py:182 #, python-brace-format @@ -1296,118 +1332,131 @@ msgid "" "You can't update the {field} on a Transaction. Please invalidate it and " "create one other." msgstr "" +"Sie können das {field} einer Transaktion nicht aktualisieren. Bitte machen " +"Sie es ungültig und erstellen Sie eine andere." #: apps/note/models/transactions.py:202 msgid "" "The note balances must be between - 92 233 720 368 547 758.08 € and 92 233 " "720 368 547 758.07 €." msgstr "" +"Die Notenguthaben müssen zwischen - 92 233 720 368 547 758,08 € und 92 233 " +"720 368 547 758,07 € liegen." -#: apps/note/models/transactions.py:283 +#: apps/note/models/transactions.py:279 +msgid "" +"The destination of this transaction must equal to the destination of the " +"template." +msgstr "" +"Der Empfänger dieser Transaktion muss dem Empfänger der Vorlage entsprechen." + +#: apps/note/models/transactions.py:288 msgid "Template" -msgstr "" +msgstr "Vorlage" -#: apps/note/models/transactions.py:286 +#: apps/note/models/transactions.py:291 msgid "recurrent transaction" -msgstr "" +msgstr "wiederkehrende Transaktion" -#: apps/note/models/transactions.py:287 +#: apps/note/models/transactions.py:292 msgid "recurrent transactions" -msgstr "" - -#: apps/note/models/transactions.py:302 -msgid "first_name" -msgstr "" +msgstr "wiederkehrende Transaktionen" #: apps/note/models/transactions.py:307 -msgid "bank" -msgstr "" +msgid "first_name" +msgstr "Vorname" -#: apps/note/models/transactions.py:324 +#: apps/note/models/transactions.py:312 +msgid "bank" +msgstr "Bank" + +#: apps/note/models/transactions.py:329 msgid "" "A special transaction is only possible between a Note associated to a " "payment method and a User or a Club" msgstr "" +"Eine Sondertransaktion ist nur zwischen einer Note, die einer " +"Zahlungsmethode zugeordnet ist, und einem User oder einem Club möglich" -#: apps/note/models/transactions.py:328 +#: apps/note/models/transactions.py:337 msgid "Special transaction" -msgstr "" +msgstr "Sondertransaktion" -#: apps/note/models/transactions.py:329 +#: apps/note/models/transactions.py:338 msgid "Special transactions" -msgstr "" +msgstr "Sondertranskationen" -#: apps/note/models/transactions.py:345 apps/note/models/transactions.py:350 +#: apps/note/models/transactions.py:354 msgid "membership transaction" -msgstr "" +msgstr "Mitgliedschafttransaktion" -#: apps/note/models/transactions.py:346 apps/treasury/models.py:273 +#: apps/note/models/transactions.py:355 apps/treasury/models.py:273 msgid "membership transactions" -msgstr "" +msgstr "Mitgliedschaftttransaktionen" -#: apps/note/tables.py:62 +#: apps/note/tables.py:93 msgid "Click to invalidate" -msgstr "" +msgstr "Klicken Sie zum Ungültigmachen" -#: apps/note/tables.py:62 +#: apps/note/tables.py:93 msgid "Click to validate" -msgstr "" +msgstr "Klicken Sie zum gültigmachen" -#: apps/note/tables.py:112 +#: apps/note/tables.py:137 msgid "No reason specified" -msgstr "" +msgstr "Kein Grund gegeben" -#: apps/note/tables.py:143 apps/note/tables.py:177 apps/treasury/tables.py:39 +#: apps/note/tables.py:168 apps/note/tables.py:202 apps/treasury/tables.py:39 #: apps/treasury/templates/treasury/invoice_confirm_delete.html:30 #: apps/treasury/templates/treasury/sogecredit_detail.html:59 #: apps/wei/tables.py:76 apps/wei/tables.py:103 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:31 msgid "Delete" -msgstr "" +msgstr "Löschen" -#: apps/note/tables.py:171 apps/note/templates/note/conso_form.html:132 +#: apps/note/tables.py:196 apps/note/templates/note/conso_form.html:132 #: apps/wei/tables.py:47 apps/wei/tables.py:48 #: apps/wei/templates/wei/base.html:89 #: apps/wei/templates/wei/bus_detail.html:20 #: apps/wei/templates/wei/busteam_detail.html:20 #: apps/wei/templates/wei/busteam_detail.html:40 msgid "Edit" -msgstr "" +msgstr "Bearbeiten" #: apps/note/templates/note/conso_form.html:32 msgid "Consum" -msgstr "" +msgstr "Konsumieren" #: apps/note/templates/note/conso_form.html:43 #: apps/note/templates/note/transaction_form.html:65 #: apps/note/templates/note/transaction_form.html:92 msgid "Name or alias..." -msgstr "" +msgstr "Name oder Alias..." #: apps/note/templates/note/conso_form.html:52 msgid "Select consumptions" -msgstr "" +msgstr "Verbrauchswerte auswählen" #: apps/note/templates/note/conso_form.html:61 msgid "Consume!" -msgstr "" +msgstr "Konsumieren!" #: apps/note/templates/note/conso_form.html:73 msgid "Highlighted buttons" -msgstr "" +msgstr "Hervorgehobene Tasten" #: apps/note/templates/note/conso_form.html:138 msgid "Single consumptions" -msgstr "" +msgstr "Solo Modus" #: apps/note/templates/note/conso_form.html:143 msgid "Double consumptions" -msgstr "" +msgstr "Doppelte Modus" #: apps/note/templates/note/conso_form.html:154 #: apps/note/templates/note/transaction_form.html:159 msgid "Recent transactions history" -msgstr "" +msgstr "Verlauf der letzten Transaktionen" #: apps/note/templates/note/mails/negative_balance.html:43 #: apps/note/templates/note/mails/negative_balance.txt:25 @@ -1417,190 +1466,192 @@ msgstr "" #: apps/registration/templates/registration/mails/email_validation_email.html:40 #: apps/registration/templates/registration/mails/email_validation_email.txt:16 msgid "Mail generated by the Note Kfet on the" -msgstr "" +msgstr "Mail generiert vom Note Kfet auf dem" #: apps/note/templates/note/transaction_form.html:54 #: apps/note/templates/note/transaction_form.html:174 msgid "Select emitters" -msgstr "" +msgstr "Sender auswählen" #: apps/note/templates/note/transaction_form.html:69 msgid "I am the emitter" -msgstr "" +msgstr "Ich bin der Sender" #: apps/note/templates/note/transaction_form.html:81 #: apps/note/templates/note/transaction_form.html:176 msgid "Select receivers" -msgstr "" +msgstr "Empfänger auswählen" #: apps/note/templates/note/transaction_form.html:104 msgid "Action" -msgstr "" +msgstr "Aktion" #: apps/note/templates/note/transaction_form.html:112 #: apps/treasury/forms.py:141 apps/treasury/tables.py:67 #: apps/treasury/tables.py:135 #: apps/treasury/templates/treasury/remittance_form.html:23 msgid "Amount" -msgstr "" +msgstr "Anzahl" #: apps/note/templates/note/transaction_form.html:128 #: apps/treasury/models.py:51 msgid "Name" -msgstr "" +msgstr "Name" #: apps/note/templates/note/transaction_form.html:173 msgid "Select emitter" -msgstr "" +msgstr "Sender auswählen" #: apps/note/templates/note/transaction_form.html:175 msgid "Select receiver" -msgstr "" +msgstr "Empfänger auswählen" #: apps/note/templates/note/transaction_form.html:177 msgid "Transfer type" -msgstr "" +msgstr "Überweisungtype" -#: apps/note/templates/note/transactiontemplate_form.html:10 +#: apps/note/templates/note/transactiontemplate_form.html:8 msgid "Buttons list" -msgstr "" +msgstr "Tastenliste" -#: apps/note/templates/note/transactiontemplate_form.html:21 +#: apps/note/templates/note/transactiontemplate_form.html:24 msgid "Price history" -msgstr "" +msgstr "Verlaufene Preisen" -#: apps/note/templates/note/transactiontemplate_form.html:24 +#: apps/note/templates/note/transactiontemplate_form.html:27 msgid "Obsolete since" -msgstr "" +msgstr "Veraltet seit" -#: apps/note/templates/note/transactiontemplate_form.html:24 +#: apps/note/templates/note/transactiontemplate_form.html:27 msgid "Current price" -msgstr "" +msgstr "Aktueller Preis" #: apps/note/templates/note/transactiontemplate_list.html:13 msgid "Name of the button..." -msgstr "" +msgstr "Name dessen Tatse." #: apps/note/templates/note/transactiontemplate_list.html:15 msgid "New button" -msgstr "" +msgstr "Neue Tatsen" #: apps/note/templates/note/transactiontemplate_list.html:22 msgid "buttons listing " -msgstr "" +msgstr "Tatsenliste " #: apps/note/templates/note/transactiontemplate_list.html:73 msgid "button successfully deleted " -msgstr "" +msgstr "Taste erfolgreich gelöscht " #: apps/note/templates/note/transactiontemplate_list.html:77 msgid "Unable to delete button " -msgstr "" +msgstr "Tatse kann nicht gelöscht werden " #: apps/note/views.py:36 msgid "Transfer money" -msgstr "" +msgstr "Geld überweisen" #: apps/note/views.py:74 msgid "Create new button" -msgstr "" +msgstr "Neue Tatse berstellen" #: apps/note/views.py:83 msgid "Search button" -msgstr "" +msgstr "Tatsen finden" #: apps/note/views.py:111 msgid "Update button" -msgstr "" +msgstr "Tatse bearbeiten" -#: apps/note/views.py:151 note_kfet/templates/base.html:72 +#: apps/note/views.py:151 note_kfet/templates/base.html:63 msgid "Consumptions" -msgstr "" +msgstr "Verbräuche" #: apps/note/views.py:165 msgid "You can't see any button." -msgstr "" +msgstr "Sie können keine Taste sehen." #: apps/note/views.py:200 msgid "Search transactions" -msgstr "" +msgstr "Transaktion finden" #: apps/permission/models.py:89 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" -msgstr "" +msgstr "Kann {type} {model}.{field} in {query}" #: apps/permission/models.py:91 #, python-brace-format msgid "Can {type} {model} in {query}" -msgstr "" +msgstr "Kann {type} {model} in {query}" #: apps/permission/models.py:104 msgid "rank" -msgstr "" +msgstr "Rank" #: apps/permission/models.py:117 msgid "permission mask" -msgstr "" +msgstr "Berechtigungsmaske" #: apps/permission/models.py:118 msgid "permission masks" -msgstr "" +msgstr "Berechtigungsmasken" #: apps/permission/models.py:124 msgid "add" -msgstr "" +msgstr "hinzufügen" #: apps/permission/models.py:125 msgid "view" -msgstr "" +msgstr "Schauen" #: apps/permission/models.py:126 msgid "change" -msgstr "" +msgstr "bearbeiten" #: apps/permission/models.py:158 msgid "query" -msgstr "" +msgstr "Abfrage" #: apps/permission/models.py:171 msgid "mask" -msgstr "" +msgstr "Maske" #: apps/permission/models.py:177 msgid "field" -msgstr "" +msgstr "Feld" #: apps/permission/models.py:182 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." msgstr "" +"Gibt an, ob die Berechtigung auch erteilt werden soll, wenn die " +"Mitgliedschaft des Benutzers abgelaufen ist." #: apps/permission/models.py:183 #: apps/permission/templates/permission/all_rights.html:89 msgid "permanent" -msgstr "" +msgstr "permanent" #: apps/permission/models.py:194 msgid "permission" -msgstr "" +msgstr "Berechtigung" #: apps/permission/models.py:195 apps/permission/models.py:334 msgid "permissions" -msgstr "" +msgstr "Berechtigungen" #: apps/permission/models.py:200 msgid "Specifying field applies only to view and change permission types." -msgstr "" +msgstr "Angabefeld gilt nur zum Anzeigen und Ändern von Berechtigungstypen." #: apps/permission/models.py:339 msgid "for club" -msgstr "" +msgstr "Für Club" #: apps/permission/models.py:349 apps/permission/models.py:350 msgid "role permissions" -msgstr "" +msgstr "Berechtigung Rollen" #: apps/permission/signals.py:63 #, python-brace-format @@ -1608,6 +1659,8 @@ msgid "" "You don't have the permission to change the field {field} on this instance " "of model {app_label}.{model_name}." msgstr "" +"Sie haben nicht die Berechtigung, das Feld {field} in dieser Instanz von " +"Modell {app_label} zu ändern. {model_name}" #: apps/permission/signals.py:73 apps/permission/views.py:89 #, python-brace-format @@ -1615,6 +1668,8 @@ msgid "" "You don't have the permission to add an instance of model {app_label}." "{model_name}." msgstr "" +"Sie haben nicht die Berechtigung, eine Instanz von model {app_label}. " +"{model_name} hinzufügen." #: apps/permission/signals.py:101 #, python-brace-format @@ -1622,46 +1677,48 @@ msgid "" "You don't have the permission to delete this instance of model {app_label}." "{model_name}." msgstr "" +"Sie haben nicht die Berechtigung, eine Instanz von model {app_label}. " +"{model_name} zulöschen." #: apps/permission/templates/permission/all_rights.html:12 msgid "Users that have surnormal rights" -msgstr "" +msgstr "User die Oberberechtigung haben" #: apps/permission/templates/permission/all_rights.html:16 msgid "Superusers have all rights on everything, to manage the website." -msgstr "" +msgstr "Superuser haben alle Berechtigung, um das Website zu handeln." #: apps/permission/templates/permission/all_rights.html:21 msgid "Superusers" -msgstr "" +msgstr "Superusers" #: apps/permission/templates/permission/all_rights.html:45 msgid "Roles description" -msgstr "" +msgstr "Rolle Beschreibung" #: apps/permission/templates/permission/all_rights.html:52 msgid "Filter with roles that I have in at least one club" -msgstr "" +msgstr "Filtern Sie nach Rollen, die ich in mindestens einem Club habe" #: apps/permission/templates/permission/all_rights.html:69 msgid "Owned" -msgstr "" +msgstr "Besetzt" #: apps/permission/templates/permission/all_rights.html:80 msgid "Own this role in the clubs" -msgstr "" +msgstr "Besitze diese Rolle in den Clubs" #: apps/permission/templates/permission/all_rights.html:86 msgid "Mask:" -msgstr "" +msgstr "Mask:" #: apps/permission/templates/permission/all_rights.html:86 msgid "Query:" -msgstr "" +msgstr "Abfrage:" #: apps/permission/templates/permission/all_rights.html:92 msgid "No associated permission" -msgstr "" +msgstr "Keine zugehörige Berechtigung" #: apps/permission/views.py:56 #, python-brace-format @@ -1669,6 +1726,9 @@ msgid "" "You don't have the permission to update this instance of the model " "\"{model}\" with these parameters. Please correct your data and retry." msgstr "" +"Sie haben nicht die Berechtigung, diese Instanz des Modells \"{model}\" mit " +"diesen Parametern zu aktualisieren. Bitte korrigieren Sie Ihre Daten und " +"versuchen Sie es erneut." #: apps/permission/views.py:60 #, python-brace-format @@ -1676,99 +1736,113 @@ msgid "" "You don't have the permission to create an instance of the model \"{model}\" " "with these parameters. Please correct your data and retry." msgstr "" +"Sie haben nicht die Berechtigung, eine Instanz des Modells \"{model}\" mit " +"diesen Parametern zu erstellen. Bitte korrigieren Sie Ihre Daten und " +"versuchen Sie es erneut." -#: apps/permission/views.py:96 note_kfet/templates/base.html:114 +#: apps/permission/views.py:96 note_kfet/templates/base.html:105 msgid "Rights" -msgstr "" +msgstr "Rechten" #: apps/permission/views.py:101 msgid "All rights" -msgstr "" +msgstr "Alle Rechten" #: apps/registration/apps.py:10 msgid "registration" -msgstr "" +msgstr "Anmeldung" #: apps/registration/forms.py:39 msgid "This email address is already used." -msgstr "" +msgstr "Diese email adresse ist schon benutzt." #: apps/registration/forms.py:49 msgid "Register to the WEI" -msgstr "" +msgstr "Zu WEI anmelden" #: apps/registration/forms.py:51 msgid "" "Check this case if you want to register to the WEI. If you hesitate, you " "will be able to register later, after validating your account in the Kfet." msgstr "" +"Überprüfen Sie diesen Fall, wenn Sie sich beim WEI registrieren möchten. " +"falls Zweifel, können Sie sich später nach Bestätigung Ihres Kontos im Kfet " +"registrieren." #: apps/registration/forms.py:96 msgid "Join BDE Club" -msgstr "" +msgstr "BDE Mitglieder werden" #: apps/registration/forms.py:103 msgid "Join Kfet Club" -msgstr "" +msgstr "Kfet Mitglieder werden" #: apps/registration/templates/registration/email_validation_complete.html:9 msgid "Your email have successfully been validated." -msgstr "" +msgstr "Ihre E-Mail wurde erfolgreich validiert." #: apps/registration/templates/registration/email_validation_complete.html:11 #, python-format msgid "You can now log in." -msgstr "" +msgstr "Sie können sich jetz anmelden ." #: apps/registration/templates/registration/email_validation_complete.html:13 msgid "" "You must pay now your membership in the Kfet to complete your registration." msgstr "" +"Sie müssen jetzt Ihre Mitgliedschaft im Kfet bezahlen, um Ihre Registrierung " +"abzuschließen." #: apps/registration/templates/registration/email_validation_complete.html:16 msgid "" "The link was invalid. The token may have expired. Please send us an email to " "activate your account." msgstr "" +"Der Link war ungültig. Das Token ist möglicherweise abgelaufen. Bitte senden " +"Sie uns eine E-Mail, um Ihr Konto zu aktivieren." #: apps/registration/templates/registration/email_validation_email_sent.html:8 msgid "Account activation" -msgstr "" +msgstr "Kontoaktivierung" #: apps/registration/templates/registration/email_validation_email_sent.html:11 msgid "" "An email has been sent. Please click on the link to activate your account." msgstr "" +"Eine E-Mail wurde gesendet. Bitte klicken Sie auf den Link, um Ihr Konto zu " +"aktivieren." #: apps/registration/templates/registration/email_validation_email_sent.html:15 msgid "" "You must also go to the Kfet to pay your membership. The WEI registration " "includes the BDE membership." msgstr "" +"Sie müssen auch zum Kfet gehen, um Ihre Mitgliedschaft zu bezahlen. Die WEI-" +"Registrierung beinhaltet die BDE-Mitgliedschaft." #: apps/registration/templates/registration/future_profile_detail.html:49 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:11 msgid "Delete registration" -msgstr "" +msgstr "Registrierung löschen" #: apps/registration/templates/registration/future_profile_detail.html:57 msgid "Validate account" -msgstr "" +msgstr "Konto validieren" #: apps/registration/templates/registration/future_profile_detail.html:64 #: apps/wei/templates/wei/weimembership_form.html:127 #: apps/wei/templates/wei/weimembership_form.html:186 msgid "Validate registration" -msgstr "" +msgstr "Registrierung validieren" #: apps/registration/templates/registration/future_user_list.html:9 msgid "New user" -msgstr "" +msgstr "Neue User" #: apps/registration/templates/registration/mails/email_validation_email.html:12 #: apps/registration/templates/registration/mails/email_validation_email.txt:3 msgid "Hi" -msgstr "" +msgstr "Hallo" #: apps/registration/templates/registration/mails/email_validation_email.html:16 #: apps/registration/templates/registration/mails/email_validation_email.txt:5 @@ -1776,6 +1850,8 @@ msgid "" "You recently registered on the Note Kfet. Please click on the link below to " "confirm your registration." msgstr "" +"Sie haben sich kürzlich beim Note Kfet registriert. Bitte klicken Sie auf " +"den Link unten, um Ihre Registrierung zu bestätigen." #: apps/registration/templates/registration/mails/email_validation_email.html:26 #: apps/registration/templates/registration/mails/email_validation_email.txt:9 @@ -1783,6 +1859,8 @@ msgid "" "This link is only valid for a couple of days, after that you will need to " "contact us to validate your email." msgstr "" +"Dieser Link ist nur einige Tage gültig. Danach müssen Sie uns kontaktieren, " +"um Ihre E-Mail zu bestätigen." #: apps/registration/templates/registration/mails/email_validation_email.html:30 #: apps/registration/templates/registration/mails/email_validation_email.txt:11 @@ -1791,81 +1869,82 @@ msgid "" "you can log in. You will need to pay your membership in the Kfet. Note that " "the WEI registration includes the Kfet membership." msgstr "" +"Danach müssen Sie warten, bis jemand Ihr Konto validiert, bevor Sie sich " +"anmelden können. Sie müssen Ihre Mitgliedschaft im Kfet bezahlen. Beachten " +"Sie, dass die WEI-Registrierung die Kfet-Mitgliedschaft enthält." #: apps/registration/templates/registration/mails/email_validation_email.html:34 #: apps/registration/templates/registration/mails/email_validation_email.txt:13 msgid "Thanks" -msgstr "" +msgstr "Danke" #: apps/registration/templates/registration/mails/email_validation_email.html:39 #: apps/registration/templates/registration/mails/email_validation_email.txt:15 msgid "The Note Kfet team." -msgstr "" +msgstr "Die NoteKfet Team." #: apps/registration/views.py:38 msgid "Register new user" -msgstr "" +msgstr "Neuen User registrieren" #: apps/registration/views.py:82 msgid "Email validation" -msgstr "" +msgstr "Email validierung" #: apps/registration/views.py:84 msgid "Validate email" -msgstr "" +msgstr "Email validieren" #: apps/registration/views.py:126 msgid "Email validation unsuccessful" -msgstr "" +msgstr "Email validierung unerfolgreich" #: apps/registration/views.py:137 msgid "Email validation email sent" -msgstr "" +msgstr "Validierungsemail wurde gesendet" #: apps/registration/views.py:145 msgid "Resend email validation link" -msgstr "" +msgstr "E-Mail-Validierungslink erneut senden" #: apps/registration/views.py:163 msgid "Pre-registered users list" -msgstr "" +msgstr "Vorregistrierte Userliste" -#: apps/registration/views.py:190 +#: apps/registration/views.py:187 msgid "Unregistered users" -msgstr "" +msgstr "Unregistrierte Users" -#: apps/registration/views.py:203 +#: apps/registration/views.py:200 msgid "Registration detail" -msgstr "" +msgstr "Registrierung Detailen" -#: apps/registration/views.py:258 +#: apps/registration/views.py:256 msgid "You must join the BDE." -msgstr "" +msgstr "Sie müssen die BDE beitreten." #: apps/registration/views.py:280 -msgid "You must join BDE club before joining Kfet club." -msgstr "" - -#: apps/registration/views.py:285 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" +"Der eingegebene Betrag reicht für die Mitgliedschaft nicht aus, sollte " +"mindestens {} betragen" -#: apps/registration/views.py:360 +#: apps/registration/views.py:355 msgid "Invalidate pre-registration" -msgstr "" +msgstr "Ungültige Vorregistrierung" -#: apps/treasury/apps.py:12 note_kfet/templates/base.html:102 +#: apps/treasury/apps.py:12 note_kfet/templates/base.html:93 msgid "Treasury" -msgstr "" +msgstr "Quaestor" #: apps/treasury/forms.py:104 msgid "Remittance is already closed." -msgstr "" +msgstr "Überweisung ist bereits geschlossen." #: apps/treasury/forms.py:109 msgid "You can't change the type of the remittance." -msgstr "" +msgstr "Sie können die Art der Überweisung nicht ändern." #: apps/treasury/forms.py:129 apps/treasury/models.py:252 #: apps/treasury/tables.py:97 apps/treasury/tables.py:105 @@ -1873,716 +1952,747 @@ msgstr "" #: apps/treasury/templates/treasury/remittance_list.html:16 #: apps/treasury/templates/treasury/sogecredit_list.html:16 msgid "Remittance" -msgstr "" +msgstr "Überweisung" #: apps/treasury/forms.py:130 msgid "No attached remittance" -msgstr "" +msgstr "Keine beigefügte Überweisung" #: apps/treasury/models.py:23 msgid "Invoice identifier" -msgstr "" +msgstr "Rechnungskennung" #: apps/treasury/models.py:37 msgid "BDE" -msgstr "" +msgstr "BDE" #: apps/treasury/models.py:42 msgid "Object" -msgstr "" +msgstr "Objekt" #: apps/treasury/models.py:46 msgid "Description" -msgstr "" +msgstr "Beschreibung" #: apps/treasury/models.py:55 msgid "Address" -msgstr "" +msgstr "Adresse" #: apps/treasury/models.py:60 apps/treasury/models.py:180 msgid "Date" -msgstr "" +msgstr "Datum" #: apps/treasury/models.py:64 msgid "Acquitted" -msgstr "" +msgstr "Bezahlt" #: apps/treasury/models.py:69 msgid "Locked" -msgstr "" +msgstr "Gesperrt" #: apps/treasury/models.py:70 msgid "An invoice can't be edited when it is locked." -msgstr "" +msgstr "Eine Rechnung kann nicht bearbeitet werden, wenn sie gesperrt ist." #: apps/treasury/models.py:76 msgid "tex source" -msgstr "" +msgstr "Tex Quelle" #: apps/treasury/models.py:89 #: apps/treasury/templates/treasury/invoice_form.html:22 msgid "This invoice is locked and can no longer be edited." -msgstr "" +msgstr "Diese Rechnung ist gesperrt und kann nicht mehr bearbeitet werden." #: apps/treasury/models.py:109 apps/treasury/models.py:122 msgid "invoice" -msgstr "" +msgstr "Rechnung" #: apps/treasury/models.py:110 msgid "invoices" -msgstr "" +msgstr "Rechnungen" #: apps/treasury/models.py:127 msgid "Designation" -msgstr "" +msgstr "Bezeichnung" #: apps/treasury/models.py:131 msgid "Quantity" -msgstr "" +msgstr "Qualität" #: apps/treasury/models.py:135 msgid "Unit price" -msgstr "" +msgstr "Einzelpreis" #: apps/treasury/models.py:151 msgid "product" -msgstr "" +msgstr "Produkt" #: apps/treasury/models.py:152 msgid "products" -msgstr "" +msgstr "Produkten" #: apps/treasury/models.py:169 msgid "remittance type" -msgstr "" +msgstr "Überweisungstyp" #: apps/treasury/models.py:170 msgid "remittance types" -msgstr "" +msgstr "Überweisungstypen" #: apps/treasury/models.py:191 msgid "Comment" -msgstr "" +msgstr "Kommentar" #: apps/treasury/models.py:196 msgid "Closed" -msgstr "" +msgstr "Geschlossen" #: apps/treasury/models.py:200 msgid "remittance" -msgstr "" +msgstr "Überweisung" #: apps/treasury/models.py:201 msgid "remittances" -msgstr "" +msgstr "Überweisungen" #: apps/treasury/models.py:233 msgid "Remittance #{:d}: {}" -msgstr "" +msgstr "Überweisung #{:d}:{}" #: apps/treasury/models.py:256 msgid "special transaction proxy" -msgstr "" +msgstr "spezielle Transaktion Proxy" #: apps/treasury/models.py:257 msgid "special transaction proxies" -msgstr "" +msgstr "spezielle Transaktion Proxies" #: apps/treasury/models.py:279 msgid "credit transaction" -msgstr "" +msgstr "Kredit Transaktion" #: apps/treasury/models.py:343 msgid "" "This user doesn't have enough money to pay the memberships with its note. " "Please ask her/him to credit the note before invalidating this credit." msgstr "" +"Dieser Benutzer hat nicht genug Geld, um die Mitgliedschaften mit seiner " +"Note zu bezahlen." #: apps/treasury/models.py:355 #: apps/treasury/templates/treasury/sogecredit_detail.html:10 msgid "Credit from the Société générale" -msgstr "" +msgstr "Kredit von der Société générale" #: apps/treasury/models.py:356 msgid "Credits from the Société générale" -msgstr "" +msgstr "Krediten von der Société générale" #: apps/treasury/tables.py:20 msgid "Invoice #{:d}" -msgstr "" +msgstr "Rechnung #{:d}" #: apps/treasury/tables.py:25 #: apps/treasury/templates/treasury/invoice_list.html:13 #: apps/treasury/templates/treasury/remittance_list.html:13 #: apps/treasury/templates/treasury/sogecredit_list.html:13 msgid "Invoice" -msgstr "" +msgstr "Rechnung" #: apps/treasury/tables.py:65 msgid "Transaction count" -msgstr "" +msgstr "Transaktionanzahl" #: apps/treasury/tables.py:70 apps/treasury/tables.py:72 msgid "View" -msgstr "" +msgstr "Schauen" #: apps/treasury/tables.py:146 msgid "Yes" -msgstr "" +msgstr "Ja" #: apps/treasury/templates/treasury/invoice_confirm_delete.html:10 #: apps/treasury/views.py:166 msgid "Delete invoice" -msgstr "" +msgstr "Rechnung löschen" #: apps/treasury/templates/treasury/invoice_confirm_delete.html:15 msgid "This invoice is locked and can't be deleted." -msgstr "" +msgstr "Eine Rechnung kann nicht gelöscht werden, wenn sie gesperrt ist." #: apps/treasury/templates/treasury/invoice_confirm_delete.html:21 msgid "" "Are you sure you want to delete this invoice? This action can't be undone." msgstr "" +"Möchten Sie diese Rechnung wirklich löschen? Diese Aktion kann nicht " +"rückgängig gemacht werden." #: apps/treasury/templates/treasury/invoice_confirm_delete.html:28 msgid "Return to invoices list" -msgstr "" +msgstr "Zurück zur Rechnungsliste" #: apps/treasury/templates/treasury/invoice_form.html:15 msgid "" "Warning: the LaTeX template is saved with this object. Updating the invoice " "implies regenerate it. Be careful if you manipulate old invoices." msgstr "" +"Warnung: Die LaTeX-Vorlage wird mit diesem Objekt gespeichert. Wenn Sie die " +"Rechnung aktualisieren, müssen Sie sie neu generieren. Seien Sie vorsichtig, " +"wenn Sie alte Rechnungen bearbeiten." #: apps/treasury/templates/treasury/invoice_form.html:69 msgid "Add product" -msgstr "" +msgstr "Produkt hinzufügen" #: apps/treasury/templates/treasury/invoice_form.html:70 msgid "Remove product" -msgstr "" +msgstr "Produkt entfernen" #: apps/treasury/templates/treasury/invoice_list.html:19 #: apps/treasury/templates/treasury/remittance_list.html:19 #: apps/treasury/templates/treasury/sogecredit_list.html:19 msgid "Société générale credits" -msgstr "" +msgstr "Krediten von der Société générale" #: apps/treasury/templates/treasury/invoice_list.html:31 msgid "New invoice" -msgstr "" +msgstr "Neue Rechnung" #: apps/treasury/templates/treasury/remittance_form.html:12 msgid "Remittance #" -msgstr "" +msgstr "Überweisung #" #: apps/treasury/templates/treasury/remittance_form.html:17 msgid "Count" -msgstr "" +msgstr "Anzahl" #: apps/treasury/templates/treasury/remittance_form.html:35 msgid "Linked transactions" -msgstr "" +msgstr "Transaktionen" #: apps/treasury/templates/treasury/remittance_form.html:42 msgid "There is no transaction linked with this remittance." -msgstr "" +msgstr "Mit dieser Überweisung ist keine Transaktion verbunden." #: apps/treasury/templates/treasury/remittance_list.html:27 msgid "Opened remittances" -msgstr "" +msgstr "Geöffnete Überweisungen" #: apps/treasury/templates/treasury/remittance_list.html:34 msgid "There is no opened remittance." -msgstr "" +msgstr "Es gibt keine offene Überweisung." #: apps/treasury/templates/treasury/remittance_list.html:39 msgid "New remittance" -msgstr "" +msgstr "Neue Überweisung" #: apps/treasury/templates/treasury/remittance_list.html:45 msgid "Transfers without remittances" -msgstr "" +msgstr "Transfer ohne Überweisungen" #: apps/treasury/templates/treasury/remittance_list.html:52 msgid "There is no transaction without any linked remittance." -msgstr "" +msgstr "Mit dieser Überweisung ist keine Transaktion verbunden." #: apps/treasury/templates/treasury/remittance_list.html:60 msgid "Transfers with opened remittances" -msgstr "" +msgstr "Transfer mit geöffneten Überweisungen" #: apps/treasury/templates/treasury/remittance_list.html:67 msgid "There is no transaction with an opened linked remittance." msgstr "" +"Es gibt keine Transaktion mit einer geöffneten verknüpften Überweisung." #: apps/treasury/templates/treasury/remittance_list.html:75 msgid "Closed remittances" -msgstr "" +msgstr "Geschlossene Überweisungen" #: apps/treasury/templates/treasury/remittance_list.html:82 msgid "There is no closed remittance yet." -msgstr "" +msgstr "Es gibt noch keine geschlossene Überweisung." #: apps/treasury/templates/treasury/sogecredit_detail.html:29 msgid "total amount" -msgstr "" +msgstr "Totalanzahlt" #: apps/treasury/templates/treasury/sogecredit_detail.html:35 msgid "" "Warning: Validating this credit implies that all membership transactions " "will be validated." msgstr "" +"Achtung: Die Validierung dieses Guthabens bedeutet, dass alle " +"Mitgliedschaftstransaktionen validiert werden." #: apps/treasury/templates/treasury/sogecredit_detail.html:36 msgid "" "If you delete this credit, there all membership transactions will be also " "validated, but no credit will be operated." msgstr "" +"Wenn Sie dieses Kredit löschen, werden dort auch alle " +"Mitgliedschaftstransaktionen validiert, es wird jedoch kein Kredit betrieben." #: apps/treasury/templates/treasury/sogecredit_detail.html:37 msgid "" "If this credit is validated, then the user won't be able to ask for a credit " "from the Société générale." msgstr "" +"Wenn dieses Kredit validiert ist, kann der Benutzer bei der Société générale " +"kein Guthaben anfordern." #: apps/treasury/templates/treasury/sogecredit_detail.html:38 msgid "If you think there is an error, please contact the \"respos info\"." msgstr "" +"Wenn Sie glauben, dass ein Fehler vorliegt, wenden Sie sich bitte an die " +"\"respos info\"." #: apps/treasury/templates/treasury/sogecredit_detail.html:44 msgid "This credit is already validated." -msgstr "" +msgstr "Dieser Kredit ist bereits validiert." #: apps/treasury/templates/treasury/sogecredit_detail.html:49 msgid "" "Warning: if you don't validate this credit, the note of the user doesn't " "have enough money to pay its memberships." msgstr "" +"Achtung: Wenn Sie diese Kredit nicht bestätigen, die Note von dem Benutzer " +"nicht genug Geld hat, um seine Mitgliedschaft zu zahlen." #: apps/treasury/templates/treasury/sogecredit_detail.html:50 msgid "Please ask the user to credit its note before deleting this credit." msgstr "" +"Bitte bitten Sie den Benutzer, seine Note gutzuschreiben, bevor Sie diese " +"Kredit löschen." #: apps/treasury/templates/treasury/sogecredit_detail.html:57 #: apps/wei/tables.py:59 apps/wei/tables.py:60 apps/wei/tables.py:99 msgid "Validate" -msgstr "" +msgstr "Validieren" #: apps/treasury/templates/treasury/sogecredit_detail.html:65 msgid "Return to credit list" -msgstr "" +msgstr "Zurück zur Kreditlist" #: apps/treasury/templates/treasury/sogecredit_list.html:34 msgid "Filter with unvalidated credits only" -msgstr "" +msgstr "Filtern Sie nur mit nicht validierten Kredits" #: apps/treasury/templates/treasury/sogecredit_list.html:44 msgid "There is no matched user that have asked for a Société générale credit." msgstr "" +"Es gibt keinen übereinstimmenden User, der eine Kredit für die Société " +"générale beantragt hat." #: apps/treasury/views.py:38 msgid "Create new invoice" -msgstr "" +msgstr "Neue Rechnung" #: apps/treasury/views.py:89 msgid "Invoices list" -msgstr "" +msgstr "Rechnunglist" #: apps/treasury/views.py:104 apps/treasury/views.py:265 #: apps/treasury/views.py:391 msgid "You are not able to see the treasury interface." -msgstr "" +msgstr "Sie können die Quaestor-App nicht sehen." #: apps/treasury/views.py:114 msgid "Update an invoice" -msgstr "" +msgstr "Rechnung bearbeiten" #: apps/treasury/views.py:226 msgid "Create a new remittance" -msgstr "" +msgstr "Neue Überweisung" #: apps/treasury/views.py:253 msgid "Remittances list" -msgstr "" +msgstr "Überweisungliste" #: apps/treasury/views.py:316 msgid "Update a remittance" -msgstr "" +msgstr "Überweisung bearbeiten" #: apps/treasury/views.py:339 msgid "Attach a transaction to a remittance" -msgstr "" +msgstr "Fügen Sie einer Überweisung eine Transaktion hinzu" #: apps/treasury/views.py:383 msgid "List of credits from the Société générale" -msgstr "" +msgstr "Kreditliste von Société générale" #: apps/treasury/views.py:426 msgid "Manage credits from the Société générale" -msgstr "" +msgstr "Krediten von der Société générale handeln" #: apps/wei/apps.py:10 apps/wei/models.py:49 apps/wei/models.py:50 #: apps/wei/models.py:61 apps/wei/models.py:167 -#: note_kfet/templates/base.html:108 +#: note_kfet/templates/base.html:99 msgid "WEI" -msgstr "" +msgstr "WEI" #: apps/wei/forms/registration.py:51 apps/wei/models.py:113 #: apps/wei/models.py:283 msgid "bus" -msgstr "" +msgstr "Bus" #: apps/wei/forms/registration.py:52 msgid "" "This choice is not definitive. The WEI organizers are free to attribute for " "you a bus and a team, in particular if you are a free eletron." msgstr "" +"Diese Wahl ist nicht endgültig. Den WEI-Organisatoren steht es frei, Ihnen " +"einen Bus und ein Team zuzuweisen, insbesondere wenn Sie ein freies Elektron " +"sind." #: apps/wei/forms/registration.py:59 msgid "Team" -msgstr "" +msgstr "Team" #: apps/wei/forms/registration.py:61 msgid "" "Leave this field empty if you won't be in a team (staff, bus chief, free " "electron)" msgstr "" +"Lassen Sie dieses Feld leer, wenn Sie nicht in einem Team sind (Mitarbeiter, " +"Buschef, freies Elektron)" #: apps/wei/forms/registration.py:67 apps/wei/forms/registration.py:77 #: apps/wei/models.py:148 msgid "WEI Roles" -msgstr "" +msgstr "WEI Rollen" #: apps/wei/forms/registration.py:68 msgid "Select the roles that you are interested in." -msgstr "" +msgstr "Wählen Sie die Rollen aus, an denen Sie interessiert sind." #: apps/wei/forms/registration.py:113 msgid "This team doesn't belong to the given bus." -msgstr "" +msgstr "Dieses Team gehört nicht zum angegebenen Bus." #: apps/wei/forms/surveys/wei2020.py:29 msgid "Choose a word:" -msgstr "" +msgstr "Wählen Sie ein Wort:" #: apps/wei/models.py:24 apps/wei/templates/wei/base.html:36 msgid "year" -msgstr "" +msgstr "Jahr" #: apps/wei/models.py:28 apps/wei/templates/wei/base.html:30 msgid "date start" -msgstr "" +msgstr "Anfangsdatum" #: apps/wei/models.py:32 apps/wei/templates/wei/base.html:33 msgid "date end" -msgstr "" +msgstr "Abschlussdatum" #: apps/wei/models.py:77 msgid "survey information" -msgstr "" +msgstr "Umfrage Infos" #: apps/wei/models.py:78 msgid "Information about the survey for new members, encoded in JSON" -msgstr "" +msgstr "Informationen zur Umfrage für neue Mitglieder, codiert in JSON" #: apps/wei/models.py:100 msgid "Bus" -msgstr "" +msgstr "Bus" #: apps/wei/models.py:101 apps/wei/templates/wei/weiclub_detail.html:51 msgid "Buses" -msgstr "" +msgstr "Buses" #: apps/wei/models.py:122 msgid "color" -msgstr "" +msgstr "Farbe" #: apps/wei/models.py:123 msgid "The color of the T-Shirt, stored with its number equivalent" -msgstr "" +msgstr "Die Farbe des T-Shirts, gespeichert mit der entsprechenden Nummer" #: apps/wei/models.py:137 msgid "Bus team" -msgstr "" +msgstr "Bus Team" #: apps/wei/models.py:138 msgid "Bus teams" -msgstr "" +msgstr "Bus Teams" #: apps/wei/models.py:147 msgid "WEI Role" -msgstr "" +msgstr "WEI Rolle" #: apps/wei/models.py:172 msgid "Credit from Société générale" -msgstr "" +msgstr "Kredit von der Société générale" #: apps/wei/models.py:177 msgid "Caution check given" -msgstr "" +msgstr "Caution check given" #: apps/wei/models.py:181 apps/wei/templates/wei/weimembership_form.html:64 msgid "birth date" -msgstr "" +msgstr "Geburtsdatum" #: apps/wei/models.py:187 apps/wei/models.py:197 msgid "Male" -msgstr "" +msgstr "Männlich" #: apps/wei/models.py:188 apps/wei/models.py:198 msgid "Female" -msgstr "" +msgstr "Weiblich" #: apps/wei/models.py:189 msgid "Non binary" -msgstr "" +msgstr "Nicht binär" #: apps/wei/models.py:191 apps/wei/templates/wei/weimembership_form.html:55 msgid "gender" -msgstr "" +msgstr "Geschlecht" #: apps/wei/models.py:200 apps/wei/templates/wei/weimembership_form.html:58 msgid "clothing cut" -msgstr "" +msgstr "Kleidung Schnitt" #: apps/wei/models.py:213 apps/wei/templates/wei/weimembership_form.html:61 msgid "clothing size" -msgstr "" +msgstr "Kleidergröße" #: apps/wei/models.py:219 apps/wei/templates/wei/weimembership_form.html:67 msgid "health issues" -msgstr "" +msgstr "Gesundheitsprobleme" #: apps/wei/models.py:224 apps/wei/templates/wei/weimembership_form.html:70 msgid "emergency contact name" -msgstr "" +msgstr "Notfall-Kontakt" #: apps/wei/models.py:229 apps/wei/templates/wei/weimembership_form.html:73 msgid "emergency contact phone" -msgstr "" +msgstr "Notfallkontakttelefon" #: apps/wei/models.py:234 apps/wei/templates/wei/weimembership_form.html:52 msgid "first year" -msgstr "" +msgstr "Erste Jahr" #: apps/wei/models.py:235 msgid "Tells if the user is new in the school." -msgstr "" +msgstr "Gibt an, ob der USer neu in der Schule ist." #: apps/wei/models.py:240 msgid "registration information" -msgstr "" +msgstr "Registrierung Detailen" #: apps/wei/models.py:241 msgid "" "Information about the registration (buses for old members, survey fot the " "new members), encoded in JSON" msgstr "" +"Informationen zur Registrierung (Busse für alte Mitglieder, Umfrage für neue " +"Mitglieder), verschlüsselt in JSON" #: apps/wei/models.py:272 msgid "WEI User" -msgstr "" +msgstr "WEI User" #: apps/wei/models.py:273 msgid "WEI Users" -msgstr "" +msgstr "WEI Users" #: apps/wei/models.py:293 msgid "team" -msgstr "" +msgstr "Team" #: apps/wei/models.py:303 msgid "WEI registration" -msgstr "" +msgstr "WEI Registrierung" #: apps/wei/models.py:307 msgid "WEI membership" -msgstr "" +msgstr "WEI Mitgliedschaft" #: apps/wei/models.py:308 msgid "WEI memberships" -msgstr "" +msgstr "WEI Mitgliedschaften" #: apps/wei/tables.py:127 msgid "Year" -msgstr "" +msgstr "Jahr" #: apps/wei/tables.py:165 apps/wei/templates/wei/bus_detail.html:32 #: apps/wei/templates/wei/busteam_detail.html:50 msgid "Teams" -msgstr "" +msgstr "Teams" #: apps/wei/tables.py:174 apps/wei/tables.py:215 msgid "Members count" -msgstr "" +msgstr "Anzahl Mitgliedern" #: apps/wei/tables.py:181 apps/wei/tables.py:212 msgid "members" -msgstr "" +msgstr "Mitglieder" #: apps/wei/templates/wei/base.html:44 msgid "WEI fee (paid students)" -msgstr "" +msgstr "WEI Preis (bezahlte Studenten)" #: apps/wei/templates/wei/base.html:47 apps/wei/templates/wei/base.html:54 msgid "The BDE membership is included in the WEI registration." -msgstr "" +msgstr "Die BDE-Mitgliedschaft ist in der WEI-Registrierung enthalten." #: apps/wei/templates/wei/base.html:51 msgid "WEI fee (unpaid students)" -msgstr "" +msgstr "WEI Preis (unbezahlte Studenten)" #: apps/wei/templates/wei/base.html:76 msgid "WEI list" -msgstr "" +msgstr "WEI Liste" #: apps/wei/templates/wei/base.html:81 apps/wei/views.py:506 msgid "Register 1A" -msgstr "" +msgstr "1A Registrieren" #: apps/wei/templates/wei/base.html:85 apps/wei/views.py:573 msgid "Register 2A+" -msgstr "" +msgstr "2A+ Registrieren" #: apps/wei/templates/wei/base.html:93 msgid "Add bus" -msgstr "" +msgstr "Neue Bus" #: apps/wei/templates/wei/base.html:97 msgid "View WEI" -msgstr "" +msgstr "WEI schauen" #: apps/wei/templates/wei/bus_detail.html:22 #: apps/wei/templates/wei/busteam_detail.html:22 msgid "Add team" -msgstr "" +msgstr "Neue Team" #: apps/wei/templates/wei/bus_detail.html:45 msgid "Members" -msgstr "" +msgstr "Mitglied" #: apps/wei/templates/wei/bus_detail.html:54 #: apps/wei/templates/wei/busteam_detail.html:60 #: apps/wei/templates/wei/weimembership_list.html:29 msgid "View as PDF" -msgstr "" +msgstr "Als PDF schauen" #: apps/wei/templates/wei/survey.html:11 #: apps/wei/templates/wei/survey_closed.html:11 #: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:978 #: apps/wei/views.py:1032 apps/wei/views.py:1042 msgid "Survey WEI" -msgstr "" +msgstr "WEI Umfrage" #: apps/wei/templates/wei/survey.html:23 msgid "Next" -msgstr "" +msgstr "Weiter" #: apps/wei/templates/wei/survey_closed.html:15 msgid "The inscription for this WEI are now closed." -msgstr "" +msgstr "Die Inschrift für diese WEI ist nun geschlossen." #: apps/wei/templates/wei/survey_closed.html:19 msgid "Return to WEI detail" -msgstr "" +msgstr "Zurück zur WEI Detaillen" #: apps/wei/templates/wei/survey_end.html:15 msgid "The survey is now ended. Your answers have been saved." -msgstr "" +msgstr "Die Umfrage ist nun beendet. Ihre Antworten wurden gespeichert." #: apps/wei/templates/wei/weiclub_detail.html:32 msgid "Register to the WEI! – 1A" -msgstr "" +msgstr "1A Registrieren" #: apps/wei/templates/wei/weiclub_detail.html:36 msgid "Register to the WEI! – 2A+" -msgstr "" +msgstr "2A+ Registrieren" #: apps/wei/templates/wei/weiclub_detail.html:40 msgid "Update my registration" -msgstr "" +msgstr "Meine Registrierung bearbeiten" #: apps/wei/templates/wei/weiclub_detail.html:63 msgid "Members of the WEI" -msgstr "" +msgstr "Mitglied der WEI" #: apps/wei/templates/wei/weiclub_detail.html:89 msgid "Unvalidated registrations" -msgstr "" +msgstr "Unvalidierte Registrierungen" #: apps/wei/templates/wei/weiclub_list.html:14 apps/wei/views.py:76 msgid "Create WEI" -msgstr "" +msgstr "Neue WEI" #: apps/wei/templates/wei/weiclub_list.html:22 msgid "WEI listing" -msgstr "" +msgstr "WEI List" #: apps/wei/templates/wei/weimembership_form.html:10 msgid "Review registration" -msgstr "" +msgstr "Registrierung schauen" #: apps/wei/templates/wei/weimembership_form.html:35 msgid "ENS year" -msgstr "" +msgstr "ENS Jahr" #: apps/wei/templates/wei/weimembership_form.html:76 msgid "Payment from Société générale" -msgstr "" +msgstr "Kredit von der Société générale" #: apps/wei/templates/wei/weimembership_form.html:80 msgid "Suggested bus from the survey:" -msgstr "" +msgstr "Vorgeschlagener Bus aus der Umfrage:" #: apps/wei/templates/wei/weimembership_form.html:85 msgid "Raw survey information" -msgstr "" +msgstr "Rohe Umfrageinformationen" #: apps/wei/templates/wei/weimembership_form.html:95 msgid "The algorithm didn't run." -msgstr "" +msgstr "Der Algorithmus wurde nicht ausgeführt." #: apps/wei/templates/wei/weimembership_form.html:98 msgid "caution check given" -msgstr "" +msgstr "Vorsichtsprüfung gegeben" #: apps/wei/templates/wei/weimembership_form.html:102 msgid "preferred bus" -msgstr "" +msgstr "bevorzugter Bus" #: apps/wei/templates/wei/weimembership_form.html:105 msgid "preferred team" -msgstr "" +msgstr "bevorzugtes Team" #: apps/wei/templates/wei/weimembership_form.html:108 msgid "preferred roles" -msgstr "" +msgstr "bevorzugte Rollen" #: apps/wei/templates/wei/weimembership_form.html:115 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:30 msgid "Update registration" -msgstr "" +msgstr "Registrierung aktualisieren" #: apps/wei/templates/wei/weimembership_form.html:131 msgid "The registration is already validated and can't be unvalidated." msgstr "" +"Die Registrierung ist bereits validiert und kann nicht ungültig gemacht " +"werden." #: apps/wei/templates/wei/weimembership_form.html:132 msgid "The user joined the bus" -msgstr "" +msgstr "Der Benutzer ist dem Bus beigetreten" #: apps/wei/templates/wei/weimembership_form.html:133 msgid "in the team" -msgstr "" +msgstr "In der Team" #: apps/wei/templates/wei/weimembership_form.html:134 msgid "in no team (staff)" -msgstr "" +msgstr "In keinem Team (staff)" #: apps/wei/templates/wei/weimembership_form.html:134 msgid "with the following roles:" -msgstr "" +msgstr "mit den folgenden Rollen:" #: apps/wei/templates/wei/weimembership_form.html:139 msgid "" @@ -2591,6 +2701,11 @@ msgid "" "created but will be invalid. You will have to validate it once the bank " "validated the creation of the account, or to change the payment method." msgstr "" +"Das WEI wird von der Société générale bezahlt. Die Mitgliedschaft wird auch " +"dann erstellt, wenn die Bank die BDE noch nicht bezahlt hat. Die " +"Mitgliedschaftstransaktion wird erstellt, ist jedoch ungültig. Sie müssen es " +"validieren, sobald die Bank die Erstellung des Kontos validiert hat, oder " +"die Zahlungsmethode ändern." #: apps/wei/templates/wei/weimembership_form.html:149 #, python-format @@ -2598,6 +2713,8 @@ msgid "" "The note don't have enough money (%(balance)s, %(pretty_fee)s required). The " "registration may fail if you don't credit the note now." msgstr "" +"Die Note hat nicht genug Geld (%(balance)s,%(pretty_fee)s erforderlich). Die " +"Registrierung kann fehlschlagen, wenn Sie die Note jetzt nicht gutschreiben." #: apps/wei/templates/wei/weimembership_form.html:157 #, python-format @@ -2605,10 +2722,12 @@ msgid "" "The note has enough money (%(pretty_fee)s required), the registration is " "possible." msgstr "" +"Die Note hat genug Geld (%(pretty_fee)s erforderlich), die Registrierung ist " +"möglich." #: apps/wei/templates/wei/weimembership_form.html:166 msgid "The user didn't give her/his caution check." -msgstr "" +msgstr "Der User hat nicht sein Vorsichtsprüfung gegeben." #: apps/wei/templates/wei/weimembership_form.html:174 msgid "" @@ -2616,14 +2735,18 @@ msgid "" "membership will be processed automatically, the WEI registration includes " "the membership fee." msgstr "" +"Dieser Benutzer ist für das kommende Jahr kein Mitglied des Kfet-Clubs. Die " +"Mitgliedschaft wird automatisch bearbeitet, die WEI-Registrierung beinhaltet " +"den Mitgliedsbeitrag." #: apps/wei/templates/wei/weimembership_list.html:23 msgid "View unvalidated registrations..." -msgstr "" +msgstr "Nicht validierte Registrierungen anzeigen ..." #: apps/wei/templates/wei/weiregistration_confirm_delete.html:16 msgid "This registration is already validated and can't be deleted." msgstr "" +"Diese Registrierung ist bereits validiert und kann nicht gelöscht werden." #: apps/wei/templates/wei/weiregistration_confirm_delete.html:23 #, python-format @@ -2631,124 +2754,128 @@ msgid "" "Are you sure you want to delete the registration of %(user)s for the WEI " "%(wei_name)s? This action can't be undone." msgstr "" +"Möchten Sie die Registrierung von %(user)s für die WEI %(wei_name)s wirklich " +"löschen? Diese Aktion kann nicht rückgängig gemacht werden." #: apps/wei/templates/wei/weiregistration_list.html:17 msgid "There is no pre-registration found with this pattern." -msgstr "" +msgstr "Bei diesem Muster wurde keine Vorregistrierung gefunden." #: apps/wei/templates/wei/weiregistration_list.html:23 msgid "View validated memberships..." -msgstr "" +msgstr "Validierte Mitgliedschaften anzeigen ..." #: apps/wei/views.py:55 msgid "Search WEI" -msgstr "" +msgstr "WEI finden" #: apps/wei/views.py:105 msgid "WEI Detail" -msgstr "" +msgstr "WEI Infos" #: apps/wei/views.py:200 msgid "View members of the WEI" -msgstr "" +msgstr "Mitglied der WEI schauen" #: apps/wei/views.py:228 msgid "Find WEI Membership" -msgstr "" +msgstr "WEI Mitgliedschaft finden" #: apps/wei/views.py:238 msgid "View registrations to the WEI" -msgstr "" +msgstr "Mitglied der WEI schauen" #: apps/wei/views.py:262 msgid "Find WEI Registration" -msgstr "" +msgstr "WEI Registrierung finden" #: apps/wei/views.py:273 msgid "Update the WEI" -msgstr "" +msgstr "WEI bearbeiten" #: apps/wei/views.py:294 msgid "Create new bus" -msgstr "" +msgstr "Neue Bus" #: apps/wei/views.py:332 msgid "Update bus" -msgstr "" +msgstr "Bus bearbeiten" #: apps/wei/views.py:362 msgid "Manage bus" -msgstr "" +msgstr "Bus ändern" #: apps/wei/views.py:389 msgid "Create new team" -msgstr "" +msgstr "Neue Bus Team" #: apps/wei/views.py:429 msgid "Update team" -msgstr "" +msgstr "Team bearbeiten" #: apps/wei/views.py:460 msgid "Manage WEI team" -msgstr "" +msgstr "WEI Team bearbeiten" #: apps/wei/views.py:482 msgid "Register first year student to the WEI" -msgstr "" +msgstr "Registrieren Sie den Erstsemester beim WEI" #: apps/wei/views.py:527 apps/wei/views.py:607 msgid "This user is already registered to this WEI." -msgstr "" +msgstr "Dieser Benutzer ist bereits bei dieser WEI registriert." #: apps/wei/views.py:532 msgid "" "This user can't be in her/his first year since he/she has already " "participated to a WEI." msgstr "" +"Dieser Benutzer kann nicht in seinem ersten Jahr sein, da er bereits an " +"einer WEI teilgenommen hat." #: apps/wei/views.py:549 msgid "Register old student to the WEI" -msgstr "" +msgstr "Registrieren Sie einen alten Studenten beim WEI" #: apps/wei/views.py:592 apps/wei/views.py:680 msgid "You already opened an account in the Société générale." -msgstr "" +msgstr "Sie haben bereits ein Konto in der Société générale eröffnet." #: apps/wei/views.py:637 msgid "Update WEI Registration" -msgstr "" +msgstr "WEI Registrierung aktualisieren" #: apps/wei/views.py:739 msgid "Delete WEI registration" -msgstr "" +msgstr "WEI Registrierung löschen" #: apps/wei/views.py:750 msgid "You don't have the right to delete this WEI registration." -msgstr "" +msgstr "Sie haben nicht das Recht, diese WEI-Registrierung zu löschen." #: apps/wei/views.py:769 msgid "Validate WEI registration" -msgstr "" +msgstr "Überprüfen Sie die WEI-Registrierung" #: apps/wei/views.py:916 msgid "This user didn't give her/his caution check." -msgstr "" +msgstr "Dieser User hat seine / ihre Vorsicht nicht überprüft." #: note_kfet/settings/base.py:155 msgid "German" -msgstr "" +msgstr "Deutsch" #: note_kfet/settings/base.py:156 msgid "English" -msgstr "" +msgstr "English" #: note_kfet/settings/base.py:157 msgid "French" -msgstr "" +msgstr "Französich" #: note_kfet/templates/400.html:10 msgid "Bad request" -msgstr "" +msgstr "Ungültige Anfrage" #: note_kfet/templates/400.html:14 msgid "" @@ -2756,32 +2883,37 @@ msgid "" "been sent to webmasters with the details of the error. You can now drink a " "coke." msgstr "" +"Entschuldigung, Ihre Anfrage war ungültig. Ich weiß nicht, was falsch sein " +"könnte. An Webmaster wurde eine E-Mail mit den Details des Fehlers gesendet. " +"Sie können jetzt eine Cola trinken." #: note_kfet/templates/403.html:10 msgid "Permission denied" -msgstr "" +msgstr "Zugang verweigert" #: note_kfet/templates/403.html:13 msgid "You don't have the right to perform this request." -msgstr "" +msgstr "Sie haben nicht das Recht, diese Anfrage auszuführen." #: note_kfet/templates/403.html:15 note_kfet/templates/404.html:19 msgid "Exception message:" -msgstr "" +msgstr "Ausnahmemeldung:" #: note_kfet/templates/404.html:10 msgid "Page not found" -msgstr "" +msgstr "Seite nicht gefunden" #: note_kfet/templates/404.html:14 #, python-format msgid "" "The requested path %(request_path)s was not found on the server." msgstr "" +"Der angeforderte Pfad %(request_path) s wurde auf dem Server " +"nicht gefunden." #: note_kfet/templates/500.html:10 msgid "Server error" -msgstr "" +msgstr "Serverfehler" #: note_kfet/templates/500.html:14 msgid "" @@ -2789,44 +2921,49 @@ msgid "" "sent to webmasters with the detail of the error, and this will be fixed " "soon. You can now drink a beer." msgstr "" +"Bei der Bearbeitung Ihrer Anfrage ist leider ein Fehler aufgetreten. Es " +"wurde eine E-Mail mit den Details des Fehlers an die Webmaster gesendet, die " +"in Kürze behoben wird. Sie können jetzt ein Bier trinken." #: note_kfet/templates/autocomplete_model.html:14 msgid "Reset" -msgstr "" +msgstr "Reset" #: note_kfet/templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." -msgstr "" +msgstr "Die BDE ENS-Paris-Saclay Note." -#: note_kfet/templates/base.html:84 +#: note_kfet/templates/base.html:75 msgid "Users" -msgstr "" +msgstr "Users" -#: note_kfet/templates/base.html:90 +#: note_kfet/templates/base.html:81 msgid "Clubs" -msgstr "" +msgstr "CLubs" -#: note_kfet/templates/base.html:119 +#: note_kfet/templates/base.html:110 msgid "Admin" -msgstr "" +msgstr "Admin" -#: note_kfet/templates/base.html:163 +#: note_kfet/templates/base.html:154 msgid "" "Your e-mail address is not validated. Please check your mail inbox and click " "on the validation link." msgstr "" +"Ihre E-Mail-Adresse ist nicht validiert. Bitte überprüfen Sie Ihren " +"Posteingang und klicken Sie auf den Validierungslink." #: note_kfet/templates/base_search.html:15 msgid "Search by attribute such as name…" -msgstr "" +msgstr "Suche nach Attributen wie Name…" #: note_kfet/templates/base_search.html:23 msgid "There is no results." -msgstr "" +msgstr "Es gibt keine Ergebnisse." #: note_kfet/templates/cas_server/base.html:7 msgid "Central Authentication Service" -msgstr "" +msgstr "Central Authentication Service" #: note_kfet/templates/cas_server/base.html:43 #, python-format @@ -2835,21 +2972,25 @@ msgid "" "%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider " "upgrading." msgstr "" +"Eine neue Version der Anwendung ist verfügbar. Diese Instanz führt " +"%(VERSION) s aus und die letzte Version ist %(LAST_VERSION) s. Bitte erwägen " +"Sie ein Upgrade." #: note_kfet/templates/registration/logged_out.html:13 msgid "Thanks for spending some quality time with the Web site today." msgstr "" +"Vielen Dank, dass Sie heute einige Zeit mit dieser Website verbracht haben." #: note_kfet/templates/registration/logged_out.html:14 msgid "Log in again" -msgstr "" +msgstr "Nochmal anmelden" #: note_kfet/templates/registration/login.html:6 #: note_kfet/templates/registration/login.html:15 #: note_kfet/templates/registration/login.html:38 #: note_kfet/templates/registration/password_reset_complete.html:15 msgid "Log in" -msgstr "" +msgstr "Anmelden" #: note_kfet/templates/registration/login.html:20 #, python-format @@ -2858,75 +2999,95 @@ msgid "" "page. Would you like to login to a different account, or with a higher " "permission mask?" msgstr "" +"Sie sind als %(username)s authentifiziert, aber nicht berechtigt, auf diese " +"Seite zuzugreifen. Möchten Sie sich bei einem anderen Konto oder mit einer " +"Maske mit höherer Berechtigung anmelden?" #: note_kfet/templates/registration/login.html:30 msgid "" "You must be logged with a staff account with the higher mask to access " "Django Admin." msgstr "" +"Sie müssen mit einem staffkonto mit der höheren Maske angemeldet sein, um " +"auf Django Admin zugreifen zu können." #: note_kfet/templates/registration/login.html:40 msgid "Forgotten your password or username?" -msgstr "" +msgstr "Passwort oder Username vergessen?" #: note_kfet/templates/registration/password_change_done.html:13 msgid "Your password was changed." -msgstr "" +msgstr "Ihr Passwort wurde geändert." #: note_kfet/templates/registration/password_change_form.html:14 msgid "" "Please enter your old password, for security's sake, and then enter your new " "password twice so we can verify you typed it in correctly." msgstr "" +"Bitte geben Sie aus Sicherheitsgründen Ihr altes Passwort ein und geben Sie " +"dann Ihr neues Passwort zweimal ein, damit wir überprüfen können, ob Sie es " +"richtig eingegeben haben." #: note_kfet/templates/registration/password_change_form.html:16 #: note_kfet/templates/registration/password_reset_confirm.html:17 msgid "Change my password" -msgstr "" +msgstr "Mein Passwort ändern" #: note_kfet/templates/registration/password_reset_complete.html:13 msgid "Your password has been set. You may go ahead and log in now." -msgstr "" +msgstr "Ihr Passwort wurde festgelegt. Sie können sich jetzt anmelden." #: note_kfet/templates/registration/password_reset_confirm.html:14 msgid "" "Please enter your new password twice so we can verify you typed it in " "correctly." msgstr "" +"Bitte geben Sie Ihr neues Passwort zweimal ein, damit wir überprüfen können, " +"ob Sie es richtig eingegeben haben." #: note_kfet/templates/registration/password_reset_confirm.html:21 msgid "" "The password reset link was invalid, possibly because it has already been " "used. Please request a new password reset." msgstr "" +"Der Link zum Zurücksetzen des Passworts war ungültig, möglicherweise weil er " +"bereits verwendet wurde. Bitte fordern Sie ein neues Passwort änderung." #: note_kfet/templates/registration/password_reset_done.html:13 msgid "" "We've emailed you instructions for setting your password, if an account " "exists with the email you entered. You should receive them shortly." msgstr "" +"Wir haben Ihnen Anweisungen zum Festlegen Ihres Passworts per E-Mail " +"gesendet, falls ein Konto mit der von Ihnen eingegebenen E-Mail-Adresse " +"vorhanden ist. Sie sollten sie in Kürze erhalten." #: note_kfet/templates/registration/password_reset_done.html:14 msgid "" "If you don't receive an email, please make sure you've entered the address " "you registered with, and check your spam folder." msgstr "" +"Wenn Sie keine E-Mail erhalten, stellen Sie sicher, dass Sie die Adresse " +"eingegeben haben, unter der Sie sich registriert haben, und überprüfen Sie " +"Ihren Spam-Ordner." #: note_kfet/templates/registration/password_reset_form.html:13 msgid "" "Forgotten your password? Enter your email address below, and we'll email " "instructions for setting a new one." msgstr "" +"Passwort vergessen? Geben Sie unten Ihre E-Mail-Adresse ein, und wir senden " +"Ihnen Anweisungen zum Festlegen einer neuen E-Mail." #: note_kfet/templates/registration/password_reset_form.html:18 msgid "Reset my password" -msgstr "" +msgstr "Mein Passwort zurücksetzen" #: note_kfet/templates/registration/signup.html:6 #: note_kfet/templates/registration/signup.html:11 #: note_kfet/templates/registration/signup.html:27 msgid "Sign up" -msgstr "" +msgstr "Registrieren" #: note_kfet/templates/registration/signup.html:15 msgid "" @@ -2935,3 +3096,8 @@ msgid "" "Kfet and pay the registration fee. You must also validate your email address " "by following the link you received." msgstr "" +"Wenn Sie sich bereits registriert haben, wird Ihre Registrierung " +"berücksichtigt. Die BDE muss Ihr Konto validieren, bevor Sie sich anmelden " +"können. Sie müssen zum Kfet gehen und die Registrierungbeitrag bezahlen. Sie " +"müssen Ihre E-Mail-Adresse auch überprüfen, indem Sie dem Link folgen, den " +"Sie erhalten haben." diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 0335d24a..53b84ef2 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,20 +3,20 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-09-01 10:28+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"POT-Creation-Date: 2020-09-04 07:44+0200\n" +"PO-Revision-Date: 2020-09-02 23:18+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 2.3\n" #: apps/activity/apps.py:10 apps/activity/models.py:145 #: apps/activity/models.py:161 @@ -49,11 +49,11 @@ msgid "You can't invite more than 3 people to this activity." msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." #: apps/activity/models.py:27 apps/activity/models.py:62 -#: apps/member/models.py:198 +#: apps/member/models.py:200 #: apps/member/templates/member/includes/club_info.html:4 #: apps/member/templates/member/includes/profile_info.html:4 -#: apps/note/models/notes.py:250 apps/note/models/transactions.py:26 -#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:297 +#: apps/note/models/notes.py:247 apps/note/models/transactions.py:26 +#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:302 #: apps/permission/models.py:329 #: apps/registration/templates/registration/future_profile_detail.html:16 #: apps/wei/models.py:66 apps/wei/models.py:118 @@ -105,13 +105,13 @@ msgstr "Lieu où l'activité est organisée, par exemple la Kfet." #: apps/activity/models.py:82 #: apps/activity/templates/activity/includes/activity_info.html:22 -#: apps/note/models/notes.py:226 apps/note/models/transactions.py:66 +#: apps/note/models/notes.py:223 apps/note/models/transactions.py:66 #: apps/permission/models.py:164 msgid "type" msgstr "type" -#: apps/activity/models.py:88 apps/logs/models.py:22 apps/member/models.py:303 -#: apps/note/models/notes.py:142 apps/treasury/models.py:267 +#: apps/activity/models.py:88 apps/logs/models.py:22 apps/member/models.py:305 +#: apps/note/models/notes.py:138 apps/treasury/models.py:267 #: apps/treasury/templates/treasury/sogecredit_detail.html:14 #: apps/wei/models.py:160 apps/wei/templates/wei/survey.html:15 msgid "user" @@ -167,7 +167,7 @@ msgid "entry time" msgstr "heure d'entrée" #: apps/activity/models.py:172 apps/note/apps.py:14 -#: apps/note/models/notes.py:76 +#: apps/note/models/notes.py:78 msgid "note" msgstr "note" @@ -240,7 +240,7 @@ msgstr "Entré le " msgid "remove" msgstr "supprimer" -#: apps/activity/tables.py:80 apps/note/forms.py:66 apps/treasury/models.py:186 +#: apps/activity/tables.py:80 apps/note/forms.py:68 apps/treasury/models.py:186 msgid "Type" msgstr "Type" @@ -257,7 +257,7 @@ msgstr "Nom de famille" msgid "First name" msgstr "Prénom" -#: apps/activity/tables.py:86 apps/note/models/notes.py:85 +#: apps/activity/tables.py:86 apps/note/models/notes.py:87 msgid "Note" msgstr "Note" @@ -273,18 +273,18 @@ msgstr "Liste des invités" #: apps/note/models/transactions.py:259 #: apps/note/templates/note/transaction_form.html:16 #: apps/note/templates/note/transaction_form.html:148 -#: note_kfet/templates/base.html:78 +#: note_kfet/templates/base.html:69 msgid "Transfer" msgstr "Virement" #: apps/activity/templates/activity/activity_entry.html:18 -#: apps/note/models/transactions.py:313 +#: apps/note/models/transactions.py:318 #: apps/note/templates/note/transaction_form.html:21 msgid "Credit" msgstr "Crédit" #: apps/activity/templates/activity/activity_entry.html:21 -#: apps/note/models/transactions.py:313 +#: apps/note/models/transactions.py:318 #: apps/note/templates/note/transaction_form.html:25 msgid "Debit" msgstr "Débit" @@ -301,7 +301,7 @@ msgstr "Retour à la page de l'activité" #: apps/activity/templates/activity/activity_form.html:16 #: apps/member/templates/member/add_members.html:32 #: apps/member/templates/member/club_form.html:16 -#: apps/note/templates/note/transactiontemplate_form.html:15 +#: apps/note/templates/note/transactiontemplate_form.html:18 #: apps/treasury/forms.py:93 apps/treasury/forms.py:147 #: apps/treasury/templates/treasury/invoice_form.html:74 #: apps/wei/templates/wei/bus_form.html:17 @@ -333,7 +333,7 @@ msgstr "Toutes les activités" #: apps/activity/templates/activity/includes/activity_info.html:32 msgid "creater" -msgstr "Créateur" +msgstr "créateur" #: apps/activity/templates/activity/includes/activity_info.html:53 msgid "opened" @@ -356,9 +356,9 @@ msgid "validate" msgstr "valider" #: apps/activity/templates/activity/includes/activity_info.html:71 -#: apps/logs/models.py:62 apps/note/tables.py:169 +#: apps/logs/models.py:62 apps/note/tables.py:194 msgid "edit" -msgstr "Modifier" +msgstr "modifier" #: apps/activity/templates/activity/includes/activity_info.html:74 msgid "Invite" @@ -368,7 +368,7 @@ msgstr "Inviter" msgid "Create new activity" msgstr "Créer une nouvelle activité" -#: apps/activity/views.py:59 note_kfet/templates/base.html:96 +#: apps/activity/views.py:59 note_kfet/templates/base.html:87 msgid "Activities" msgstr "Activités" @@ -416,37 +416,37 @@ msgstr "Adresse IP" #: apps/logs/models.py:36 apps/permission/models.py:134 msgid "model" -msgstr "Modèle" +msgstr "modèle" #: apps/logs/models.py:43 msgid "identifier" -msgstr "Identifiant" +msgstr "identifiant" #: apps/logs/models.py:48 msgid "previous data" -msgstr "Données précédentes" +msgstr "données précédentes" #: apps/logs/models.py:53 msgid "new data" -msgstr "Nouvelles données" +msgstr "ouvelles données" #: apps/logs/models.py:61 msgid "create" -msgstr "Créer" +msgstr "créer" -#: apps/logs/models.py:63 apps/note/tables.py:139 apps/note/tables.py:175 +#: apps/logs/models.py:63 apps/note/tables.py:164 apps/note/tables.py:200 #: apps/permission/models.py:127 apps/treasury/tables.py:38 #: apps/wei/tables.py:75 msgid "delete" -msgstr "Supprimer" +msgstr "supprimer" #: apps/logs/models.py:66 msgid "action" -msgstr "Action" +msgstr "action" #: apps/logs/models.py:74 msgid "timestamp" -msgstr "Date" +msgstr "date" #: apps/logs/models.py:78 msgid "Logs cannot be destroyed." @@ -460,21 +460,21 @@ msgstr "journal de modification" msgid "changelogs" msgstr "journaux de modifications" -#: apps/member/admin.py:52 apps/member/models.py:225 +#: apps/member/admin.py:50 apps/member/models.py:227 #: apps/member/templates/member/includes/club_info.html:34 msgid "membership fee (paid students)" msgstr "cotisation pour adhérer (normalien élève)" -#: apps/member/admin.py:53 apps/member/models.py:230 +#: apps/member/admin.py:51 apps/member/models.py:232 #: apps/member/templates/member/includes/club_info.html:37 msgid "membership fee (unpaid students)" msgstr "cotisation pour adhérer (normalien étudiant)" -#: apps/member/admin.py:67 apps/member/models.py:314 +#: apps/member/admin.py:65 apps/member/models.py:316 msgid "roles" msgstr "rôles" -#: apps/member/admin.py:68 apps/member/models.py:328 +#: apps/member/admin.py:66 apps/member/models.py:330 msgid "fee" msgstr "cotisation" @@ -496,14 +496,14 @@ msgstr "Vous ne pouvez pas vous inscrire à la note si vous venez du futur." #: apps/member/forms.py:73 msgid "select an image" -msgstr "Choisissez une image" +msgstr "choisissez une image" #: apps/member/forms.py:74 msgid "Maximal size: 2MB" msgstr "Taille maximale : 2 Mo" -#: apps/member/forms.py:87 apps/member/views.py:101 -#: apps/registration/forms.py:33 +#: apps/member/forms.py:87 apps/member/views.py:100 +#: apps/registration/forms.py:33 apps/registration/views.py:237 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." @@ -720,7 +720,7 @@ msgstr "profil utilisateur" msgid "Activate your Note Kfet account" msgstr "Activez votre compte Note Kfet" -#: apps/member/models.py:203 +#: apps/member/models.py:205 #: apps/member/templates/member/includes/club_info.html:55 #: apps/member/templates/member/includes/profile_info.html:31 #: apps/registration/templates/registration/future_profile_detail.html:22 @@ -729,88 +729,88 @@ msgstr "Activez votre compte Note Kfet" msgid "email" msgstr "courriel" -#: apps/member/models.py:210 +#: apps/member/models.py:212 msgid "parent club" msgstr "club parent" -#: apps/member/models.py:219 +#: apps/member/models.py:221 msgid "require memberships" msgstr "nécessite des adhésions" -#: apps/member/models.py:220 +#: apps/member/models.py:222 msgid "Uncheck if this club don't require memberships." msgstr "Décochez si ce club n'utilise pas d'adhésions." -#: apps/member/models.py:236 +#: apps/member/models.py:238 #: apps/member/templates/member/includes/club_info.html:26 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:237 +#: apps/member/models.py:239 msgid "The longest time (in days) a membership can last (NULL = infinite)." msgstr "La durée maximale (en jours) d'une adhésion (NULL = infinie)." -#: apps/member/models.py:244 +#: apps/member/models.py:246 #: apps/member/templates/member/includes/club_info.html:16 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:245 +#: apps/member/models.py:247 msgid "Date from which the members can renew their membership." msgstr "" "Date à partir de laquelle les adhérents peuvent renouveler leur adhésion." -#: apps/member/models.py:251 +#: apps/member/models.py:253 #: apps/member/templates/member/includes/club_info.html:21 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:252 +#: apps/member/models.py:254 msgid "Maximal date of a membership, after which members must renew it." msgstr "" "Date maximale d'une fin d'adhésion, après laquelle les adhérents doivent la " "renouveler." -#: apps/member/models.py:284 apps/member/models.py:309 -#: apps/note/models/notes.py:183 +#: apps/member/models.py:286 apps/member/models.py:311 +#: apps/note/models/notes.py:179 msgid "club" msgstr "club" -#: apps/member/models.py:285 +#: apps/member/models.py:287 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:319 +#: apps/member/models.py:321 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:323 +#: apps/member/models.py:325 msgid "membership ends on" msgstr "l'adhésion finit le" -#: apps/member/models.py:374 +#: apps/member/models.py:375 #, python-brace-format msgid "The role {role} does not apply to the club {club}." msgstr "Le rôle {role} ne s'applique pas au club {club}." -#: apps/member/models.py:385 apps/member/views.py:676 +#: apps/member/models.py:384 apps/member/views.py:669 msgid "User is already a member of the club" msgstr "L'utilisateur est déjà membre du club" -#: apps/member/models.py:433 +#: apps/member/models.py:432 msgid "User is not a member of the parent club" msgstr "L'utilisateur n'est pas membre du club parent" -#: apps/member/models.py:486 +#: apps/member/models.py:480 #, python-brace-format msgid "Membership of {user} for the club {club}" msgstr "Adhésion de {user} pour le club {club}" -#: apps/member/models.py:489 +#: apps/member/models.py:483 apps/note/models/transactions.py:359 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:490 +#: apps/member/models.py:484 msgid "memberships" msgstr "adhésions" @@ -904,8 +904,8 @@ msgstr "" "à nouveau possible." #: apps/member/templates/member/club_alias.html:10 -#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:240 -#: apps/member/views.py:450 +#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:238 +#: apps/member/views.py:443 msgid "Note aliases" msgstr "Alias de la note" @@ -969,7 +969,7 @@ msgstr "solde du compte" #: apps/member/templates/member/includes/club_info.html:47 #: apps/member/templates/member/includes/profile_info.html:20 -#: apps/note/models/notes.py:273 apps/wei/templates/wei/base.html:66 +#: apps/note/models/notes.py:270 apps/wei/templates/wei/base.html:66 msgid "aliases" msgstr "alias" @@ -992,10 +992,9 @@ msgstr "mot de passe" msgid "Change password" msgstr "Changer le mot de passe" -#: apps/member/templates/member/includes/profile_info.html:52 -#: apps/member/views.py:318 -msgid "Manage auth token" -msgstr "Gérer les jetons d'authentification" +#: apps/member/templates/member/includes/profile_info.html:53 +msgid "API token" +msgstr "Acces API" #: apps/member/templates/member/manage_auth_tokens.html:19 msgid "Token" @@ -1037,39 +1036,43 @@ msgstr "Inscriptions" msgid "This address must be valid." msgstr "Cette adresse doit être valide." -#: apps/member/views.py:138 +#: apps/member/views.py:137 msgid "Profile detail" msgstr "Détails de l'utilisateur" -#: apps/member/views.py:201 +#: apps/member/views.py:197 msgid "Search user" msgstr "Chercher un utilisateur" -#: apps/member/views.py:260 +#: apps/member/views.py:258 msgid "Update note picture" msgstr "Modifier la photo de la note" -#: apps/member/views.py:346 +#: apps/member/views.py:311 +msgid "Manage auth token" +msgstr "Gérer les jetons d'authentification" + +#: apps/member/views.py:338 msgid "Create new club" msgstr "Créer un nouveau club" -#: apps/member/views.py:364 +#: apps/member/views.py:357 msgid "Search club" msgstr "Chercher un club" -#: apps/member/views.py:397 +#: apps/member/views.py:390 msgid "Club detail" msgstr "Détails du club" -#: apps/member/views.py:473 +#: apps/member/views.py:466 msgid "Update club" msgstr "Modifier le club" -#: apps/member/views.py:507 +#: apps/member/views.py:500 msgid "Add new member to the club" msgstr "Ajouter un nouveau membre au club" -#: apps/member/views.py:667 apps/wei/views.py:922 +#: apps/member/views.py:660 apps/wei/views.py:922 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -1077,43 +1080,43 @@ msgstr "" "Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " "avoir un solde négatif." -#: apps/member/views.py:680 +#: apps/member/views.py:673 msgid "The membership must start after {:%m-%d-%Y}." msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}." -#: apps/member/views.py:685 +#: apps/member/views.py:678 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." -#: apps/member/views.py:701 apps/member/views.py:703 apps/member/views.py:705 -#: apps/registration/views.py:292 apps/registration/views.py:294 -#: apps/registration/views.py:296 apps/wei/views.py:927 apps/wei/views.py:931 +#: apps/member/views.py:694 apps/member/views.py:696 apps/member/views.py:698 +#: apps/registration/views.py:287 apps/registration/views.py:289 +#: apps/registration/views.py:291 apps/wei/views.py:927 apps/wei/views.py:931 msgid "This field is required." msgstr "Ce champ est requis." -#: apps/member/views.py:789 +#: apps/member/views.py:771 msgid "Manage roles of an user in the club" msgstr "Gérer les rôles d'un utilisateur dans le club" -#: apps/member/views.py:814 +#: apps/member/views.py:796 msgid "Members of the club" msgstr "Membres du club" -#: apps/note/admin.py:133 apps/note/models/transactions.py:106 +#: apps/note/admin.py:129 apps/note/models/transactions.py:106 msgid "source" msgstr "source" -#: apps/note/admin.py:141 apps/note/admin.py:191 +#: apps/note/admin.py:137 apps/note/admin.py:205 #: apps/note/models/transactions.py:56 apps/note/models/transactions.py:119 msgid "destination" msgstr "destination" -#: apps/note/admin.py:196 apps/note/models/transactions.py:60 +#: apps/note/admin.py:210 apps/note/models/transactions.py:60 #: apps/note/models/transactions.py:137 msgid "amount" msgstr "montant" -#: apps/note/api/serializers.py:177 apps/note/api/serializers.py:183 +#: apps/note/api/serializers.py:178 apps/note/api/serializers.py:184 #: apps/note/models/transactions.py:224 msgid "" "The transaction can't be saved since the source note or the destination note " @@ -1122,74 +1125,74 @@ msgstr "" "La transaction ne peut pas être sauvegardée puisque la note source ou la " "note de destination n'est pas active." -#: apps/note/forms.py:37 +#: apps/note/forms.py:39 msgid "Source" msgstr "Source" -#: apps/note/forms.py:51 +#: apps/note/forms.py:53 msgid "Destination" msgstr "Destination" -#: apps/note/forms.py:72 apps/note/templates/note/transaction_form.html:119 +#: apps/note/forms.py:74 apps/note/templates/note/transaction_form.html:119 msgid "Reason" msgstr "Raison" -#: apps/note/forms.py:77 apps/treasury/tables.py:139 +#: apps/note/forms.py:79 apps/treasury/tables.py:139 msgid "Valid" msgstr "Valide" -#: apps/note/forms.py:83 +#: apps/note/forms.py:85 msgid "Total amount greater than" msgstr "Montant total supérieur à" -#: apps/note/forms.py:91 +#: apps/note/forms.py:93 msgid "Total amount less than" msgstr "Montant total inférieur à" -#: apps/note/forms.py:97 +#: apps/note/forms.py:99 msgid "Created after" msgstr "Créé après" -#: apps/note/forms.py:104 +#: apps/note/forms.py:106 msgid "Created before" msgstr "Créé avant" -#: apps/note/models/notes.py:30 +#: apps/note/models/notes.py:32 msgid "account balance" msgstr "solde du compte" -#: apps/note/models/notes.py:31 +#: apps/note/models/notes.py:33 msgid "in centimes, money credited for this instance" msgstr "en centimes, argent crédité pour cette instance" -#: apps/note/models/notes.py:36 +#: apps/note/models/notes.py:38 msgid "last negative date" msgstr "dernier date de négatif" -#: apps/note/models/notes.py:37 +#: apps/note/models/notes.py:39 msgid "last time the balance was negative" msgstr "dernier instant où la note était en négatif" -#: apps/note/models/notes.py:43 +#: apps/note/models/notes.py:45 msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:52 apps/note/models/transactions.py:129 +#: apps/note/models/notes.py:54 apps/note/models/transactions.py:129 msgid "created at" msgstr "créée le" -#: apps/note/models/notes.py:57 +#: apps/note/models/notes.py:59 msgid "active" msgstr "actif" -#: apps/note/models/notes.py:60 +#: apps/note/models/notes.py:62 msgid "" "Designates whether this note should be treated as active. Unselect this " "instead of deleting notes." msgstr "" "Indique si la note est active. Désactiver cela plutôt que supprimer la note." -#: apps/note/models/notes.py:67 +#: apps/note/models/notes.py:69 msgid "" "The user blocked his/her note manually, eg. when he/she left the school for " "holidays. It can be reactivated at any time." @@ -1197,78 +1200,78 @@ msgstr "" "La note a été bloquée manuellement, par exemple pour des raisons de " "vacances. Elle peut être débloquée à tout moment." -#: apps/note/models/notes.py:69 +#: apps/note/models/notes.py:71 msgid "The note is blocked by the the BDE and can't be manually reactivated." msgstr "" "La note est bloquée de force par le BDE et ne peut pas être débloquée par le " "possesseur de la note." -#: apps/note/models/notes.py:77 +#: apps/note/models/notes.py:79 msgid "notes" msgstr "notes" -#: apps/note/models/notes.py:102 apps/note/models/notes.py:126 +#: apps/note/models/notes.py:122 msgid "This alias is already taken." msgstr "Cet alias est déjà pris." -#: apps/note/models/notes.py:146 +#: apps/note/models/notes.py:142 msgid "one's note" msgstr "note d'un utilisateur" -#: apps/note/models/notes.py:147 +#: apps/note/models/notes.py:143 msgid "users note" msgstr "notes des utilisateurs" -#: apps/note/models/notes.py:153 +#: apps/note/models/notes.py:149 #, python-format msgid "%(user)s's note" msgstr "Note de %(user)s" -#: apps/note/models/notes.py:187 +#: apps/note/models/notes.py:183 msgid "club note" msgstr "note d'un club" -#: apps/note/models/notes.py:188 +#: apps/note/models/notes.py:184 msgid "clubs notes" msgstr "notes des clubs" -#: apps/note/models/notes.py:194 +#: apps/note/models/notes.py:190 #, python-format msgid "Note of %(club)s club" msgstr "Note du club %(club)s" -#: apps/note/models/notes.py:232 +#: apps/note/models/notes.py:229 msgid "special note" msgstr "note spéciale" -#: apps/note/models/notes.py:233 +#: apps/note/models/notes.py:230 msgid "special notes" msgstr "notes spéciales" -#: apps/note/models/notes.py:256 +#: apps/note/models/notes.py:253 msgid "Invalid alias" msgstr "Alias invalide" -#: apps/note/models/notes.py:272 +#: apps/note/models/notes.py:269 msgid "alias" msgstr "alias" -#: apps/note/models/notes.py:296 +#: apps/note/models/notes.py:293 msgid "Alias is too long." msgstr "L'alias est trop long." -#: apps/note/models/notes.py:299 +#: apps/note/models/notes.py:296 msgid "" "This alias contains only complex character. Please use a more simple alias." msgstr "" "Cet alias ne contient que des caractères complexes. Merci d'utiliser un " "alias plus simple." -#: apps/note/models/notes.py:303 +#: apps/note/models/notes.py:300 msgid "An alias with a similar name already exists: {} " -msgstr "Un alias avec un nom similaire existe déjà : {}" +msgstr "Un alias avec un nom similaire existe déjà : {} " -#: apps/note/models/notes.py:316 +#: apps/note/models/notes.py:313 msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." @@ -1282,7 +1285,7 @@ msgstr "catégories de transaction" #: apps/note/models/transactions.py:49 msgid "A template with this name already exist" -msgstr "Un modèle de transaction avec un nom similaire existe déjà." +msgstr "Un modèle de transaction avec un nom similaire existe déjà" #: apps/note/models/transactions.py:72 msgid "display" @@ -1294,14 +1297,14 @@ msgstr "mis en avant" #: apps/note/models/transactions.py:87 msgid "transaction template" -msgstr "Modèle de transaction" +msgstr "modèle de transaction" #: apps/note/models/transactions.py:88 msgid "transaction templates" -msgstr "Modèles de transaction" +msgstr "modèles de transaction" #: apps/note/models/transactions.py:112 apps/note/models/transactions.py:125 -#: apps/note/tables.py:34 apps/note/tables.py:43 +#: apps/note/tables.py:34 apps/note/tables.py:44 msgid "used alias" msgstr "alias utilisé" @@ -1313,18 +1316,18 @@ msgstr "quantité" msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:151 apps/note/tables.py:114 +#: apps/note/models/transactions.py:151 apps/note/tables.py:139 msgid "invalidity reason" -msgstr "Motif d'invalidité" +msgstr "motif d'invalidité" #: apps/note/models/transactions.py:159 msgid "transaction" -msgstr "Transaction" +msgstr "transaction" #: apps/note/models/transactions.py:160 #: apps/treasury/templates/treasury/sogecredit_detail.html:22 msgid "transactions" -msgstr "Transactions" +msgstr "transactions" #: apps/note/models/transactions.py:182 #, python-brace-format @@ -1344,63 +1347,71 @@ msgstr "" "€ et 92 233 720 368 547 758.07 €. Ne cherchez pas à capitaliser l'argent du " "BDE." -#: apps/note/models/transactions.py:283 +#: apps/note/models/transactions.py:279 +msgid "" +"The destination of this transaction must equal to the destination of the " +"template." +msgstr "" +"Le destinataire de cette transaction doit être identique à celui du bouton " +"utilisé." + +#: apps/note/models/transactions.py:288 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:286 +#: apps/note/models/transactions.py:291 msgid "recurrent transaction" -msgstr "Transaction issue de bouton" +msgstr "transaction issue de bouton" -#: apps/note/models/transactions.py:287 +#: apps/note/models/transactions.py:292 msgid "recurrent transactions" -msgstr "Transactions issues de boutons" +msgstr "transactions issues de boutons" -#: apps/note/models/transactions.py:302 +#: apps/note/models/transactions.py:307 msgid "first_name" msgstr "prénom" -#: apps/note/models/transactions.py:307 +#: apps/note/models/transactions.py:312 msgid "bank" msgstr "banque" -#: apps/note/models/transactions.py:324 +#: apps/note/models/transactions.py:329 msgid "" "A special transaction is only possible between a Note associated to a " "payment method and a User or a Club" msgstr "" "Une transaction spéciale n'est possible que entre une note associée à un " -"mode de paiement et un utilisateur ou un club." +"mode de paiement et un utilisateur ou un club" -#: apps/note/models/transactions.py:328 +#: apps/note/models/transactions.py:337 msgid "Special transaction" msgstr "Transaction de crédit/retrait" -#: apps/note/models/transactions.py:329 +#: apps/note/models/transactions.py:338 msgid "Special transactions" msgstr "Transactions de crédit/retrait" -#: apps/note/models/transactions.py:345 apps/note/models/transactions.py:350 +#: apps/note/models/transactions.py:354 msgid "membership transaction" -msgstr "Transaction d'adhésion" +msgstr "transaction d'adhésion" -#: apps/note/models/transactions.py:346 apps/treasury/models.py:273 +#: apps/note/models/transactions.py:355 apps/treasury/models.py:273 msgid "membership transactions" -msgstr "Transactions d'adhésion" +msgstr "transactions d'adhésion" -#: apps/note/tables.py:62 +#: apps/note/tables.py:93 msgid "Click to invalidate" msgstr "Cliquez pour dévalider" -#: apps/note/tables.py:62 +#: apps/note/tables.py:93 msgid "Click to validate" msgstr "Cliquez pour valider" -#: apps/note/tables.py:112 +#: apps/note/tables.py:137 msgid "No reason specified" msgstr "Pas de motif spécifié" -#: apps/note/tables.py:143 apps/note/tables.py:177 apps/treasury/tables.py:39 +#: apps/note/tables.py:168 apps/note/tables.py:202 apps/treasury/tables.py:39 #: apps/treasury/templates/treasury/invoice_confirm_delete.html:30 #: apps/treasury/templates/treasury/sogecredit_detail.html:59 #: apps/wei/tables.py:76 apps/wei/tables.py:103 @@ -1408,7 +1419,7 @@ msgstr "Pas de motif spécifié" msgid "Delete" msgstr "Supprimer" -#: apps/note/tables.py:171 apps/note/templates/note/conso_form.html:132 +#: apps/note/tables.py:196 apps/note/templates/note/conso_form.html:132 #: apps/wei/tables.py:47 apps/wei/tables.py:48 #: apps/wei/templates/wei/base.html:89 #: apps/wei/templates/wei/bus_detail.html:20 @@ -1504,19 +1515,19 @@ msgstr "Sélection du destinataire" msgid "Transfer type" msgstr "Type de transfert" -#: apps/note/templates/note/transactiontemplate_form.html:10 +#: apps/note/templates/note/transactiontemplate_form.html:8 msgid "Buttons list" msgstr "Liste des boutons" -#: apps/note/templates/note/transactiontemplate_form.html:21 +#: apps/note/templates/note/transactiontemplate_form.html:24 msgid "Price history" msgstr "Historique des prix" -#: apps/note/templates/note/transactiontemplate_form.html:24 +#: apps/note/templates/note/transactiontemplate_form.html:27 msgid "Obsolete since" msgstr "Obsolète depuis" -#: apps/note/templates/note/transactiontemplate_form.html:24 +#: apps/note/templates/note/transactiontemplate_form.html:27 msgid "Current price" msgstr "Prix actuel" @@ -1530,11 +1541,11 @@ msgstr "Nouveau bouton" #: apps/note/templates/note/transactiontemplate_list.html:22 msgid "buttons listing " -msgstr "Liste des boutons" +msgstr "liste des boutons " #: apps/note/templates/note/transactiontemplate_list.html:73 msgid "button successfully deleted " -msgstr "Le bouton a bien été supprimé" +msgstr "le bouton a bien été supprimé " #: apps/note/templates/note/transactiontemplate_list.html:77 msgid "Unable to delete button " @@ -1556,7 +1567,7 @@ msgstr "Chercher un bouton" msgid "Update button" msgstr "Modifier le bouton" -#: apps/note/views.py:151 note_kfet/templates/base.html:72 +#: apps/note/views.py:151 note_kfet/templates/base.html:63 msgid "Consumptions" msgstr "Consommations" @@ -1580,7 +1591,7 @@ msgstr "Can {type} {model} in {query}" #: apps/permission/models.py:104 msgid "rank" -msgstr "Rang" +msgstr "rang" #: apps/permission/models.py:117 msgid "permission mask" @@ -1647,7 +1658,7 @@ msgstr "s'applique au club" #: apps/permission/models.py:349 apps/permission/models.py:350 msgid "role permissions" -msgstr "Permissions par rôles" +msgstr "permissions par rôles" #: apps/permission/signals.py:63 #, python-brace-format @@ -1735,7 +1746,7 @@ msgstr "" "Vous n'avez pas la permission d'ajouter une instance du modèle « {model} » " "avec ces paramètres. Merci de les corriger et de réessayer." -#: apps/permission/views.py:96 note_kfet/templates/base.html:114 +#: apps/permission/views.py:96 note_kfet/templates/base.html:105 msgid "Rights" msgstr "Droits" @@ -1890,11 +1901,11 @@ msgstr "Valider l'adresse e-mail" #: apps/registration/views.py:126 msgid "Email validation unsuccessful" -msgstr " La validation de l'adresse mail a échoué" +msgstr "La validation de l'adresse mail a échoué" #: apps/registration/views.py:137 msgid "Email validation email sent" -msgstr "L'email de vérification de l'adresse email a bien été envoyé." +msgstr "L'email de vérification de l'adresse email a bien été envoyé" #: apps/registration/views.py:145 msgid "Resend email validation link" @@ -1904,34 +1915,30 @@ msgstr "Renvoyer le lien de validation" msgid "Pre-registered users list" msgstr "Liste des utilisateurs en attente d'inscription" -#: apps/registration/views.py:190 +#: apps/registration/views.py:187 msgid "Unregistered users" msgstr "Utilisateurs en attente d'inscription" -#: apps/registration/views.py:203 +#: apps/registration/views.py:200 msgid "Registration detail" msgstr "Détails de l'inscription" -#: apps/registration/views.py:258 +#: apps/registration/views.py:256 msgid "You must join the BDE." msgstr "Vous devez adhérer au BDE." #: apps/registration/views.py:280 -msgid "You must join BDE club before joining Kfet club." -msgstr "Vous devez adhérer au club BDE avant d'adhérer au club Kfet." - -#: apps/registration/views.py:285 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" "Le montant crédité est trop faible pour adhérer, il doit être au minimum de " "{}" -#: apps/registration/views.py:360 +#: apps/registration/views.py:355 msgid "Invalidate pre-registration" msgstr "Invalider l'inscription" -#: apps/treasury/apps.py:12 note_kfet/templates/base.html:102 +#: apps/treasury/apps.py:12 note_kfet/templates/base.html:93 msgid "Treasury" msgstr "Trésorerie" @@ -1993,7 +2000,7 @@ msgstr "Une facture ne peut plus être modifiée si elle est verrouillée." #: apps/treasury/models.py:76 msgid "tex source" -msgstr "Fichier TeX source" +msgstr "fichier TeX source" #: apps/treasury/models.py:89 #: apps/treasury/templates/treasury/invoice_form.html:22 @@ -2058,11 +2065,11 @@ msgstr "Remise n°{:d} : {}" #: apps/treasury/models.py:256 msgid "special transaction proxy" -msgstr "Proxy de transaction spéciale" +msgstr "proxy de transaction spéciale" #: apps/treasury/models.py:257 msgid "special transaction proxies" -msgstr "Proxys de transactions spéciales" +msgstr "proxys de transactions spéciales" #: apps/treasury/models.py:279 msgid "credit transaction" @@ -2320,14 +2327,14 @@ msgstr "Gérer les crédits de la Société générale" #: apps/wei/apps.py:10 apps/wei/models.py:49 apps/wei/models.py:50 #: apps/wei/models.py:61 apps/wei/models.py:167 -#: note_kfet/templates/base.html:108 +#: note_kfet/templates/base.html:99 msgid "WEI" msgstr "WEI" #: apps/wei/forms/registration.py:51 apps/wei/models.py:113 #: apps/wei/models.py:283 msgid "bus" -msgstr "Bus" +msgstr "bus" #: apps/wei/forms/registration.py:52 msgid "" @@ -2459,11 +2466,11 @@ msgstr "problèmes de santé" #: apps/wei/models.py:224 apps/wei/templates/wei/weimembership_form.html:70 msgid "emergency contact name" -msgstr "Nom du contact en cas d'urgence" +msgstr "nom du contact en cas d'urgence" #: apps/wei/models.py:229 apps/wei/templates/wei/weimembership_form.html:73 msgid "emergency contact phone" -msgstr "Téléphone du contact en cas d'urgence" +msgstr "téléphone du contact en cas d'urgence" #: apps/wei/models.py:234 apps/wei/templates/wei/weimembership_form.html:52 msgid "first year" @@ -2499,15 +2506,15 @@ msgstr "équipe" #: apps/wei/models.py:303 msgid "WEI registration" -msgstr "inscription au WEI" +msgstr "Inscription au WEI" #: apps/wei/models.py:307 msgid "WEI membership" -msgstr "adhésion au WEI" +msgstr "Adhésion au WEI" #: apps/wei/models.py:308 msgid "WEI memberships" -msgstr "adhésions au WEI" +msgstr "Adhésions au WEI" #: apps/wei/tables.py:127 msgid "Year" @@ -2928,19 +2935,19 @@ msgstr "Réinitialiser" msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: note_kfet/templates/base.html:84 +#: note_kfet/templates/base.html:75 msgid "Users" msgstr "Utilisateurs" -#: note_kfet/templates/base.html:90 +#: note_kfet/templates/base.html:81 msgid "Clubs" msgstr "Clubs" -#: note_kfet/templates/base.html:119 +#: note_kfet/templates/base.html:110 msgid "Admin" msgstr "Admin" -#: note_kfet/templates/base.html:163 +#: note_kfet/templates/base.html:154 msgid "" "Your e-mail address is not validated. Please check your mail inbox and click " "on the validation link." @@ -3018,6 +3025,9 @@ msgid "" "Please enter your old password, for security's sake, and then enter your new " "password twice so we can verify you typed it in correctly." msgstr "" +"Veuillez entrer votre ancien mot de passe pour des raisons de sécurité, puis " +"renseigné votre nouveau mot de passe à deux reprises, pour être sur de " +"l'avoir tapé correctement." #: note_kfet/templates/registration/password_change_form.html:16 #: note_kfet/templates/registration/password_reset_confirm.html:17 diff --git a/note_kfet/static/js/base.js b/note_kfet/static/js/base.js index a20c72bc..8315f01a 100644 --- a/note_kfet/static/js/base.js +++ b/note_kfet/static/js/base.js @@ -122,7 +122,7 @@ function displayStyle (note) { */ function displayNote (note, alias, user_note_field = null, profile_pic_field = null) { if (!note.display_image) { - note.display_image = '/media/pic/default.png'; + note.display_image = '/static/member/img/default_picture.png'; } let img = note.display_image; if (alias !== note.name && note.name) diff --git a/note_kfet/static/js/consos.js b/note_kfet/static/js/consos.js index fc04b2b2..25e8113e 100644 --- a/note_kfet/static/js/consos.js +++ b/note_kfet/static/js/consos.js @@ -27,7 +27,7 @@ $(document).ready(function() { }); // Switching in double consumptions mode should update the layout - $("#double_conso").click(function() { + $("#double_conso").change(function() { $("#consos_list_div").removeClass('d-none'); $("#user_select_div").attr('class', 'col-xl-4'); $("#infos_div").attr('class', 'col-sm-5 col-xl-6'); @@ -47,7 +47,7 @@ $(document).ready(function() { } }); - $("#single_conso").click(function() { + $("#single_conso").change(function() { $("#consos_list_div").addClass('d-none'); $("#user_select_div").attr('class', 'col-xl-7'); $("#infos_div").attr('class', 'col-sm-5 col-md-4'); @@ -158,7 +158,7 @@ function reset() { $("#consos_list").html(""); $("#note").val(""); $("#note").attr("data-original-title", "").tooltip("hide"); - $("#profile_pic").attr("src", "/media/pic/default.png"); + $("#profile_pic").attr("src", "/static/member/img/default_picture.png"); $("#profile_pic_link").attr("href", "#"); refreshHistory(); refreshBalance(); diff --git a/note_kfet/static/js/konami.js b/note_kfet/static/js/konami.js new file mode 100644 index 00000000..a430a4b6 --- /dev/null +++ b/note_kfet/static/js/konami.js @@ -0,0 +1,45 @@ +/* + * Konami code support + */ + +// Cursor denote the position in konami code +let cursor = 0 +const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65] + +function afterKonami() { + // Load Rythm.js + var rythmScript = document.createElement('script') + rythmScript.setAttribute('src','//unpkg.com/rythm.js@2.2.5/rythm.min.js') + document.head.appendChild(rythmScript) + + rythmScript.addEventListener('load', function() { + // Ker-Lyon audio courtesy of @adalan, ker-lyon.fr + const audioElement = new Audio('/static/song/konami.ogg') + audioElement.loop = true + audioElement.play() + + const rythm = new Rythm() + rythm.connectExternalAudioElement(audioElement) + rythm.addRythm('card', 'pulse', 50, 50, { + min: 1, + max: 1.1 + }) + rythm.addRythm('d-flex', 'color', 50, 50, { + from: [64,64,64], + to:[128,64,128] + }) + rythm.addRythm('nav-link', 'jump', 150, 50, { + min: 0, + max: 10 + }) + rythm.start() + }); +} + +// Register custom event +document.addEventListener('keydown', (e) => { + cursor = (e.keyCode == KONAMI_CODE[cursor]) ? cursor + 1 : 0; + if (cursor == KONAMI_CODE.length) { + afterKonami() + } +}); diff --git a/note_kfet/static/js/transfer.js b/note_kfet/static/js/transfer.js index 28b28aef..e22d2b3f 100644 --- a/note_kfet/static/js/transfer.js +++ b/note_kfet/static/js/transfer.js @@ -40,7 +40,7 @@ function reset(refresh=true) { $("#first_name").val(""); $("#bank").val(""); $("#user_note").val(""); - $("#profile_pic").attr("src", "/media/pic/default.png"); + $("#profile_pic").attr("src", "/static/member/img/default_picture.png"); $("#profile_pic_link").attr("href", "#"); if (refresh) { refreshBalance(); @@ -96,7 +96,7 @@ $(document).ready(function() { let source = $("#source_note"); let dest = $("#dest_note"); - $("#type_transfer").click(function() { + $("#type_transfer").change(function() { if (LOCK) return; @@ -117,7 +117,7 @@ $(document).ready(function() { location.hash = "transfer"; }); - $("#type_credit").click(function() { + $("#type_credit").change(function() { if (LOCK) return; @@ -146,7 +146,7 @@ $(document).ready(function() { location.hash = "credit"; }); - $("#type_debit").click(function() { + $("#type_debit").change(function() { if (LOCK) return; diff --git a/note_kfet/static/song/konami.ogg b/note_kfet/static/song/konami.ogg new file mode 100644 index 00000000..e84f32ec Binary files /dev/null and b/note_kfet/static/song/konami.ogg differ diff --git a/note_kfet/templates/base.html b/note_kfet/templates/base.html index 3381c78e..fcee608a 100644 --- a/note_kfet/templates/base.html +++ b/note_kfet/templates/base.html @@ -35,6 +35,7 @@ SPDX-License-Identifier: GPL-3.0-or-later + {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} {% if form.media %} diff --git a/requirements.txt b/requirements.txt index 782beb77..870ea3b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,17 @@ -django-htcpcp-tea==0.3.1 -django-mailer==2.0.1 -django-phonenumber-field==5.0.0 -django-tables2==2.3.1 -django-rest-polymorphic==0.1.9 -django-bootstrap-datepicker-plus==3.0.5 -django-colorfield==0.3.2 +beautifulsoup4~=4.7.1 +Django~=2.2.15 +django-bootstrap-datepicker-plus~=3.0.5 +django-cas-server>=0.9.0 +django-colorfield~=0.3.2 +django-crispy-forms~=1.7.2 +django-extensions~=2.1.4 +django-filter~=2.1.0 +django-htcpcp-tea~=0.3.1 +django-mailer~=2.0.1 +django-phonenumber-field~=5.0.0 +django-polymorphic~=2.0.3 +djangorestframework~=3.9.0 +django-rest-polymorphic~=0.1.9 +django-tables2~=2.3.1 +phonenumbers~=8.9.10 +Pillow>=5.4.1 diff --git a/tox.ini b/tox.ini index 240c9523..b160324b 100644 --- a/tox.ini +++ b/tox.ini @@ -11,8 +11,6 @@ skipsdist = True [testenv] sitepackages = True -setenv = - PYTHONWARNINGS = all deps = -r{toxinidir}/requirements.txt coverage @@ -23,7 +21,6 @@ commands = [testenv:linters] deps = - -r{toxinidir}/requirements.txt flake8 flake8-colors flake8-import-order @@ -34,8 +31,7 @@ commands = flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury apps/wei [flake8] -# Ignore too many errors, should be reduced in the future -ignore = D203, W503, E203, I100, I101, C901 +ignore = W503, I100, I101 exclude = .tox, .git,