diff --git a/.env_example b/.env_example
new file mode 100644
index 00000000..5aba0d14
--- /dev/null
+++ b/.env_example
@@ -0,0 +1,13 @@
+DJANGO_APP_STAGE="dev"
+# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
+DJANGO_DEV_STORE_METHOD="sqllite"
+DJANGO_DB_HOST="localhost"
+DJANGO_DB_NAME="note_db"
+DJANGO_DB_USER="note"
+DJANGO_DB_PASSWORD="CHANGE_ME"
+DJANGO_DB_PORT=""
+DJANGO_SECRET_KEY="CHANGE_ME"
+DJANGO_SETTINGS_MODULE="note_kfet.settings"
+DOMAIN="localhost"
+CONTACT_EMAIL="tresorerie.bde@localhost"
+NOTE_URL="localhost"
diff --git a/Dockerfile b/Dockerfile
index a2f45b00..d42bdd1f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,13 +9,13 @@ RUN apt update && \
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
rm -rf /var/lib/apt/lists/*
-COPY requirements.txt /code/
+COPY . /code/
+
+# Comment what is not needed
RUN pip install -r requirements/base.txt
RUN pip install -r requirements/api.txt
RUN pip install -r requirements/cas.txt
RUN pip install -r requirements/production.txt
-COPY . /code/
-
ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 8000
diff --git a/README.md b/README.md
index 14ec5f42..91f2f17d 100644
--- a/README.md
+++ b/README.md
@@ -40,14 +40,13 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
$ cp nginx_note.conf_example nginx_note.conf
-***Modifier le fichier pour être en accord avec le reste de votre config***
+ ***Modifier le fichier pour être en accord avec le reste de votre config***
On utilise uwsgi et Nginx pour gérer le coté serveur :
- $ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
+ $ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
-
- Si l'on a un emperor (plusieurs instance uwsgi):
+ Si l'on a un emperor (plusieurs instance uwsgi):
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
@@ -85,7 +84,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
postgres=# CREATE DATABASE note_db OWNER note;
CREATE DATABASE
- Si tout va bien:
+ Si tout va bien :
postgres=#\list
List of databases
@@ -96,22 +95,29 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres+postgres=CTc/postgres
template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres +postgres=CTc/postgres
(4 rows)
-
- Dans un fichier `.env` à la racine du projet on renseigne des secrets:
- DJANGO_APP_STAGE='prod'
- DJANGO_DB_PASSWORD='le_mot_de_passe_de_la_bdd'
- DJANGO_SECRET_KEY='une_secret_key_longue_et_compliquee'
- ALLOWED_HOSTS='le_ndd_de_votre_instance'
-
-
6. Variable d'environnement et Migrations
+ On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet
+ et on renseigne des secrets et des paramètres :
+
+ DJANGO_APP_STAGE="dev"
+ DJANGO_DEV_STORE_METHOD="sqllite"
+ DJANGO_DB_HOST="localhost"
+ DJANGO_DB_NAME="note_db"
+ DJANGO_DB_USER="note"
+ DJANGO_DB_PASSWORD="CHANGE_ME"
+ DJANGO_DB_PORT=""
+ DJANGO_SECRET_KEY="CHANGE_ME"
+ DJANGO_SETTINGS_MODULE="note_kfet.settings"
+ DOMAIN="localhost"
+ CONTACT_EMAIL="tresorerie.bde@localhost"
+ NOTE_URL="localhost"
-Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
+ Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
$ source /env/bin/activate
- (env)$ ./manage.py check # pas de bétise qui traine
+ (env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
@@ -126,17 +132,21 @@ Il est possible de travailler sur une instance Docker.
$ git clone git@gitlab.crans.org:bde/nk20.git
-2. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
+2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
+et mettez à jour vos variables d'environnement
+
+3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
nk20:
build: /chemin/vers/nk20
volumes:
- /chemin/vers/nk20:/code/
+ env_file: /chemin/vers/nk20/.env
restart: always
labels:
- - traefik.domain=ndd.exemple.com
- - traefik.frontend.rule=Host:ndd.exemple.com
+ - traefik.domain=ndd.example.com
+ - traefik.frontend.rule=Host:ndd.example.com
- traefik.port=8000
3. Enjoy :
@@ -159,17 +169,20 @@ un serveur de développement par exemple sur son ordinateur.
$ source venv/bin/activate
(env)$ pip install -r requirements.txt
-3. Migrations et chargement des données initiales :
+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
-4. Créer un super-utilisateur :
+5. Créer un super-utilisateur :
(env)$ ./manage.py createsuperuser
-5. Enjoy :
+6. Enjoy :
(env)$ ./manage.py runserver 0.0.0.0:8000
@@ -184,4 +197,4 @@ Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
## Documentation
La documentation est générée par django et son module admindocs.
-**Commenter votre code !**
+**Commentez votre code !**
diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py
index 6a6c024e..4ee2194d 100644
--- a/apps/activity/api/views.py
+++ b/apps/activity/api/views.py
@@ -1,7 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-
+from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
+from rest_framework.filters import SearchFilter
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
from ..models import ActivityType, Activity, Guest
@@ -15,6 +16,8 @@ class ActivityTypeViewSet(viewsets.ModelViewSet):
"""
queryset = ActivityType.objects.all()
serializer_class = ActivityTypeSerializer
+ filter_backends = [DjangoFilterBackend]
+ filterset_fields = ['name', 'can_invite', ]
class ActivityViewSet(viewsets.ModelViewSet):
@@ -25,6 +28,8 @@ class ActivityViewSet(viewsets.ModelViewSet):
"""
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
+ filter_backends = [DjangoFilterBackend]
+ filterset_fields = ['name', 'description', 'activity_type', ]
class GuestViewSet(viewsets.ModelViewSet):
@@ -35,3 +40,5 @@ class GuestViewSet(viewsets.ModelViewSet):
"""
queryset = Guest.objects.all()
serializer_class = GuestSerializer
+ filter_backends = [SearchFilter]
+ search_fields = ['$name', ]
diff --git a/apps/api/urls.py b/apps/api/urls.py
index c1b6bf48..95ed5f99 100644
--- a/apps/api/urls.py
+++ b/apps/api/urls.py
@@ -3,10 +3,14 @@
from django.conf.urls import url, include
from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import routers, serializers, viewsets
+from rest_framework.filters import SearchFilter
from activity.api.urls import register_activity_urls
from member.api.urls import register_members_urls
from note.api.urls import register_note_urls
+from logs.api.urls import register_logs_urls
class UserSerializer(serializers.ModelSerializer):
@@ -24,6 +28,17 @@ class UserSerializer(serializers.ModelSerializer):
)
+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(viewsets.ModelViewSet):
"""
REST API View set.
@@ -32,15 +47,30 @@ class UserViewSet(viewsets.ModelViewSet):
"""
queryset = User.objects.all()
serializer_class = UserSerializer
+ filter_backends = [DjangoFilterBackend, SearchFilter]
+ filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
+ search_fields = ['$username', '$first_name', '$last_name', ]
+
+
+class ContentTypeViewSet(viewsets.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
# Routers provide an easy way of automatically determining the URL conf.
# Register each app API router and user viewset
router = routers.DefaultRouter()
+router.register('models', ContentTypeViewSet)
router.register('user', UserViewSet)
register_members_urls(router, 'members')
register_activity_urls(router, 'activity')
register_note_urls(router, 'note')
+register_logs_urls(router, 'logs')
app_name = 'api'
diff --git a/apps/logs/api/__init__.py b/apps/logs/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/logs/api/serializers.py b/apps/logs/api/serializers.py
new file mode 100644
index 00000000..c76e3a5d
--- /dev/null
+++ b/apps/logs/api/serializers.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from rest_framework import serializers
+
+from ..models import Changelog
+
+
+class ChangelogSerializer(serializers.ModelSerializer):
+ """
+ REST API Serializer for Changelog types.
+ The djangorestframework plugin will analyse the model `Changelog` and parse all fields in the API.
+ """
+
+ class Meta:
+ model = Changelog
+ fields = '__all__'
+ # noinspection PyProtectedMember
+ read_only_fields = [f.name for f in model._meta.get_fields()] # Changelogs are read-only protected
diff --git a/apps/logs/api/urls.py b/apps/logs/api/urls.py
new file mode 100644
index 00000000..9a0ceaa8
--- /dev/null
+++ b/apps/logs/api/urls.py
@@ -0,0 +1,11 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from .views import ChangelogViewSet
+
+
+def register_logs_urls(router, path):
+ """
+ Configure router for Activity REST API.
+ """
+ router.register(path, ChangelogViewSet)
diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py
new file mode 100644
index 00000000..2c47b7a2
--- /dev/null
+++ b/apps/logs/api/views.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework import viewsets
+from rest_framework.filters import OrderingFilter
+
+from .serializers import ChangelogSerializer
+from ..models import Changelog
+
+
+class ChangelogViewSet(viewsets.ReadOnlyModelViewSet):
+ """
+ REST API View set.
+ The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
+ then render it on /api/logs/
+ """
+ queryset = Changelog.objects.all()
+ serializer_class = ChangelogSerializer
+ filter_backends = [DjangoFilterBackend, OrderingFilter]
+ filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
+ ordering_fields = ['timestamp', ]
+ ordering = ['-timestamp', ]
diff --git a/apps/logs/apps.py b/apps/logs/apps.py
index f48820c7..239f86cf 100644
--- a/apps/logs/apps.py
+++ b/apps/logs/apps.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
+from django.db.models.signals import pre_save, post_save, post_delete
from django.utils.translation import gettext_lazy as _
@@ -11,4 +12,7 @@ class LogsConfig(AppConfig):
def ready(self):
# noinspection PyUnresolvedReferences
- import logs.signals
+ from . import signals
+ pre_save.connect(signals.pre_save_object)
+ post_save.connect(signals.save_object)
+ post_delete.connect(signals.delete_object)
diff --git a/apps/logs/middlewares.py b/apps/logs/middlewares.py
new file mode 100644
index 00000000..77f749b9
--- /dev/null
+++ b/apps/logs/middlewares.py
@@ -0,0 +1,55 @@
+# 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 AnonymousUser
+
+from threading import local
+
+
+USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
+IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
+
+_thread_locals = local()
+
+
+def _set_current_user_and_ip(user=None, ip=None):
+ setattr(_thread_locals, USER_ATTR_NAME, user)
+ setattr(_thread_locals, IP_ATTR_NAME, ip)
+
+
+def get_current_user():
+ return getattr(_thread_locals, USER_ATTR_NAME, None)
+
+
+def get_current_ip():
+ return getattr(_thread_locals, IP_ATTR_NAME, None)
+
+
+def get_current_authenticated_user():
+ current_user = get_current_user()
+ if isinstance(current_user, AnonymousUser):
+ return None
+ return current_user
+
+
+class LogsMiddleware(object):
+ """
+ This middleware get the current user with his or her IP address on each request.
+ """
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ user = request.user
+ if 'HTTP_X_FORWARDED_FOR' in request.META:
+ ip = request.META.get('HTTP_X_FORWARDED_FOR')
+ else:
+ ip = request.META.get('REMOTE_ADDR')
+
+ _set_current_user_and_ip(user, ip)
+ response = self.get_response(request)
+ _set_current_user_and_ip(None, None)
+
+ return response
diff --git a/apps/logs/models.py b/apps/logs/models.py
index 9ab3cf6a..10e2651f 100644
--- a/apps/logs/models.py
+++ b/apps/logs/models.py
@@ -56,6 +56,12 @@ class Changelog(models.Model):
max_length=16,
null=False,
blank=False,
+ choices=[
+ ('create', _('create')),
+ ('edit', _('edit')),
+ ('delete', _('delete')),
+ ],
+ default='edit',
verbose_name=_('action'),
)
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index 415e7c1c..fb17157a 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -1,67 +1,40 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-import inspect
-
from django.contrib.contenttypes.models import ContentType
-from django.core import serializers
-from django.db.models.signals import pre_save, post_save, post_delete
-from django.dispatch import receiver
+from rest_framework.renderers import JSONRenderer
+from rest_framework.serializers import ModelSerializer
+import getpass
+
+from note.models import NoteUser, Alias
+
+from .middlewares import get_current_authenticated_user, get_current_ip
from .models import Changelog
-def get_request_in_signal(sender):
- req = None
- for entry in reversed(inspect.stack()):
- try:
- req = entry[0].f_locals['request']
- # Check if there is a user
- # noinspection PyStatementEffect
- req.user
- break
- except:
- pass
-
- if not req:
- print("WARNING: Attempt to save " + str(sender) + " with no user")
-
- return req
-
-
-def get_user_and_ip(sender):
- req = get_request_in_signal(sender)
- try:
- user = req.user
- if 'HTTP_X_FORWARDED_FOR' in req.META:
- ip = req.META.get('HTTP_X_FORWARDED_FOR')
- else:
- ip = req.META.get('REMOTE_ADDR')
- except:
- user = None
- ip = None
- return user, ip
-
-
+# Ces modèles ne nécessitent pas de logs
EXCLUDED = [
'admin.logentry',
'authtoken.token',
+ 'cas_server.proxygrantingticket',
+ 'cas_server.proxyticket',
+ 'cas_server.serviceticket',
'cas_server.user',
'cas_server.userattributes',
'contenttypes.contenttype',
- 'logs.changelog',
+ 'logs.changelog', # Never remove this line
'migrations.migration',
- 'note.noteuser',
- 'note.noteclub',
- 'note.notespecial',
+ 'note.note' # We only store the subclasses
+ 'note.transaction',
'sessions.session',
- 'reversion.revision',
- 'reversion.version',
]
-@receiver(pre_save)
def pre_save_object(sender, instance, **kwargs):
+ """
+ Before a model get saved, we get the previous instance that is currently in the database
+ """
qs = sender.objects.filter(pk=instance.pk).all()
if qs.exists():
instance._previous = qs.get()
@@ -69,30 +42,51 @@ def pre_save_object(sender, instance, **kwargs):
instance._previous = None
-@receiver(post_save)
def save_object(sender, instance, **kwargs):
+ """
+ Each time a model is saved, an entry in the table `Changelog` is added in the database
+ in order to store each modification made
+ """
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
return
+ # noinspection PyProtectedMember
previous = instance._previous
- user, ip = get_user_and_ip(sender)
+ # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
+ user, ip = get_current_authenticated_user(), get_current_ip()
- from django.contrib.auth.models import AnonymousUser
- if isinstance(user, AnonymousUser):
- user = None
+ if user is None:
+ # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
+ # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
+ # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
+ ip = "127.0.0.1"
+ username = Alias.normalize(getpass.getuser())
+ note = NoteUser.objects.filter(alias__normalized_name=username)
+ # if not note.exists():
+ # print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username)
+ # else:
+ if note.exists():
+ user = note.get().user
+ # noinspection PyProtectedMember
if user is not None and instance._meta.label_lower == "auth.user" and previous:
- # Don't save last login modifications
+ # On n'enregistre pas les connexions
if instance.last_login != previous.last_login:
return
- previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None
- instance_json = serializers.serialize('json', [instance, ])[1:-1]
+ # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
+ class CustomSerializer(ModelSerializer):
+ class Meta:
+ model = instance.__class__
+ fields = '__all__'
+
+ previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else None
+ instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
if previous_json == instance_json:
- # No modification
+ # Pas de log s'il n'y a pas de modification
return
Changelog.objects.create(user=user,
@@ -105,15 +99,38 @@ def save_object(sender, instance, **kwargs):
).save()
-@receiver(post_delete)
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
- user, ip = get_user_and_ip(sender)
+ # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
+ user, ip = get_current_authenticated_user(), get_current_ip()
+
+ if user is None:
+ # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
+ # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
+ # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
+ ip = "127.0.0.1"
+ username = Alias.normalize(getpass.getuser())
+ note = NoteUser.objects.filter(alias__normalized_name=username)
+ # if not note.exists():
+ # print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username)
+ # else:
+ if note.exists():
+ user = note.get().user
+
+ # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
+ class CustomSerializer(ModelSerializer):
+ class Meta:
+ model = instance.__class__
+ fields = '__all__'
+
+ instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
- instance_json = serializers.serialize('json', [instance, ])[1:-1]
Changelog.objects.create(user=user,
ip=ip,
model=ContentType.objects.get_for_model(instance),
diff --git a/apps/logs/urls.py b/apps/logs/urls.py
deleted file mode 100644
index 6d76674c..00000000
--- a/apps/logs/urls.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-app_name = 'logs'
-
-# TODO User interface
-urlpatterns = [
-]
diff --git a/apps/member/api/views.py b/apps/member/api/views.py
index 7e7dcd1d..c85df903 100644
--- a/apps/member/api/views.py
+++ b/apps/member/api/views.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets
+from rest_framework.filters import SearchFilter
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
from ..models import Profile, Club, Role, Membership
@@ -25,6 +26,8 @@ class ClubViewSet(viewsets.ModelViewSet):
"""
queryset = Club.objects.all()
serializer_class = ClubSerializer
+ filter_backends = [SearchFilter]
+ search_fields = ['$name', ]
class RoleViewSet(viewsets.ModelViewSet):
@@ -35,6 +38,8 @@ class RoleViewSet(viewsets.ModelViewSet):
"""
queryset = Role.objects.all()
serializer_class = RoleSerializer
+ filter_backends = [SearchFilter]
+ search_fields = ['$name', ]
class MembershipViewSet(viewsets.ModelViewSet):
diff --git a/apps/member/models.py b/apps/member/models.py
index 5cdc4c77..b6d17a08 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -46,7 +46,7 @@ class Profile(models.Model):
class Meta:
verbose_name = _('user profile')
verbose_name_plural = _('user profile')
- indexes = [ models.Index(fields=['user']) ]
+ indexes = [models.Index(fields=['user'])]
def get_absolute_url(self):
return reverse('user_detail', args=(self.pk,))
@@ -153,7 +153,7 @@ class Membership(models.Model):
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
- indexes = [ models.Index(fields=['user']) ]
+ indexes = [models.Index(fields=['user'])]
# @receiver(post_save, sender=settings.AUTH_USER_MODEL)
# def save_user_profile(instance, created, **_kwargs):
diff --git a/apps/member/views.py b/apps/member/views.py
index 21c8de5f..82c15b99 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -300,7 +300,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
qs = User.objects.all()
if self.q:
- qs = qs.filter(username__regex=self.q)
+ qs = qs.filter(username__regex="^" + self.q)
return qs
diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py
index 1696bfee..73beead1 100644
--- a/apps/note/api/serializers.py
+++ b/apps/note/api/serializers.py
@@ -5,7 +5,8 @@ from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
-from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
+from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
+ TemplateTransaction
class NoteSerializer(serializers.ModelSerializer):
@@ -78,6 +79,17 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
}
+class TemplateCategorySerializer(serializers.ModelSerializer):
+ """
+ REST API Serializer for Transaction templates.
+ The djangorestframework plugin will analyse the model `TemplateCategory` and parse all fields in the API.
+ """
+
+ class Meta:
+ model = TemplateCategory
+ fields = '__all__'
+
+
class TransactionTemplateSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transaction templates.
@@ -100,6 +112,17 @@ class TransactionSerializer(serializers.ModelSerializer):
fields = '__all__'
+class TemplateTransactionSerializer(serializers.ModelSerializer):
+ """
+ REST API Serializer for Transactions.
+ The djangorestframework plugin will analyse the model `TemplateTransaction` and parse all fields in the API.
+ """
+
+ class Meta:
+ model = TemplateTransaction
+ fields = '__all__'
+
+
class MembershipTransactionSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Membership transactions.
@@ -109,3 +132,11 @@ class MembershipTransactionSerializer(serializers.ModelSerializer):
class Meta:
model = MembershipTransaction
fields = '__all__'
+
+
+class TransactionPolymorphicSerializer(PolymorphicSerializer):
+ model_serializer_mapping = {
+ Transaction: TransactionSerializer,
+ TemplateTransaction: TemplateTransactionSerializer,
+ MembershipTransaction: MembershipTransactionSerializer,
+ }
diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py
index 54218796..796a397f 100644
--- a/apps/note/api/urls.py
+++ b/apps/note/api/urls.py
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, \
- TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet
+ TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
def register_note_urls(router, path):
@@ -12,6 +12,6 @@ def register_note_urls(router, path):
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
+ router.register(path + '/transaction/category', TemplateCategoryViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet)
router.register(path + '/transaction/template', TransactionTemplateViewSet)
- router.register(path + '/transaction/membership', MembershipTransactionViewSet)
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index cf0136f2..14f64003 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -2,13 +2,15 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db.models import Q
+from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
+from rest_framework.filters import SearchFilter
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
NoteUserSerializer, AliasSerializer, \
- TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer
+ TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
-from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
+from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
class NoteViewSet(viewsets.ModelViewSet):
@@ -69,8 +71,8 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
- Q(alias__name__regex=alias)
- | Q(alias__normalized_name__regex=alias.lower()))
+ Q(alias__name__regex="^" + alias)
+ | Q(alias__normalized_name__regex="^" + alias.lower()))
note_type = self.request.query_params.get("type", None)
if note_type:
@@ -107,7 +109,7 @@ class AliasViewSet(viewsets.ModelViewSet):
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
- Q(name__regex=alias) | Q(normalized_name__regex=alias.lower()))
+ Q(name__regex="^" + alias) | Q(normalized_name__regex="^" + alias.lower()))
note_id = self.request.query_params.get("note", None)
if note_id:
@@ -131,6 +133,18 @@ class AliasViewSet(viewsets.ModelViewSet):
return queryset
+class TemplateCategoryViewSet(viewsets.ModelViewSet):
+ """
+ REST API View set.
+ The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
+ then render it on /api/note/transaction/category/
+ """
+ queryset = TemplateCategory.objects.all()
+ serializer_class = TemplateCategorySerializer
+ filter_backends = [SearchFilter]
+ search_fields = ['$name', ]
+
+
class TransactionTemplateViewSet(viewsets.ModelViewSet):
"""
REST API View set.
@@ -139,6 +153,8 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
"""
queryset = TransactionTemplate.objects.all()
serializer_class = TransactionTemplateSerializer
+ filter_backends = [DjangoFilterBackend]
+ filterset_fields = ['name', 'amount', 'display', 'category', ]
class TransactionViewSet(viewsets.ModelViewSet):
@@ -148,14 +164,6 @@ class TransactionViewSet(viewsets.ModelViewSet):
then render it on /api/note/transaction/transaction/
"""
queryset = Transaction.objects.all()
- serializer_class = TransactionSerializer
-
-
-class MembershipTransactionViewSet(viewsets.ModelViewSet):
- """
- REST API View set.
- The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer,
- then render it on /api/note/transaction/membership/
- """
- queryset = MembershipTransaction.objects.all()
- serializer_class = MembershipTransactionSerializer
+ serializer_class = TransactionPolymorphicSerializer
+ filter_backends = [SearchFilter]
+ search_fields = ['$reason', ]
diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json
index f80332c0..3654fa2f 100644
--- a/apps/note/fixtures/initial.json
+++ b/apps/note/fixtures/initial.json
@@ -3,7 +3,7 @@
"model": "note.note",
"pk": 1,
"fields": {
- "polymorphic_ctype": 39,
+ "polymorphic_ctype": 40,
"balance": 0,
"is_active": true,
"display_image": "",
@@ -14,7 +14,7 @@
"model": "note.note",
"pk": 2,
"fields": {
- "polymorphic_ctype": 39,
+ "polymorphic_ctype": 40,
"balance": 0,
"is_active": true,
"display_image": "",
@@ -25,7 +25,7 @@
"model": "note.note",
"pk": 3,
"fields": {
- "polymorphic_ctype": 39,
+ "polymorphic_ctype": 40,
"balance": 0,
"is_active": true,
"display_image": "",
@@ -36,7 +36,7 @@
"model": "note.note",
"pk": 4,
"fields": {
- "polymorphic_ctype": 39,
+ "polymorphic_ctype": 40,
"balance": 0,
"is_active": true,
"display_image": "",
@@ -47,7 +47,7 @@
"model": "note.note",
"pk": 5,
"fields": {
- "polymorphic_ctype": 38,
+ "polymorphic_ctype": 39,
"balance": 0,
"is_active": true,
"display_image": "",
@@ -58,7 +58,7 @@
"model": "note.note",
"pk": 6,
"fields": {
- "polymorphic_ctype": 38,
+ "polymorphic_ctype": 39,
"balance": 0,
"is_active": true,
"display_image": "",
diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py
index 70810ad8..b6b00aa8 100644
--- a/apps/note/models/notes.py
+++ b/apps/note/models/notes.py
@@ -235,7 +235,7 @@ class Alias(models.Model):
try:
sim_alias = Alias.objects.get(normalized_name=normalized_name)
if self != sim_alias:
- raise ValidationError(_('An alias with a similar name already exists: {} '.format(sim_alias)),
+ raise ValidationError(_('An alias with a similar name already exists: {} ').format(sim_alias),
code="same_alias"
)
except Alias.DoesNotExist:
diff --git a/apps/note/templatetags/getenv.py b/apps/note/templatetags/getenv.py
new file mode 100644
index 00000000..c133cb8f
--- /dev/null
+++ b/apps/note/templatetags/getenv.py
@@ -0,0 +1,14 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django import template
+
+import os
+
+
+def getenv(value):
+ return os.getenv(value)
+
+
+register = template.Library()
+register.filter('getenv', getenv)
diff --git a/apps/note/views.py b/apps/note/views.py
index fb5e98c5..16e2e39b 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -69,7 +69,7 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
# self.q est le paramètre de la recherche
if self.q:
- qs = qs.filter(Q(name__regex=self.q) | Q(normalized_name__regex=Alias.normalize(self.q))) \
+ qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \
.order_by('normalized_name').distinct()
# Filtrage par type de note (user, club, special)
@@ -141,7 +141,7 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
context = super().get_context_data(**kwargs)
context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
.order_by('category')
- context['title'] = _("Consommations")
+ context['title'] = _("Consumptions")
# select2 compatibility
context['no_cache'] = True
diff --git a/entrypoint.sh b/entrypoint.sh
index f05e962a..e5a22a5a 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -2,12 +2,17 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+if [ -z ${NOTE_URL+x} ]; then
+ echo "Warning: your env files are not configurated."
+else
+ sed -i -e "s/example.com/$DOMAIN/g" /code/apps/member/fixtures/initial.json
+ sed -i -e "s/localhost/$NOTE_URL/g" /code/note_kfet/fixtures/initial.json
+ sed -i -e "s/\.\*/https?:\/\/$NOTE_URL\/.*/g" /code/note_kfet/fixtures/cas.json
+ sed -i -e "s/REPLACEME/La Note Kfet \\\\ud83c\\\\udf7b/g" /code/note_kfet/fixtures/cas.json
+fi
+
python manage.py compilemessages
python manage.py makemigrations
-
-# Wait for database
-sleep 5
python manage.py migrate
-# TODO: use uwsgi in production
python manage.py runserver 0.0.0.0:8000
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index ce17f5de..6c60a9fe 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-07 18:01+0100\n"
+"POT-Creation-Date: 2020-03-11 11:44+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME {% trans "Field filters" %}
+{% crispy filter.form %}
diff --git a/templates/django_filters/rest_framework/form.html b/templates/django_filters/rest_framework/form.html
new file mode 100644
index 00000000..b116e353
--- /dev/null
+++ b/templates/django_filters/rest_framework/form.html
@@ -0,0 +1,6 @@
+{% load i18n %}
+{% trans "Field filters" %}
+
diff --git a/templates/django_filters/widgets/multiwidget.html b/templates/django_filters/widgets/multiwidget.html
new file mode 100644
index 00000000..089ddb20
--- /dev/null
+++ b/templates/django_filters/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}
diff --git a/templates/member/club_form.html b/templates/member/club_form.html
index 3fc2dd8b..577297bb 100644
--- a/templates/member/club_form.html
+++ b/templates/member/club_form.html
@@ -1,11 +1,12 @@
{% extends "base.html" %}
{% load static %}
+{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
-
+
{% endblock %}
diff --git a/templates/member/club_list.html b/templates/member/club_list.html
index f807c25c..16571113 100644
--- a/templates/member/club_list.html
+++ b/templates/member/club_list.html
@@ -1,10 +1,11 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
+{% load i18n %}
{% block content %}
{% render_table table %}
-New Club
+{% trans "New club" %}
{% endblock %}
{% block extrajavascript %}
diff --git a/templates/member/signup.html b/templates/member/signup.html
index e682bd9b..d7b3c23e 100644
--- a/templates/member/signup.html
+++ b/templates/member/signup.html
@@ -2,16 +2,16 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% load i18n %}
-{% block title %}Sign Up{% endblock %}
+{% block title %}{% trans "Sign up" %}{% endblock %}
{% block content %}
- Sign up
+ {% trans "Sign up" %}
{% endblock %}
diff --git a/templates/note/transactiontemplate_form.html b/templates/note/transactiontemplate_form.html
index 3fc2dd8b..1f9a574a 100644
--- a/templates/note/transactiontemplate_form.html
+++ b/templates/note/transactiontemplate_form.html
@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% load static %}
+{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
-
+