Compare commits
168 Commits
v1.0.3
...
281e6e4e73
Author | SHA1 | Date | |
---|---|---|---|
281e6e4e73 | |||
fe029893b0 | |||
767e98c2a3 | |||
1bdad76fe9 | |||
0196db7fff | |||
1f53ad4407 | |||
018f6e3f13 | |||
9752a030d9 | |||
b27bdb090d | |||
55a0fbb6cb | |||
c356534309 | |||
51315a0555 | |||
e5f9fe2cf5 | |||
c630a3fbd5 | |||
79b8ebeca4 | |||
dc14ba0101 | |||
6028bfeb56 | |||
bd9773a8af | |||
cdeb76d9f8 | |||
ac4574200d | |||
b17d31e8ee | |||
30d27459dd | |||
333f7aa284 | |||
587314e03c | |||
9f888a5281 | |||
88b1a25ca0 | |||
8cb50f58f2 | |||
041a8f20a9 | |||
b1ffb28532 | |||
6225fb51f1 | |||
1dd74e8024 | |||
1af9f5f23c | |||
83d5a7ceff | |||
a7cba0a4a3 | |||
ccd9a66ab9 | |||
c7a92fa4b2 | |||
5f1b698d58 | |||
0a5368d23f | |||
26b351a51c | |||
1836677c47 | |||
e7a98c86f0 | |||
eb5044490b | |||
983d7ec052 | |||
dc56deaf85 | |||
19d1ecfc66 | |||
694f54e1c4 | |||
b0c3eee699 | |||
cd942779ca | |||
0d0fdef363 | |||
7ed544b3ac | |||
821efbf78b | |||
a209e0d366 | |||
ef485e0628 | |||
1481aa0635 | |||
867bf9fd25 | |||
47fda0ea36 | |||
623290827a | |||
a87ce625f3 | |||
3559787fa7 | |||
bd6ed27ae5 | |||
43dc676747 | |||
caaeab6b0b | |||
54ba786884 | |||
80e109114f | |||
787005e60d | |||
414e103686 | |||
942d887c2e | |||
a63c34fe37 | |||
2be6133458 | |||
7975fe47a6 | |||
476fbceeea | |||
8fbaa0bdc8 | |||
a0de63effd | |||
09fb1d227e | |||
2e27d4f05c | |||
5d16dc4e7d | |||
3c34033bf5 | |||
131f508433 | |||
c1a353963a | |||
178ce2b579 | |||
9162319734 | |||
5d2a8e9b79 | |||
33c94d0720 | |||
5040e8e8ea | |||
c5697c4cb4 | |||
e188c5a153 | |||
94e1fdc93a | |||
d1ef367bab | |||
0fbb19c5fd | |||
21cbf2b21a | |||
185a2cabf2 | |||
7552e55c8d | |||
361de9f8b4 | |||
e2426bd6a6 | |||
7fea619a9f | |||
7b5eefcc0a | |||
e4aa16986f | |||
b92e6e4e10 | |||
dd675b3676 | |||
f50849b4f8 | |||
73ff35c232 | |||
a5df98224f | |||
2cb9ac8735 | |||
35d4849a28 | |||
96539d262f | |||
946674f59b | |||
a201d8376a | |||
a21b9275ea | |||
d4e85e8215 | |||
7af2ebba40 | |||
bd94400883 | |||
5558341c8c | |||
35ef82223c | |||
9ccac36831 | |||
2e71ce05a9 | |||
f2cb10b69f | |||
24c4edf2e3 | |||
213e9a8b12 | |||
2c56178b15 | |||
48a5b04579 | |||
549f56dc0b | |||
debeb33d46 | |||
6d7076b03e | |||
196df1e775 | |||
0d9891fbd8 | |||
b2b1f03b46 | |||
1c5ed2bd3f | |||
a7e87ea639 | |||
6f67d2c629 | |||
4b97ab2e2a | |||
dcfd0167e7 | |||
50a680eed2 | |||
226a2a6357 | |||
48462f2ffc | |||
260513ae3b | |||
210a3cc93c | |||
896095a44c | |||
3f997f94fa | |||
0801ad64ae | |||
64bd5ed546 | |||
4c390dce17 | |||
adacc293f5 | |||
968fa64d37 | |||
a481adbae4 | |||
4de2e987ef | |||
9e6342c929 | |||
74de358953 | |||
bbbdcc7247 | |||
feeb99041f | |||
96215cc1ff | |||
b7a71d911d | |||
2ee7f41dfe | |||
fb3337966e | |||
399a32bece | |||
82fea65b5e | |||
abc88d0118 | |||
b6b81a8b8f | |||
d228dbf225 | |||
516a7f4be5 | |||
2f8c9b54e7 | |||
e9f18c3ed9 | |||
e6f3084588 | |||
145e55da75 | |||
d3ba95cdca | |||
8ffb0ebb56 | |||
5038af9e34 | |||
819b4214c9 | |||
b8a93b0b75 |
@ -7,25 +7,10 @@ stages:
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
# Debian Buster
|
||||
# py37-django22:
|
||||
# stage: test
|
||||
# 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-oauth-toolkit python3-psycopg2 python3-pil
|
||||
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
# python3-bs4 python3-setuptools tox texlive-xetex
|
||||
# script: tox -e py37-django22
|
||||
|
||||
# Ubuntu 20.04
|
||||
py38-django22:
|
||||
# Ubuntu 22.04
|
||||
py310-django42:
|
||||
stage: test
|
||||
image: ubuntu:20.04
|
||||
image: ubuntu:22.04
|
||||
before_script:
|
||||
# Fix tzdata prompt
|
||||
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
||||
@ -37,12 +22,12 @@ py38-django22:
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py38-django22
|
||||
script: tox -e py310-django42
|
||||
|
||||
# Debian Bullseye
|
||||
py39-django22:
|
||||
# Debian Bookworm
|
||||
py311-django42:
|
||||
stage: test
|
||||
image: debian:bullseye
|
||||
image: debian:bookworm
|
||||
before_script:
|
||||
- >
|
||||
apt-get update &&
|
||||
@ -52,11 +37,11 @@ py39-django22:
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py39-django22
|
||||
script: tox -e py311-django42
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: debian:bullseye
|
||||
image: debian:bookworm
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y tox
|
||||
script: tox -e linters
|
||||
|
@ -4,13 +4,14 @@
|
||||
from datetime import timedelta
|
||||
from random import shuffle
|
||||
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Club
|
||||
from note.models import Note, NoteUser
|
||||
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
||||
from note_kfet.inputs import Autocomplete
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-28 08:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('note', '0006_trust'),
|
||||
('activity', '0004_opener'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='opener',
|
||||
options={'verbose_name': 'Opener', 'verbose_name_plural': 'Openers'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opener',
|
||||
name='opener',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.note', verbose_name='Opener'),
|
||||
),
|
||||
]
|
@ -38,6 +38,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</a>
|
||||
|
||||
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
|
||||
<button id="trigger" class="btn btn-secondary">Click me !</button>
|
||||
|
||||
<hr>
|
||||
|
||||
@ -63,15 +64,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
refreshBalance();
|
||||
}
|
||||
|
||||
function process_qrcode() {
|
||||
let name = alias_obj.val();
|
||||
$.get("/api/note/note?search=" + name + "&format=json").done(
|
||||
function (res) {
|
||||
let note = res.results[0];
|
||||
$.post("/api/activity/entry/?format=json", {
|
||||
csrfmiddlewaretoken: CSRF_TOKEN,
|
||||
activity: {{ activity.id }},
|
||||
note: note.id,
|
||||
guest: null
|
||||
}).done(function () {
|
||||
addMsg(interpolate(gettext(
|
||||
"Entry made for %s whose balance is %s €"),
|
||||
[note.name, note.balance / 100]), "success", 4000);
|
||||
reloadTable(true);
|
||||
}).fail(function (xhr) {
|
||||
errMsg(xhr.responseJSON, 4000);
|
||||
});
|
||||
}).fail(function (xhr) {
|
||||
errMsg(xhr.responseJSON, 4000);
|
||||
});
|
||||
}
|
||||
|
||||
alias_obj.keyup(function(event) {
|
||||
let code = event.originalEvent.keyCode
|
||||
if (65 <= code <= 122 || code === 13) {
|
||||
debounce(reloadTable)()
|
||||
}
|
||||
if (code === 0)
|
||||
process_qrcode();
|
||||
});
|
||||
|
||||
$(document).ready(init);
|
||||
|
||||
alias_obj2 = document.getElementById("alias");
|
||||
$("#trigger").click(function (e) {
|
||||
addMsg("Clicked", "success", 1000);
|
||||
alias_obj.val(alias_obj.val() + "\0");
|
||||
alias_obj2.dispatchEvent(new KeyboardEvent('keyup'));
|
||||
})
|
||||
function init() {
|
||||
$(".table-row").click(function (e) {
|
||||
let target = e.target.parentElement;
|
||||
|
@ -265,12 +265,11 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||
# Keep only users that have a note
|
||||
note_qs = note_qs.filter(note__noteuser__isnull=False)
|
||||
|
||||
# Keep only members
|
||||
# Keep only valid members
|
||||
note_qs = note_qs.filter(
|
||||
note__noteuser__user__memberships__club=activity.attendees_club,
|
||||
note__noteuser__user__memberships__date_start__lte=timezone.now(),
|
||||
note__noteuser__user__memberships__date_end__gte=timezone.now(),
|
||||
)
|
||||
note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced')
|
||||
|
||||
# Filter with permission backend
|
||||
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
|
||||
@ -330,7 +329,7 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
|
||||
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
||||
|
||||
activities_open = Activity.objects.filter(open=True).filter(
|
||||
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||
context["activities_open"] = [a for a in activities_open
|
||||
if PermissionBackend.check_perm(self.request,
|
||||
|
@ -2,7 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from django.conf.urls import include
|
||||
from django.urls import re_path
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import UserInformationView
|
||||
@ -14,40 +15,48 @@ router = routers.DefaultRouter()
|
||||
router.register('models', ContentTypeViewSet)
|
||||
router.register('user', UserViewSet)
|
||||
|
||||
if "member" in settings.INSTALLED_APPS:
|
||||
from member.api.urls import register_members_urls
|
||||
register_members_urls(router, 'members')
|
||||
|
||||
if "member" in settings.INSTALLED_APPS:
|
||||
if "activity" in settings.INSTALLED_APPS:
|
||||
from activity.api.urls import register_activity_urls
|
||||
register_activity_urls(router, 'activity')
|
||||
|
||||
if "note" in settings.INSTALLED_APPS:
|
||||
from note.api.urls import register_note_urls
|
||||
register_note_urls(router, 'note')
|
||||
|
||||
if "treasury" in settings.INSTALLED_APPS:
|
||||
from treasury.api.urls import register_treasury_urls
|
||||
register_treasury_urls(router, 'treasury')
|
||||
|
||||
if "permission" in settings.INSTALLED_APPS:
|
||||
from permission.api.urls import register_permission_urls
|
||||
register_permission_urls(router, 'permission')
|
||||
if "food" in settings.INSTALLED_APPS:
|
||||
from food.api.urls import register_food_urls
|
||||
register_food_urls(router, 'food')
|
||||
|
||||
if "logs" in settings.INSTALLED_APPS:
|
||||
from logs.api.urls import register_logs_urls
|
||||
register_logs_urls(router, 'logs')
|
||||
|
||||
if "member" in settings.INSTALLED_APPS:
|
||||
from member.api.urls import register_members_urls
|
||||
register_members_urls(router, 'members')
|
||||
|
||||
if "note" in settings.INSTALLED_APPS:
|
||||
from note.api.urls import register_note_urls
|
||||
register_note_urls(router, 'note')
|
||||
|
||||
if "permission" in settings.INSTALLED_APPS:
|
||||
from permission.api.urls import register_permission_urls
|
||||
register_permission_urls(router, 'permission')
|
||||
|
||||
if "treasury" in settings.INSTALLED_APPS:
|
||||
from treasury.api.urls import register_treasury_urls
|
||||
register_treasury_urls(router, 'treasury')
|
||||
|
||||
if "wei" in settings.INSTALLED_APPS:
|
||||
from wei.api.urls import register_wei_urls
|
||||
register_wei_urls(router, 'wei')
|
||||
|
||||
if "wrapped" in settings.INSTALLED_APPS:
|
||||
from wrapped.api.urls import register_wrapped_urls
|
||||
register_wrapped_urls(router, 'wrapped')
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^me/', UserInformationView.as_view()),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
re_path('^', include(router.urls)),
|
||||
re_path('^me/', UserInformationView.as_view()),
|
||||
re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
||||
|
0
apps/food/__init__.py
Normal file
37
apps/food/admin.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db import transaction
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models import Allergen, BasicFood, QRCode, TransformedFood
|
||||
|
||||
|
||||
@admin.register(QRCode, site=admin_site)
|
||||
class QRCodeAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(BasicFood, site=admin_site)
|
||||
class BasicFoodAdmin(admin.ModelAdmin):
|
||||
@transaction.atomic
|
||||
def save_related(self, *args, **kwargs):
|
||||
ans = super().save_related(*args, **kwargs)
|
||||
args[1].instance.update()
|
||||
return ans
|
||||
|
||||
|
||||
@admin.register(TransformedFood, site=admin_site)
|
||||
class TransformedFoodAdmin(admin.ModelAdmin):
|
||||
exclude = ["allergens", "expiry_date"]
|
||||
|
||||
@transaction.atomic
|
||||
def save_related(self, request, form, *args, **kwargs):
|
||||
super().save_related(request, form, *args, **kwargs)
|
||||
form.instance.update()
|
||||
|
||||
|
||||
@admin.register(Allergen, site=admin_site)
|
||||
class AllergenAdmin(admin.ModelAdmin):
|
||||
pass
|
0
apps/food/api/__init__.py
Normal file
50
apps/food/api/serializers.py
Normal file
@ -0,0 +1,50 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Allergen, BasicFood, QRCode, TransformedFood
|
||||
|
||||
|
||||
class AllergenSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Allergen.
|
||||
The djangorestframework plugin will analyse the model `Allergen` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Allergen
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BasicFoodSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for BasicFood.
|
||||
The djangorestframework plugin will analyse the model `BasicFood` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = BasicFood
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class QRCodeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for QRCode.
|
||||
The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = QRCode
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TransformedFoodSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for TransformedFood.
|
||||
The djangorestframework plugin will analyse the model `TransformedFood` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
fields = '__all__'
|
14
apps/food/api/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet
|
||||
|
||||
|
||||
def register_food_urls(router, path):
|
||||
"""
|
||||
Configure router for Food REST API.
|
||||
"""
|
||||
router.register(path + '/allergen', AllergenViewSet)
|
||||
router.register(path + '/basic_food', BasicFoodViewSet)
|
||||
router.register(path + '/qrcode', QRCodeViewSet)
|
||||
router.register(path + '/transformed_food', TransformedFoodViewSet)
|
61
apps/food/api/views.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
from .serializers import AllergenSerializer, BasicFoodSerializer, QRCodeSerializer, TransformedFoodSerializer
|
||||
from ..models import Allergen, BasicFood, QRCode, TransformedFood
|
||||
|
||||
|
||||
class AllergenViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Allergen` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/food/allergen/
|
||||
"""
|
||||
queryset = Allergen.objects.order_by('id')
|
||||
serializer_class = AllergenSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', ]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class BasicFoodViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `BasicFood` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/food/basic_food/
|
||||
"""
|
||||
queryset = BasicFood.objects.order_by('id')
|
||||
serializer_class = BasicFoodSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', ]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class QRCodeViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `QRCode` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/food/qrcode/
|
||||
"""
|
||||
queryset = QRCode.objects.order_by('id')
|
||||
serializer_class = QRCodeSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['qr_code_number', ]
|
||||
search_fields = ['$qr_code_number', ]
|
||||
|
||||
|
||||
class TransformedFoodViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `TransformedFood` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/food/transformed_food/
|
||||
"""
|
||||
queryset = TransformedFood.objects.order_by('id')
|
||||
serializer_class = TransformedFoodSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', ]
|
||||
search_fields = ['$name', ]
|
11
apps/food/apps.py
Normal file
@ -0,0 +1,11 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FoodkfetConfig(AppConfig):
|
||||
name = 'food'
|
||||
verbose_name = _('food')
|
114
apps/food/forms.py
Normal file
@ -0,0 +1,114 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from random import shuffle
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from member.models import Club
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from note_kfet.inputs import Autocomplete
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import BasicFood, QRCode, TransformedFood
|
||||
|
||||
|
||||
class AddIngredientForms(forms.ModelForm):
|
||||
"""
|
||||
Form for add an ingredient
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(
|
||||
polymorphic_ctype__model='transformedfood',
|
||||
is_ready=False,
|
||||
is_active=True,
|
||||
was_eaten=False,
|
||||
)
|
||||
# Caution, the logic is inverted here, we flip the logic on saving in AddIngredientView
|
||||
self.fields['is_active'].initial = True
|
||||
self.fields['is_active'].label = _("Fully used")
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
fields = ('ingredient', 'is_active')
|
||||
|
||||
|
||||
class BasicFoodForms(forms.ModelForm):
|
||||
"""
|
||||
Form for add non-transformed food
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
|
||||
self.fields['name'].required = True
|
||||
self.fields['owner'].required = True
|
||||
|
||||
# Some example
|
||||
self.fields['name'].widget.attrs.update({"placeholder": _("Pasta METRO 5kg")})
|
||||
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
|
||||
shuffle(clubs)
|
||||
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||
|
||||
class Meta:
|
||||
model = BasicFood
|
||||
fields = ('name', 'owner', 'date_type', 'expiry_date', 'is_active', 'was_eaten', 'allergens',)
|
||||
widgets = {
|
||||
"owner": Autocomplete(
|
||||
model=Club,
|
||||
attrs={"api_url": "/api/members/club/"},
|
||||
),
|
||||
'expiry_date': DateTimePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class QRCodeForms(forms.ModelForm):
|
||||
"""
|
||||
Form for create QRCode
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
|
||||
is_active=True,
|
||||
was_eaten=False,
|
||||
polymorphic_ctype__model='transformedfood',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = QRCode
|
||||
fields = ('food_container',)
|
||||
|
||||
|
||||
class TransformedFoodForms(forms.ModelForm):
|
||||
"""
|
||||
Form for add transformed food
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
|
||||
self.fields['name'].required = True
|
||||
self.fields['owner'].required = True
|
||||
self.fields['creation_date'].required = True
|
||||
self.fields['creation_date'].initial = timezone.now
|
||||
self.fields['is_active'].initial = True
|
||||
self.fields['is_ready'].initial = False
|
||||
self.fields['was_eaten'].initial = False
|
||||
|
||||
# Some example
|
||||
self.fields['name'].widget.attrs.update({"placeholder": _("Lasagna")})
|
||||
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
|
||||
shuffle(clubs)
|
||||
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
fields = ('name', 'creation_date', 'owner', 'is_active', 'is_ready', 'was_eaten', 'shelf_life')
|
||||
widgets = {
|
||||
"owner": Autocomplete(
|
||||
model=Club,
|
||||
attrs={"api_url": "/api/members/club/"},
|
||||
),
|
||||
'creation_date': DateTimePickerInput(),
|
||||
}
|
84
apps/food/migrations/0001_initial.py
Normal file
@ -0,0 +1,84 @@
|
||||
# Generated by Django 2.2.28 on 2024-07-05 08:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('member', '0011_profile_vss_charter_read'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Allergen',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Allergen',
|
||||
'verbose_name_plural': 'Allergens',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Food',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
||||
('expiry_date', models.DateTimeField(verbose_name='expiry date')),
|
||||
('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
|
||||
('is_ready', models.BooleanField(default=False, verbose_name='is ready')),
|
||||
('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
|
||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_food.food_set+', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'foods',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BasicFood',
|
||||
fields=[
|
||||
('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
|
||||
('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)),
|
||||
('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Basic food',
|
||||
'verbose_name_plural': 'Basic foods',
|
||||
},
|
||||
bases=('food.food',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='QRCode',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('qr_code_number', models.PositiveIntegerField(unique=True, verbose_name='QR-code number')),
|
||||
('food_container', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', verbose_name='food container')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'QR-code',
|
||||
'verbose_name_plural': 'QR-codes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TransformedFood',
|
||||
fields=[
|
||||
('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
|
||||
('creation_date', models.DateTimeField(verbose_name='creation date')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='is active')),
|
||||
('ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.Food', verbose_name='transformed ingredient')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Transformed food',
|
||||
'verbose_name_plural': 'Transformed foods',
|
||||
},
|
||||
bases=('food.food',),
|
||||
),
|
||||
]
|
19
apps/food/migrations/0002_transformedfood_shelf_life.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.28 on 2024-07-06 20:37
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('food', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transformedfood',
|
||||
name='shelf_life',
|
||||
field=models.DurationField(default=datetime.timedelta(days=3), verbose_name='shelf life'),
|
||||
),
|
||||
]
|
62
apps/food/migrations/0003_create_14_allergens_mandatory.py
Normal file
@ -0,0 +1,62 @@
|
||||
from django.db import migrations
|
||||
|
||||
def create_14_mandatory_allergens(apps, schema_editor):
|
||||
"""
|
||||
There are 14 mandatory allergens, they are pre-injected
|
||||
"""
|
||||
|
||||
Allergen = apps.get_model("food", "allergen")
|
||||
|
||||
Allergen.objects.get_or_create(
|
||||
name="Gluten",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Fruits à coques",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Crustacés",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Céléri",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Oeufs",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Moutarde",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Poissons",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Soja",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Lait",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Sulfites",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Sésame",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Lupin",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Arachides",
|
||||
)
|
||||
Allergen.objects.get_or_create(
|
||||
name="Mollusques",
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('food', '0002_transformedfood_shelf_life'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_14_mandatory_allergens),
|
||||
]
|
||||
|
||||
|
28
apps/food/migrations/0004_auto_20240813_2358.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 2.2.28 on 2024-08-13 21:58
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('food', '0003_create_14_allergens_mandatory'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='transformedfood',
|
||||
name='is_active',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='is active'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='qrcode',
|
||||
name='food_container',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='QR_code', to='food.Food', verbose_name='food container'),
|
||||
),
|
||||
]
|
20
apps/food/migrations/0005_alter_food_polymorphic_ctype.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-28 08:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('food', '0004_auto_20240813_2358'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='food',
|
||||
name='polymorphic_ctype',
|
||||
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
|
||||
),
|
||||
]
|
0
apps/food/migrations/__init__.py
Normal file
226
apps/food/models.py
Normal file
@ -0,0 +1,226 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Club
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
|
||||
class QRCode(models.Model):
|
||||
"""
|
||||
An QRCode model
|
||||
"""
|
||||
qr_code_number = models.PositiveIntegerField(
|
||||
verbose_name=_("QR-code number"),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
food_container = models.ForeignKey(
|
||||
'Food',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='QR_code',
|
||||
verbose_name=_('food container'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("QR-code")
|
||||
verbose_name_plural = _("QR-codes")
|
||||
|
||||
def __str__(self):
|
||||
return _("QR-code number {qr_code_number}").format(qr_code_number=self.qr_code_number)
|
||||
|
||||
|
||||
class Allergen(models.Model):
|
||||
"""
|
||||
A list of allergen and alimentary restrictions
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Allergen')
|
||||
verbose_name_plural = _('Allergens')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Food(PolymorphicModel):
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
owner = models.ForeignKey(
|
||||
Club,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
verbose_name=_('owner'),
|
||||
)
|
||||
|
||||
allergens = models.ManyToManyField(
|
||||
Allergen,
|
||||
blank=True,
|
||||
verbose_name=_('allergen'),
|
||||
)
|
||||
|
||||
expiry_date = models.DateTimeField(
|
||||
verbose_name=_('expiry date'),
|
||||
null=False,
|
||||
)
|
||||
|
||||
was_eaten = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('was eaten'),
|
||||
)
|
||||
|
||||
# is_ready != is_active : is_ready signifie que la nourriture est prête à être manger,
|
||||
# is_active signifie que la nourriture n'est pas encore archivé
|
||||
# il sert dans les cas où il est plus intéressant que de l'open soit conservé (confiture par ex)
|
||||
|
||||
is_ready = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('is ready'),
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_('is active'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('food')
|
||||
verbose_name = _('foods')
|
||||
|
||||
|
||||
class BasicFood(Food):
|
||||
"""
|
||||
Food which has been directly buy on supermarket
|
||||
"""
|
||||
date_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=(
|
||||
("DLC", "DLC"),
|
||||
("DDM", "DDM"),
|
||||
)
|
||||
)
|
||||
|
||||
arrival_date = models.DateTimeField(
|
||||
verbose_name=_('arrival date'),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
# label = models.ImageField(
|
||||
# verbose_name=_('food label'),
|
||||
# max_length=255,
|
||||
# blank=False,
|
||||
# null=False,
|
||||
# upload_to='label/',
|
||||
# )
|
||||
|
||||
@transaction.atomic
|
||||
def update_allergens(self):
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_allergens()
|
||||
|
||||
@transaction.atomic
|
||||
def update_expiry_date(self):
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_expiry_date()
|
||||
|
||||
@transaction.atomic
|
||||
def update(self):
|
||||
self.update_allergens()
|
||||
self.update_expiry_date()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Basic food')
|
||||
verbose_name_plural = _('Basic foods')
|
||||
|
||||
|
||||
class TransformedFood(Food):
|
||||
"""
|
||||
Transformed food are a mix between basic food and meal
|
||||
"""
|
||||
creation_date = models.DateTimeField(
|
||||
verbose_name=_('creation date'),
|
||||
)
|
||||
|
||||
ingredient = models.ManyToManyField(
|
||||
Food,
|
||||
blank=True,
|
||||
symmetrical=False,
|
||||
related_name='transformed_ingredient_inv',
|
||||
verbose_name=_('transformed ingredient'),
|
||||
)
|
||||
|
||||
# Without microbiological analyzes, the storage time is 3 days
|
||||
shelf_life = models.DurationField(
|
||||
verbose_name=_("shelf life"),
|
||||
default=timedelta(days=3),
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def archive(self):
|
||||
# When a meal are archived, if it was eaten, update ingredient fully used for this meal
|
||||
raise NotImplementedError
|
||||
|
||||
@transaction.atomic
|
||||
def update_allergens(self):
|
||||
# When allergens are changed, simply update the parents' allergens
|
||||
old_allergens = list(self.allergens.all())
|
||||
self.allergens.clear()
|
||||
for ingredient in self.ingredient.iterator():
|
||||
self.allergens.set(self.allergens.union(ingredient.allergens.all()))
|
||||
|
||||
if old_allergens == list(self.allergens.all()):
|
||||
return
|
||||
super().save()
|
||||
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_allergens()
|
||||
|
||||
@transaction.atomic
|
||||
def update_expiry_date(self):
|
||||
# When expiry_date is changed, simply update the parents' expiry_date
|
||||
old_expiry_date = self.expiry_date
|
||||
self.expiry_date = self.creation_date + self.shelf_life
|
||||
for ingredient in self.ingredient.iterator():
|
||||
self.expiry_date = min(self.expiry_date, ingredient.expiry_date)
|
||||
|
||||
if old_expiry_date == self.expiry_date:
|
||||
return
|
||||
super().save()
|
||||
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_expiry_date()
|
||||
|
||||
@transaction.atomic
|
||||
def update(self):
|
||||
self.update_allergens()
|
||||
self.update_expiry_date()
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Transformed food')
|
||||
verbose_name_plural = _('Transformed foods')
|
19
apps/food/tables.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
|
||||
from .models import TransformedFood
|
||||
|
||||
|
||||
class TransformedFoodTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'food:food_view',
|
||||
args=[A('pk'), ],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', "owner", "allergens", "expiry_date")
|
20
apps/food/templates/food/add_ingredient_form.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body" id="form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
37
apps/food/templates/food/basicfood_detail.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }} {{ food.name }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
|
||||
<li><p>{% trans 'Arrival date' %} : {{ food.arrival_date }}</p></li>
|
||||
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }} ({{ food.date_type }})</p></li>
|
||||
<li>{% trans 'Allergens' %} :</li>
|
||||
<ul>
|
||||
{% for allergen in food.allergens.iterator %}
|
||||
<li>{{ allergen.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>
|
||||
<li><p>{% trans 'Active' %} : {{ food.is_active }}<p></li>
|
||||
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}<p></li>
|
||||
</ul>
|
||||
{% if can_update %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=food.pk %}">{% trans 'Update' %}</a>
|
||||
{% endif %}
|
||||
{% if can_add_ingredient %}
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
|
||||
{% trans 'Add to a meal' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
20
apps/food/templates/food/basicfood_form.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body" id="form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
55
apps/food/templates/food/create_qrcode_form.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body" id="form">
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}">
|
||||
{% trans 'New basic food' %}
|
||||
</a>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
<div class="card-body" id="profile_infos">
|
||||
<h4>{% trans "Copy constructor" %}</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="orderable">
|
||||
{% trans "Name" %}
|
||||
</th>
|
||||
<th class="orderable">
|
||||
{% trans "Owner" %}
|
||||
</th>
|
||||
<th class="orderable">
|
||||
{% trans "Arrival date" %}
|
||||
</th>
|
||||
<th class="orderable">
|
||||
{% trans "Expiry date" %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for basic in last_basic %}
|
||||
<tr>
|
||||
<td><a href="{% url "food:qrcode_basic_create" slug=slug %}?copy={{ basic.pk }}">{{ basic.name }}</a></td>
|
||||
<td>{{ basic.owner }}</td>
|
||||
<td>{{ basic.arrival_date }}</td>
|
||||
<td>{{ basic.expiry_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
39
apps/food/templates/food/qrcode_detail.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }} {% trans 'number' %} {{ qrcode.qr_code_number }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
<li><p>{% trans 'Name' %} : {{ qrcode.food_container.name }}</p></li>
|
||||
<li><p>{% trans 'Owner' %} : {{ qrcode.food_container.owner }}</p></li>
|
||||
<li><p>{% trans 'Expiry date' %} : {{ qrcode.food_container.expiry_date }}</p></li>
|
||||
</ul>
|
||||
{% if qrcode.food_container.polymorphic_ctype.model == 'basicfood' and can_update_basic %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=qrcode.food_container.pk %}" data-turbolinks="false">
|
||||
{% trans 'Update' %}
|
||||
</a>
|
||||
{% elif can_update_transformed %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=qrcode.food_container.pk %}">
|
||||
{% trans 'Update' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_view_detail %}
|
||||
<a class="btn btn-sm btn-primary" href="{% url "food:food_view" pk=qrcode.food_container.pk %}">
|
||||
{% trans 'View details' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_add_ingredient %}
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">
|
||||
{% trans 'Add to a meal' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
51
apps/food/templates/food/transformedfood_detail.html
Normal file
@ -0,0 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }} {{ food.name }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
|
||||
{% if can_see_ready %}
|
||||
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
|
||||
{% endif %}
|
||||
<li><p>{% trans 'Creation date' %} : {{ food.creation_date }}</p></li>
|
||||
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p></li>
|
||||
<li>{% trans 'Allergens' %} :</li>
|
||||
<ul>
|
||||
{% for allergen in food.allergens.iterator %}
|
||||
<li>{{ allergen.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>
|
||||
<li>{% trans 'Ingredients' %} :</li>
|
||||
<ul>
|
||||
{% for ingredient in food.ingredient.iterator %}
|
||||
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>
|
||||
<li><p>{% trans 'Shelf life' %} : {{ food.shelf_life }}</p></li>
|
||||
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
|
||||
<li><p>{% trans 'Active' %} : {{ food.is_active }}</p></li>
|
||||
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}</p></li>
|
||||
</ul>
|
||||
{% if can_update %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=food.pk %}">
|
||||
{% trans 'Update' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_add_ingredient %}
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
|
||||
{% trans 'Add to a meal' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
20
apps/food/templates/food/transformedfood_form.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body" id="form">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
60
apps/food/templates/food/transformedfood_list.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "Meal served" %}
|
||||
</h3>
|
||||
{% if can_create_meal %}
|
||||
<div class="card-footer">
|
||||
<a class="btn btn-sm btn-success" href="{% url 'food:transformed_create' %}" data-turbolinks="false">
|
||||
{% trans 'New meal' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if served.data %}
|
||||
{% render_table served %}
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
{% trans "There is no meal served." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "Open" %}
|
||||
</h3>
|
||||
{% if open.data %}
|
||||
{% render_table open %}
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
{% trans "There is no free meal." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "All meals" %}
|
||||
</h3>
|
||||
{% if table.data %}
|
||||
{% render_table table %}
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
{% trans "There is no meal." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
3
apps/food/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
21
apps/food/urls.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'food'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.TransformedListView.as_view(), name='food_list'),
|
||||
path('<int:slug>', views.QRCodeView.as_view(), name='qrcode_view'),
|
||||
path('detail/<int:pk>', views.FoodView.as_view(), name='food_view'),
|
||||
|
||||
path('<int:slug>/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'),
|
||||
path('<int:slug>/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
|
||||
path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
|
||||
path('update/basic/<int:pk>', views.BasicFoodUpdateView.as_view(), name='basic_update'),
|
||||
path('update/transformed/<int:pk>', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
|
||||
path('add/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
|
||||
]
|
421
apps/food/views.py
Normal file
@ -0,0 +1,421 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db import transaction
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django_tables2.views import MultiTableMixin
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.views.generic import DetailView, UpdateView
|
||||
from django.views.generic.list import ListView
|
||||
from django.forms import HiddenInput
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
|
||||
from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
|
||||
from .models import BasicFood, Food, QRCode, TransformedFood
|
||||
from .tables import TransformedFoodTable
|
||||
|
||||
|
||||
class AddIngredientView(ProtectQuerysetMixin, UpdateView):
|
||||
"""
|
||||
A view to add an ingredient
|
||||
"""
|
||||
model = Food
|
||||
template_name = 'food/add_ingredient_form.html'
|
||||
extra_context = {"title": _("Add the ingredient")}
|
||||
form_class = AddIngredientForms
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["pk"] = self.kwargs["pk"]
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
food = Food.objects.get(pk=self.kwargs['pk'])
|
||||
add_ingredient_form = AddIngredientForms(data=self.request.POST)
|
||||
if food.is_ready:
|
||||
form.add_error(None, _("The product is already prepared"))
|
||||
return self.form_invalid(form)
|
||||
if not add_ingredient_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# We flip logic ""fully used = not is_active""
|
||||
food.is_active = not food.is_active
|
||||
# Save the aliment and the allergens associed
|
||||
for transformed_pk in self.request.POST.getlist('ingredient'):
|
||||
transformed = TransformedFood.objects.get(pk=transformed_pk)
|
||||
if not transformed.is_ready:
|
||||
transformed.ingredient.add(food)
|
||||
transformed.update()
|
||||
food.save()
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse('food:food_list')
|
||||
|
||||
|
||||
class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
A view to update a basic food
|
||||
"""
|
||||
model = BasicFood
|
||||
form_class = BasicFoodForms
|
||||
template_name = 'food/basicfood_form.html'
|
||||
extra_context = {"title": _("Update an aliment")}
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
basic_food_form = BasicFoodForms(data=self.request.POST)
|
||||
if not basic_food_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
ans = super().form_valid(form)
|
||||
form.instance.update()
|
||||
return ans
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:food_view', kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
return context
|
||||
|
||||
|
||||
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
A view to see a food
|
||||
"""
|
||||
model = Food
|
||||
extra_context = {"title": _("Details of:")}
|
||||
context_object_name = "food"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["can_update"] = PermissionBackend.check_perm(self.request, "food.change_food")
|
||||
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
|
||||
return context
|
||||
|
||||
|
||||
class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
#####################################################################
|
||||
# TO DO
|
||||
# - this feature is very pratical for meat or fish, nevertheless we can implement this later
|
||||
# - fix picture save
|
||||
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
|
||||
#####################################################################
|
||||
"""
|
||||
A view to add a basic food with a qrcode
|
||||
"""
|
||||
model = BasicFood
|
||||
form_class = BasicFoodForms
|
||||
template_name = 'food/basicfood_form.html'
|
||||
extra_context = {"title": _("Add a new basic food with QRCode")}
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
basic_food_form = BasicFoodForms(data=self.request.POST)
|
||||
if not basic_food_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Save the aliment and the allergens associed
|
||||
basic_food = form.save(commit=False)
|
||||
# We assume the date of labeling and the same as the date of arrival
|
||||
basic_food.arrival_date = timezone.now()
|
||||
basic_food.is_ready = False
|
||||
basic_food.is_active = True
|
||||
basic_food.was_eaten = False
|
||||
basic_food._force_save = True
|
||||
basic_food.save()
|
||||
basic_food.refresh_from_db()
|
||||
|
||||
qrcode = QRCode()
|
||||
qrcode.qr_code_number = self.kwargs['slug']
|
||||
qrcode.food_container = basic_food
|
||||
qrcode.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
|
||||
|
||||
def get_sample_object(self):
|
||||
|
||||
# We choose a club which may work or BDE else
|
||||
owner_id = 1
|
||||
for membership in self.request.user.memberships.all():
|
||||
club_id = membership.club.id
|
||||
food = BasicFood(name="", expiry_date=timezone.now(), owner_id=club_id)
|
||||
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
|
||||
owner_id = club_id
|
||||
|
||||
return BasicFood(
|
||||
name="",
|
||||
expiry_date=timezone.now(),
|
||||
owner_id=owner_id,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# Some field are hidden on create
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
form = context['form']
|
||||
form.fields['is_active'].widget = HiddenInput()
|
||||
form.fields['was_eaten'].widget = HiddenInput()
|
||||
|
||||
copy = self.request.GET.get('copy', None)
|
||||
if copy is not None:
|
||||
basic = BasicFood.objects.get(pk=copy)
|
||||
for field in ['date_type', 'expiry_date', 'name', 'owner']:
|
||||
form.fields[field].initial = getattr(basic, field)
|
||||
for field in ['allergens']:
|
||||
form.fields[field].initial = getattr(basic, field).all()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
A view to add a new qrcode
|
||||
"""
|
||||
model = QRCode
|
||||
template_name = 'food/create_qrcode_form.html'
|
||||
form_class = QRCodeForms
|
||||
extra_context = {"title": _("Add a new QRCode")}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
qrcode = kwargs["slug"]
|
||||
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
|
||||
return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
|
||||
else:
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["slug"] = self.kwargs["slug"]
|
||||
|
||||
context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
|
||||
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
qrcode_food_form = QRCodeForms(data=self.request.POST)
|
||||
if not qrcode_food_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Save the qrcode
|
||||
qrcode = form.save(commit=False)
|
||||
qrcode.qr_code_number = self.kwargs["slug"]
|
||||
qrcode._force_save = True
|
||||
qrcode.save()
|
||||
qrcode.refresh_from_db()
|
||||
|
||||
qrcode.food_container.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
|
||||
|
||||
def get_sample_object(self):
|
||||
return QRCode(
|
||||
qr_code_number=self.kwargs["slug"],
|
||||
food_container_id=1
|
||||
)
|
||||
|
||||
|
||||
class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
A view to see a qrcode
|
||||
"""
|
||||
model = QRCode
|
||||
extra_context = {"title": _("QRCode")}
|
||||
context_object_name = "qrcode"
|
||||
slug_field = "qr_code_number"
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
qrcode = kwargs["slug"]
|
||||
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
|
||||
return super().get(*args, **kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
qr_code_number = self.kwargs['slug']
|
||||
qrcode = self.model.objects.get(qr_code_number=qr_code_number)
|
||||
|
||||
model = qrcode.food_container.polymorphic_ctype.model
|
||||
|
||||
if model == "basicfood":
|
||||
context["can_update_basic"] = PermissionBackend.check_perm(self.request, "food.change_basicfood")
|
||||
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_basicfood")
|
||||
if model == "transformedfood":
|
||||
context["can_update_transformed"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
|
||||
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_transformedfood")
|
||||
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
|
||||
return context
|
||||
|
||||
|
||||
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
A view to add a tranformed food
|
||||
"""
|
||||
model = TransformedFood
|
||||
template_name = 'food/transformedfood_form.html'
|
||||
form_class = TransformedFoodForms
|
||||
extra_context = {"title": _("Add a new meal")}
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
transformed_food_form = TransformedFoodForms(data=self.request.POST)
|
||||
if not transformed_food_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Save the aliment and allergens associated
|
||||
transformed_food = form.save(commit=False)
|
||||
transformed_food.expiry_date = transformed_food.creation_date
|
||||
transformed_food.is_active = True
|
||||
transformed_food.is_ready = False
|
||||
transformed_food.was_eaten = False
|
||||
transformed_food._force_save = True
|
||||
transformed_food.save()
|
||||
transformed_food.refresh_from_db()
|
||||
ans = super().form_valid(form)
|
||||
transformed_food.update()
|
||||
return ans
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:food_view', kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_sample_object(self):
|
||||
# We choose a club which may work or BDE else
|
||||
owner_id = 1
|
||||
for membership in self.request.user.memberships.all():
|
||||
club_id = membership.club.id
|
||||
food = TransformedFood(name="",
|
||||
creation_date=timezone.now(),
|
||||
expiry_date=timezone.now(),
|
||||
owner_id=club_id)
|
||||
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
|
||||
owner_id = club_id
|
||||
break
|
||||
|
||||
return TransformedFood(
|
||||
name="",
|
||||
owner_id=owner_id,
|
||||
creation_date=timezone.now(),
|
||||
expiry_date=timezone.now(),
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Some field are hidden on create
|
||||
form = context['form']
|
||||
form.fields['is_active'].widget = HiddenInput()
|
||||
form.fields['is_ready'].widget = HiddenInput()
|
||||
form.fields['was_eaten'].widget = HiddenInput()
|
||||
form.fields['shelf_life'].widget = HiddenInput()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
A view to update transformed product
|
||||
"""
|
||||
model = TransformedFood
|
||||
template_name = 'food/transformedfood_form.html'
|
||||
form_class = TransformedFoodForms
|
||||
extra_context = {'title': _('Update a meal')}
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
transformedfood_form = TransformedFoodForms(data=self.request.POST)
|
||||
if not transformedfood_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
ans = super().form_valid(form)
|
||||
form.instance.update()
|
||||
return ans
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:food_view', kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
return context
|
||||
|
||||
|
||||
class TransformedListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
||||
"""
|
||||
Displays ready TransformedFood
|
||||
"""
|
||||
model = TransformedFood
|
||||
tables = [TransformedFoodTable, TransformedFoodTable, TransformedFoodTable]
|
||||
extra_context = {"title": _("Transformed food")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).distinct()
|
||||
|
||||
def get_tables(self):
|
||||
tables = super().get_tables()
|
||||
|
||||
tables[0].prefix = "all-"
|
||||
tables[1].prefix = "open-"
|
||||
tables[2].prefix = "served-"
|
||||
return tables
|
||||
|
||||
def get_tables_data(self):
|
||||
# first table = all transformed food, second table = free, third = served
|
||||
return [
|
||||
self.get_queryset().order_by("-creation_date"),
|
||||
TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__lt=timezone.now())
|
||||
.filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
|
||||
.distinct()
|
||||
.order_by("-creation_date"),
|
||||
TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__gte=timezone.now())
|
||||
.filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
|
||||
.distinct()
|
||||
.order_by("-creation_date")
|
||||
]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# We choose a club which should work
|
||||
for membership in self.request.user.memberships.all():
|
||||
club_id = membership.club.id
|
||||
food = TransformedFood(
|
||||
name="",
|
||||
owner_id=club_id,
|
||||
creation_date=timezone.now(),
|
||||
expiry_date=timezone.now(),
|
||||
)
|
||||
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
|
||||
context['can_create_meal'] = True
|
||||
break
|
||||
|
||||
tables = context["tables"]
|
||||
for name, table in zip(["table", "open", "served"], tables):
|
||||
context[name] = table
|
||||
return context
|
@ -3,7 +3,7 @@
|
||||
|
||||
import io
|
||||
|
||||
from PIL import Image, ImageSequence
|
||||
from bootstrap_datepicker_plus.widgets import DatePickerInput
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
@ -13,8 +13,9 @@ from django.forms import CheckboxSelectMultiple
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial, Alias
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||
from note_kfet.inputs import Autocomplete, AmountInput
|
||||
from permission.models import PermissionMask, Role
|
||||
from PIL import Image, ImageSequence
|
||||
|
||||
from .models import Profile, Club, Membership
|
||||
|
||||
@ -32,7 +33,7 @@ class UserForm(forms.ModelForm):
|
||||
# Django usernames can only contain letters, numbers, @, ., +, - and _.
|
||||
# We want to allow users to have uncommon and unpractical usernames:
|
||||
# That is their problem, and we have normalized aliases for us.
|
||||
return super()._get_validation_exclusions() + ["username"]
|
||||
return super()._get_validation_exclusions() | {"username"}
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@ -43,6 +44,7 @@ class ProfileForm(forms.ModelForm):
|
||||
"""
|
||||
A form for the extras field provided by the :model:`member.Profile` model.
|
||||
"""
|
||||
# Remove widget=forms.HiddenInput() if you want to use report frequency.
|
||||
report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
|
||||
|
||||
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
||||
@ -75,7 +77,8 @@ class ProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', )
|
||||
# Remove ml_[asso]_registration from exclude if the concerned association uses nk20 to manage its mailing list.
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', 'ml_sport_registration', )
|
||||
|
||||
|
||||
class ImageForm(forms.Form):
|
||||
|
@ -42,12 +42,12 @@ class UserTable(tables.Table):
|
||||
"""
|
||||
alias = tables.Column()
|
||||
|
||||
section = tables.Column(accessor='profile__section')
|
||||
section = tables.Column(accessor='profile__section', orderable=False)
|
||||
|
||||
# Override the column to let replace the URL
|
||||
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
|
||||
|
||||
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
|
||||
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"), orderable=False)
|
||||
|
||||
def render_email(self, record, value):
|
||||
# Replace the email by a dash if the user can't see the profile detail
|
||||
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note…">
|
||||
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note...">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label" for="only_active">
|
||||
<input type="checkbox" class="checkboxinput form-check-input" id="only_active"
|
||||
|
@ -60,7 +60,10 @@
|
||||
{% if user_object.pk == user.pk %}
|
||||
<div class="text-center">
|
||||
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
||||
<i class="fa fa-cogs"></i> {% trans 'API token' %}
|
||||
</a>
|
||||
<a class="small badge badge-secondary" href="{% url 'member:qr_code' user_object.pk %}">
|
||||
<i class="fa fa-qrcode"></i> {% trans 'QR Code' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
36
apps/member/templates/member/qr_code.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "QR Code for" %} {{ user_object.username }} ({{ user_object.first_name }} {{user_object.last_name }})
|
||||
</h3>
|
||||
<div class="text-center" id="qrcode">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script>
|
||||
var qrc = new QRCode(document.getElementById("qrcode"), {
|
||||
text: "{{ user_object.pk }}\0",
|
||||
width: 1024,
|
||||
height: 1024
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extracss %}
|
||||
<style>
|
||||
img {
|
||||
width: 100%
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
@ -25,4 +25,5 @@ urlpatterns = [
|
||||
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
||||
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
|
||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||
path('user/<int:pk>/qr_code/', views.QRCodeView.as_view(), name='qr_code'),
|
||||
]
|
||||
|
@ -26,6 +26,7 @@ from note_kfet.middlewares import _set_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
from django import forms
|
||||
|
||||
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
|
||||
CustomAuthenticationForm, MembershipRolesForm
|
||||
@ -72,11 +73,24 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form.fields['email'].required = True
|
||||
form.fields['email'].help_text = _("This address must be valid.")
|
||||
|
||||
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||
data=self.request.POST if self.request.POST else None)
|
||||
if not self.object.profile.report_frequency:
|
||||
del context['profile_form'].fields["last_report"]
|
||||
profile_form = self.profile_form(instance=context['user_object'].profile,
|
||||
data=self.request.POST if self.request.POST else None)
|
||||
|
||||
if not self.object.profile.report_frequency:
|
||||
del profile_form.fields["last_report"]
|
||||
|
||||
fields_to_check = list(profile_form.fields.keys())
|
||||
fields_modifiable = False
|
||||
|
||||
# Delete the fields for which the user does not have the permission to modify
|
||||
for field_name in fields_to_check:
|
||||
if not PermissionBackend.check_perm(self.request, f"member.change_profile_{field_name}", context['user_object'].profile):
|
||||
profile_form.fields[field_name].widget = forms.HiddenInput()
|
||||
else:
|
||||
fields_modifiable = True
|
||||
|
||||
if fields_modifiable:
|
||||
context['profile_form'] = profile_form
|
||||
|
||||
return context
|
||||
|
||||
@ -388,6 +402,14 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
|
||||
return context
|
||||
|
||||
class QRCodeView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Affiche le QR Code
|
||||
"""
|
||||
model = User
|
||||
context_object_name = "user_object"
|
||||
template_name = "member/qr_code.html"
|
||||
extra_context = {"title": _("QR Code")}
|
||||
|
||||
# ******************************* #
|
||||
# CLUB #
|
||||
|
@ -183,19 +183,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
# We match first an alias if it is matched without normalization,
|
||||
# then if the normalized pattern matches a normalized alias.
|
||||
queryset = queryset.filter(
|
||||
**{f'name{suffix}': alias_prefix + alias}
|
||||
).union(
|
||||
queryset.filter(
|
||||
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||
),
|
||||
all=True).union(
|
||||
queryset.filter(
|
||||
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
||||
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||
),
|
||||
all=True)
|
||||
Q(**{f'name{suffix}': alias_prefix + alias})
|
||||
| Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||
| Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
||||
)
|
||||
|
||||
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
|
||||
else queryset.order_by("name")
|
||||
|
@ -2,12 +2,13 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import datetime
|
||||
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
|
||||
from note_kfet.inputs import Autocomplete, AmountInput
|
||||
|
||||
from .models import TransactionTemplate, NoteClub, Alias
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-28 08:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('note', '0006_trust'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='note',
|
||||
name='polymorphic_ctype',
|
||||
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='polymorphic_ctype',
|
||||
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
|
||||
),
|
||||
]
|
@ -260,11 +260,13 @@ class ButtonTable(tables.Table):
|
||||
text=_('edit'),
|
||||
accessor='pk',
|
||||
verbose_name=_("Edit"),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
hideshow = tables.Column(
|
||||
verbose_name=_("Hide/Show"),
|
||||
accessor="pk",
|
||||
orderable=False,
|
||||
attrs={
|
||||
'td': {
|
||||
'class': 'col-sm-1',
|
||||
@ -276,7 +278,8 @@ class ButtonTable(tables.Table):
|
||||
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={'td': {'class': 'col-sm-1'}},
|
||||
verbose_name=_("Delete"), )
|
||||
verbose_name=_("Delete"),
|
||||
orderable=False, )
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
name="{{ widget.name }}"
|
||||
{# Other attributes are loaded #}
|
||||
{% for name, value in widget.attrs.items %}
|
||||
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
||||
{% if value is not False %}{{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}
|
||||
{% endfor %}>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">€</span>
|
||||
|
@ -31,3 +31,4 @@ class RoleAdmin(admin.ModelAdmin):
|
||||
Admin customisation for Role
|
||||
"""
|
||||
list_display = ('name', )
|
||||
filter_horizontal = ('permissions',)
|
||||
|
@ -135,18 +135,18 @@ class Permission(models.Model):
|
||||
|
||||
# A json encoded Q object with the following grammar
|
||||
# query -> [] | {} (the empty query representing all objects)
|
||||
# query -> ["AND", query, …] AND multiple queries
|
||||
# | ["OR", query, …] OR multiple queries
|
||||
# query -> ["AND", query, ...] AND multiple queries
|
||||
# | ["OR", query, ...] OR multiple queries
|
||||
# | ["NOT", query] Opposite of query
|
||||
# query -> {key: value, …} A list of fields and values of a Q object
|
||||
# query -> {key: value, ...} A list of fields and values of a Q object
|
||||
# key -> string A field name
|
||||
# value -> int | string | bool | null Literal values
|
||||
# | [parameter, …] A parameter. See compute_param for more details.
|
||||
# | [parameter, ...] A parameter. See compute_param for more details.
|
||||
# | {"F": oper} An F object
|
||||
# oper -> [string, …] A parameter. See compute_param for more details.
|
||||
# | ["ADD", oper, …] Sum multiple F objects or literal
|
||||
# oper -> [string, ...] A parameter. See compute_param for more details.
|
||||
# | ["ADD", oper, ...] Sum multiple F objects or literal
|
||||
# | ["SUB", oper, oper] Substract two F objects or literal
|
||||
# | ["MUL", oper, …] Multiply F objects or literals
|
||||
# | ["MUL", oper, ...] Multiply F objects or literals
|
||||
# | int | string | bool | null Literal values
|
||||
# | ["F", string] A field
|
||||
#
|
||||
|
@ -35,6 +35,8 @@ class PermissionScopes(BaseScopes):
|
||||
|
||||
|
||||
class PermissionOAuth2Validator(OAuth2Validator):
|
||||
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0
|
||||
|
||||
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
||||
"""
|
||||
User can request as many scope as he wants, including invalid scopes,
|
||||
|
@ -300,9 +300,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
# join_bde = True
|
||||
# join_kfet = True
|
||||
|
||||
if not join_bde:
|
||||
if not (join_bde or any(b for _, b in join_clubs)):
|
||||
# This software belongs to the BDE.
|
||||
form.add_error('join_bde', _("You must join the BDE."))
|
||||
form.add_error('join_bde', _("You must join a club."))
|
||||
return super().form_invalid(form)
|
||||
|
||||
if join_kfet and not join_bde:
|
||||
form.add_error('join_bde', _("You must also join the parent club BDE."))
|
||||
return super().form_invalid(form)
|
||||
|
||||
# Calculate required registration fee
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-28 08:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('note', '0007_alter_note_polymorphic_ctype_and_more'),
|
||||
('treasury', '0008_auto_20240322_0045'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sogecredit',
|
||||
name='transactions',
|
||||
field=models.ManyToManyField(blank=True, related_name='+', to='note.membershiptransaction', verbose_name='membership transactions'),
|
||||
),
|
||||
]
|
@ -37,6 +37,7 @@ class InvoiceTable(tables.Table):
|
||||
args=[A('id')],
|
||||
verbose_name=_("delete"),
|
||||
text=_("Delete"),
|
||||
orderable=False,
|
||||
attrs={
|
||||
'th': {
|
||||
'id': 'delete-membership-header'
|
||||
@ -70,6 +71,7 @@ class RemittanceTable(tables.Table):
|
||||
verbose_name=_("View"),
|
||||
args=[A("pk")],
|
||||
text=_("View"),
|
||||
orderable=False,
|
||||
attrs={
|
||||
'a': {'class': 'btn btn-primary'}
|
||||
}, )
|
||||
@ -97,6 +99,7 @@ class SpecialTransactionTable(tables.Table):
|
||||
verbose_name=_("Remittance"),
|
||||
args=[A("specialtransactionproxy__pk")],
|
||||
text=_("Add"),
|
||||
orderable=False,
|
||||
attrs={
|
||||
'a': {'class': 'btn btn-primary'}
|
||||
}, )
|
||||
@ -105,6 +108,7 @@ class SpecialTransactionTable(tables.Table):
|
||||
verbose_name=_("Remittance"),
|
||||
args=[A("specialtransactionproxy__pk")],
|
||||
text=_("Remove"),
|
||||
orderable=False,
|
||||
attrs={
|
||||
'a': {'class': 'btn btn-primary btn-danger'}
|
||||
}, )
|
||||
@ -130,10 +134,12 @@ class SogeCreditTable(tables.Table):
|
||||
|
||||
amount = tables.Column(
|
||||
verbose_name=_("Amount"),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
valid = tables.Column(
|
||||
verbose_name=_("Valid"),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
def render_amount(self, value):
|
||||
|
@ -183,10 +183,6 @@ Facture n°\FactureNum
|
||||
|
||||
}
|
||||
|
||||
\begin{center}
|
||||
TVA non applicable, article 293 B du CGI.
|
||||
\end{center}
|
||||
|
||||
|
||||
\end{document}
|
||||
{% endlanguage %}
|
||||
|
@ -1,13 +1,14 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bootstrap_datepicker_plus.widgets import DatePickerInput
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial, NoteUser
|
||||
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
|
||||
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
|
||||
|
||||
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
||||
|
||||
@ -80,6 +81,11 @@ class WEIChooseBusForm(forms.Form):
|
||||
|
||||
|
||||
class WEIMembershipForm(forms.ModelForm):
|
||||
caution_check = forms.BooleanField(
|
||||
required=False,
|
||||
label=_("Caution check given"),
|
||||
)
|
||||
|
||||
roles = forms.ModelMultipleChoiceField(
|
||||
queryset=WEIRole.objects,
|
||||
label=_("WEI Roles"),
|
||||
@ -148,6 +154,7 @@ class WEIMembership1AForm(WEIMembershipForm):
|
||||
"""
|
||||
Used to confirm registrations of first year members without choosing a bus now.
|
||||
"""
|
||||
caution_check = None
|
||||
roles = None
|
||||
|
||||
def clean(self):
|
||||
|
@ -2,11 +2,11 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||
from .wei2023 import WEISurvey2023
|
||||
from .wei2024 import WEISurvey2024
|
||||
|
||||
|
||||
__all__ = [
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
||||
|
||||
CurrentSurvey = WEISurvey2023
|
||||
CurrentSurvey = WEISurvey2024
|
||||
|
@ -82,7 +82,7 @@ WORDS = {
|
||||
5: "La quoi ?"
|
||||
}],
|
||||
"kokarde": ["Qu'est-ce que le mot Kokarde t'évoque ?", {
|
||||
1: "Vraiment pas mon truc les soirées…",
|
||||
1: "Vraiment pas mon truc les soirées...",
|
||||
2: "Bof, je viens pour manger et je repars aussitôt",
|
||||
3: "Je kiffe, good vibes",
|
||||
4: "Perso, je ne m'arrêterai pas de danser sur la piste !",
|
||||
@ -117,15 +117,15 @@ WORDS = {
|
||||
5: "Je pourrais en faire à n'importe qui. Pourquoi ne pas créer le club Câl[ENS] ?"
|
||||
}],
|
||||
"vomi": ["Quel est ton rapport au vomi ?", {
|
||||
1: "C'est compliqué…",
|
||||
1: "C'est compliqué...",
|
||||
2: "Jamais je ne vomis mais je nettoie quand mes potes vomissent",
|
||||
3: "Jamais je ne vomis et jamais je ne nettoie celui de quelqu'un d'autre",
|
||||
4: "Je vomis quelquefois, ça arrive, faites pas cette tête, mais je fins toujours par nettoyer !",
|
||||
5: "Je vomis à chaque soirée et ce n'est jamais moi qui nettoie"
|
||||
}],
|
||||
"kfet": ["Qu'est ce que la Kfet t'évoque ?", {
|
||||
1: "La Kfet, quel lieu de dépravé⋅es sérieux…",
|
||||
2: "C'est un endroit à l'hygiène plus que douteuse…",
|
||||
1: "La Kfet, quel lieu de dépravé⋅es sérieux...",
|
||||
2: "C'est un endroit à l'hygiène plus que douteuse...",
|
||||
3: "Téma les prix des boissons et des snacks, c'est aberrant !",
|
||||
4: "En vrai, c'est cool, petit billard, petit canapé, chill !",
|
||||
5: "Banger, j'y reste jusqu'à la fin de mes jours"
|
||||
@ -147,7 +147,7 @@ WORDS = {
|
||||
"scolarite": ["Comment tu vois ton cursus à l'ENS ?", {
|
||||
1: "La tranquillité et le travail",
|
||||
2: "On va s'amuser tout en bossant",
|
||||
3: "Ça va profiter et réviser au dernier moment pour les exams…",
|
||||
3: "Ça va profiter et réviser au dernier moment pour les exams...",
|
||||
4: "Nous festoierons sans songer aux conséquences",
|
||||
5: "Je ne vois qu'une seule issue : la débauche"
|
||||
}]
|
||||
|
378
apps/wei/forms/surveys/wei2024.py
Normal file
@ -0,0 +1,378 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
|
||||
from ...models import WEIMembership
|
||||
|
||||
|
||||
buses_descr = [
|
||||
[
|
||||
"Magi[Kar]p 🐙🎮🎲", "#ef5568", 1,
|
||||
"""Vous l'aurez compris au nom du bus, l'ambiance est aux jeux et à la culture geek ! Ici, vous trouverez une ambiance
|
||||
calme avec une bonne dose d'autodérision et de second degré. Que vous ayez besoin de beaucoup dormir pour tenir la soirée
|
||||
du lendemain, ou que vous souhaitiez faire nuit blanche pour jouer toute la nuit, vous pouvez nous rejoindre. Votre voix
|
||||
n'y survivra peut-être pas à force de chanter. PS : les meilleurs cocktails du WEI sont chez nous, à déguster, pas à
|
||||
siphonner !""",
|
||||
],
|
||||
[
|
||||
"Va[car]me 🎷🍎🔊", "#fd7a28", 3,
|
||||
"""Ici, c'est le bus du bruit. Si vous voulez réveiller les autres bus en musique, apprendre de merveilleuses
|
||||
mélodies au kazoo tout le week-end, ou simplement profiter d'une bonne ambiance musicale, le BDA et la
|
||||
F[ENS]foire sont là pour vous. Vous pourrez également goûter au célèbre cocktail de la fanfare, concocté
|
||||
pour l'occasion par les tout nouveaux "meilleurs artisans v*********** de France" ! Alors que vous soyez artiste
|
||||
dans l'âme ou que vous souhaitiez juste faire le plus grand Vacarme, rejoignez-nous !""",
|
||||
],
|
||||
[
|
||||
"[Kar]aïbes 🏝️🏴☠️🥥", "#a5cfdd", 3,
|
||||
"""Ahoy, explorateurs du WEI ! Le bus Karaibes t’invite à une traversée sous les tropiques, où l’ambiance est
|
||||
toujours au beau fixe ! ☀️🍹 Ici, c’est soleil, rhum, et bonne humeur assurée : une atmosphère de vacances où
|
||||
l’on se laisse porter par la chaleur humaine et la fête. Que tu sois un pirate en quête de sensations fortes ou
|
||||
un amateur de chill avec un cocktail à la main, tu seras à ta place dans notre bus. Les soirées seront marquées
|
||||
par des rythmes tropicaux qui te feront vibrer jusqu’à l’aube. Prêt à embarquer pour une aventure inoubliable
|
||||
avec les meilleurs matelots du WEI ? On t’attend sur le pont du Karaibes pour lever l’ancre ensemble !""",
|
||||
],
|
||||
[
|
||||
"[Kar]di [Bus] 🎙️💅", "#e46398", 2.5,
|
||||
"""Bienvenue à bord du Kardi Bus, la seul, l’unique, l’inimitable pépite de ce weekend d’intégration ! Inspiré par les
|
||||
icônes suprêmes de la pop culture telles les Bratz, les Winx et autres Mean Girls, notre bus est un sanctuaire de style,
|
||||
d’audace et de pur plaisir. A nos cotés attends toi à siroter tes meilleurs Cosmo, sex on the Beach et autres cocktails
|
||||
de maxi pétasse tout en papotant entre copains copines ! Si tu rejoins le Kardi Bus, tu entres dans un monde où tu
|
||||
pourras te déhancher sur du Beyoncé, Britney, Aya et autres reines de la pop ! À très vite, les futures stars du Kardi
|
||||
Bus !""",
|
||||
],
|
||||
[
|
||||
"Sparta[bus] 🐺🐒🏉", "#ebdac2", 5,
|
||||
"""Dans notre bus, on vous donne un avant goût des plus grandes assos de l'ENS : les Kyottes et l'Aspique (clubs de rugby
|
||||
féminin et masculin, mais pas que). Bien entendu, qui dit rugby dit les copaings, le pastaga et la Pena Bayona, mais vous
|
||||
verrez par vous même qu'on est ouvert⋅e à toutes propositions quand il s'agit de faire la fête. Pour les casse-cous comme
|
||||
pour les plus calmes, vous trouverez au bus Aspique-Kyottes les 2A+ qui vous feront kiffer votre WEI.""",
|
||||
],
|
||||
[
|
||||
"Zanzo[Bus] 🤯🚸🐒", "#FFFF", 3,
|
||||
"""Dans un entre-trois bien senti entre zinzinerie, enfance et vieillerie, le Zanzo[BUS] est un concentré de fun mêlé à
|
||||
de la dinguerie à gogo. N'hésitez plus et rejoignez-nous pour un WEI toujours plus déjanté !""",
|
||||
],
|
||||
[
|
||||
"Bran[Kar] 🍹🥳", "#6da1ac", 4,
|
||||
"""Si vous ne connaissez pas le Bran[Kar], c’est comme une grande famille qui fait un apéro, qui se bourre un peu la
|
||||
gueule en discutant des heures autour d’une table remplie de bouffe et de super bons cocktails (la plupart des
|
||||
barmen/barwomen du bus sont les barmans de Shakens), sauf qu’on est un bus du Wei (vous comprendrez bien le nom de notre
|
||||
bus en voyant l’état de certain·e·s). Il nous arrive de faire quelques conneries, mais surtout de jouer au Bière-pong en
|
||||
musique !""",
|
||||
],
|
||||
[
|
||||
"Techno [kar]ade 🔊🚩", "#8065a3", 3,
|
||||
"""Avis à tous·tes les gauchos, amoureux·ses de la fête et des manifs : le Techno [kar]ade vous ouvre grand ses bras pour
|
||||
finir en beauté votre première inté. Préparez-vous à vous abreuver de cocktails (savamment élaborés) à la vibration d’un
|
||||
système son fabriqué pour l’occasion. Des sets technos à « Mon père était tellement de gauche » en passant par « Female
|
||||
Body », le car accueillant les meilleures DJs du plateau saura animer le trajet aussi bien que les soirées. Si alcool et
|
||||
musique seront au rendez-vous, les maîtres mots sont sécurité et inclusivité. Qui que vous soyez et quelle que soit votre
|
||||
manière de vous amuser, notre objectif est que vous vous sentiez à l’aise pour rencontrer au mieux les 1A, les 2A et les
|
||||
(nombreux⋅ses) 3A+ qui auront répondu à l’appel. Bref, rejoignez-nous, on est super cools :)"""
|
||||
],
|
||||
[
|
||||
"[Bus]ka-P 🥇🍻🎤", "#7c4768", 4.5,
|
||||
"""Booska-p, c’est le « site N°1 du Rap français ». Le [Bus]ka-p ? Le bus N°1 sur l’ambiance au WEI. Les nuits vont être
|
||||
courtes, les cocktails vont couler à flots : tout sera réuni pour vivre un week-end dont tu te souviendras toute ta vie.
|
||||
Au programme pas un seul temps mort et un maximum de rencontres pour bien commencer ta première année à l’ENS. Et bien
|
||||
entendu, le tout accompagné des meilleurs sons, de Jul à Aya, en passant par ABBA et Sexion d’Assaut. Bref, si tu veux
|
||||
vivre un WEI d’anthologie et faire la fête, de jour comme de nuit, nous t’accueillons avec plaisir !""",
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def print_bus(i):
|
||||
return f"""<h1 style="color:{buses_descr[i][1]};-webkit-text-stroke: 2px black;font-size: 50px;">{buses_descr[i][0]}</h1><br>
|
||||
<b>Alcoolomètre : {buses_descr[i][2]} / 5 🍻</b><br><br>{buses_descr[i][3]}<br>"""
|
||||
|
||||
|
||||
def print_all_buses():
|
||||
liste = [print_bus(i) for i in range(len(buses_descr))]
|
||||
return "<br><br><br><br>".join(liste)
|
||||
|
||||
|
||||
def get_number_comment(i):
|
||||
if i == 1:
|
||||
return "Même pas en rêve"
|
||||
elif i == 2:
|
||||
return "Pas envie"
|
||||
elif i == 3:
|
||||
return "Mouais..."
|
||||
elif i == 4:
|
||||
return "Pourquoi pas !"
|
||||
elif i == 5:
|
||||
return "Ce bus ou rien !!!"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
WORDS = {
|
||||
"recap":
|
||||
[
|
||||
"""<b>Chèr⋅e 1A, te voilà arrivé⋅e au moment fatidique du choix de ton bus !<br><br><br>
|
||||
Ton bus est constitué des gens avec qui tu passeras la majorité de ton temps : que ce soit le voyage d'aller et de
|
||||
retour et les différentes activité qu'ils pourront te proposer tout au long du WEI donc choisis le bien !
|
||||
<br><br>Tu trouveras ci-dessous la liste de tous les bus ainsi qu'une description détaillée de ces derniers.
|
||||
Prends ton temps pour étudier chacun d'eux et quand tu te sens prêt⋅e, appuie sur le bouton « J'ai pris connaissance
|
||||
des bus » pour continuer
|
||||
<br>(pas besoin d'apprendre par cœur chaque bus, la description de chaque bus te sera rappeler avant de lui attribuer
|
||||
une note !)</b><br><br><br>""" + print_all_buses(),
|
||||
{
|
||||
"1": "J'ai pris connaissance des différents bus et me sent fin prêt à choisir celui qui me convient le mieux !",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
WORDS.update({
|
||||
f"bus{id}": [print_bus(id), {i: f"{get_number_comment(i)} ({i}/5)" for i in range(1, 5 + 1)}] for id in range(len(buses_descr))
|
||||
})
|
||||
|
||||
|
||||
class WEISurveyForm2024(forms.Form):
|
||||
"""
|
||||
Survey form for the year 2024.
|
||||
Members score the different buses, from which we calculate the best associated bus.
|
||||
"""
|
||||
def set_registration(self, registration):
|
||||
"""
|
||||
Filter the bus selector with the buses of the current WEI.
|
||||
"""
|
||||
information = WEISurveyInformation2024(registration)
|
||||
|
||||
question = information.questions[information.step]
|
||||
self.fields[question] = forms.ChoiceField(
|
||||
label=mark_safe(WORDS[question][0]),
|
||||
widget=forms.RadioSelect(),
|
||||
)
|
||||
answers = [(answer, WORDS[question][1][answer]) for answer in WORDS[question][1]]
|
||||
self.fields[question].choices = answers
|
||||
|
||||
|
||||
class WEIBusInformation2024(WEIBusInformation):
|
||||
"""
|
||||
For each question, the bus has ordered answers
|
||||
"""
|
||||
scores: dict
|
||||
|
||||
def __init__(self, bus):
|
||||
self.scores = {}
|
||||
for question in WORDS:
|
||||
self.scores[question] = []
|
||||
super().__init__(bus)
|
||||
|
||||
|
||||
class WEISurveyInformation2024(WEISurveyInformation):
|
||||
"""
|
||||
We store the id of the selected bus. We store only the name, but is not used in the selection:
|
||||
that's only for humans that try to read data.
|
||||
"""
|
||||
|
||||
step = 0
|
||||
questions = list(WORDS.keys())
|
||||
|
||||
def __init__(self, registration):
|
||||
for question in WORDS:
|
||||
setattr(self, str(question), None)
|
||||
super().__init__(registration)
|
||||
|
||||
|
||||
class WEISurvey2024(WEISurvey):
|
||||
"""
|
||||
Survey for the year 2024.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_year(cls):
|
||||
return 2024
|
||||
|
||||
@classmethod
|
||||
def get_survey_information_class(cls):
|
||||
return WEISurveyInformation2024
|
||||
|
||||
def get_form_class(self):
|
||||
return WEISurveyForm2024
|
||||
|
||||
def update_form(self, form):
|
||||
"""
|
||||
Filter the bus selector with the buses of the WEI.
|
||||
"""
|
||||
form.set_registration(self.registration)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.information.step += 1
|
||||
for question in WORDS:
|
||||
if question in form.cleaned_data:
|
||||
answer = form.cleaned_data[question]
|
||||
setattr(self.information, question, answer)
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def get_algorithm_class(cls):
|
||||
return WEISurveyAlgorithm2024
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""
|
||||
The survey is complete once the bus is chosen.
|
||||
"""
|
||||
for question in WORDS:
|
||||
if not getattr(self.information, question):
|
||||
return False
|
||||
return True
|
||||
|
||||
@lru_cache()
|
||||
def score(self, bus):
|
||||
if not self.is_complete():
|
||||
raise ValueError("Survey is not ended, can't calculate score")
|
||||
|
||||
bus_info = self.get_algorithm_class().get_bus_information(bus)
|
||||
# Score is the given score by the bus subtracted to the mid-score of the buses.
|
||||
s = 0
|
||||
for question in WORDS:
|
||||
s += bus_info.scores[question][str(getattr(self.information, question))]
|
||||
return s
|
||||
|
||||
@lru_cache()
|
||||
def scores_per_bus(self):
|
||||
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
|
||||
|
||||
@lru_cache()
|
||||
def ordered_buses(self):
|
||||
values = list(self.scores_per_bus().items())
|
||||
values.sort(key=lambda item: -item[1])
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
return super().clear_cache()
|
||||
|
||||
|
||||
class WEISurveyAlgorithm2024(WEISurveyAlgorithm):
|
||||
"""
|
||||
The algorithm class for the year 2024.
|
||||
We use Gale-Shapley algorithm to attribute 1y students into buses.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_survey_class(cls):
|
||||
return WEISurvey2024
|
||||
|
||||
@classmethod
|
||||
def get_bus_information_class(cls):
|
||||
return WEIBusInformation2024
|
||||
|
||||
def run_algorithm(self, display_tqdm=False):
|
||||
"""
|
||||
Gale-Shapley algorithm implementation.
|
||||
We modify it to allow buses to have multiple "weddings".
|
||||
"""
|
||||
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
|
||||
surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
|
||||
# Don't manage hardcoded people
|
||||
# surveys = [s for s in surveys if s.bus_id != None]
|
||||
# surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]
|
||||
|
||||
# surveys = [s for s in surveys if s.registration.user_id in free_users]
|
||||
|
||||
# hardcoded_first_year_mb = WEIMembership.objects.filter(bus != None,registration__first_year=True)
|
||||
# hardcoded_first_year = hardcoded_first_year_mb.values_list('user__id', 'bus__id')
|
||||
|
||||
hardcoded_first_year_mb = WEIMembership.objects.filter(registration__first_year=True)
|
||||
hardcoded_first_year = {mb.user.id if mb.bus else None: mb.bus.id if mb.bus else None for mb in hardcoded_first_year_mb}
|
||||
|
||||
# Reset previous algorithm run
|
||||
for survey in surveys:
|
||||
survey.free()
|
||||
if survey.registration.user_id in hardcoded_first_year.keys():
|
||||
survey.select_bus(hardcoded_first_year[survey.registration.user_id])
|
||||
survey.save()
|
||||
|
||||
non_men = [s for s in surveys if s.registration.gender != 'male']
|
||||
men = [s for s in surveys if s.registration.gender == 'male']
|
||||
|
||||
quotas = {}
|
||||
registrations = self.get_registrations()
|
||||
non_men_total = registrations.filter(~Q(gender='male')).count()
|
||||
for bus in self.get_buses():
|
||||
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
|
||||
free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
|
||||
quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)
|
||||
|
||||
tqdm_obj = None
|
||||
if display_tqdm:
|
||||
from tqdm import tqdm
|
||||
tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")
|
||||
|
||||
# Repartition for non men people first
|
||||
self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)
|
||||
|
||||
quotas = {}
|
||||
for bus in self.get_buses():
|
||||
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
|
||||
free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
|
||||
quotas[bus] = free_seats
|
||||
|
||||
if display_tqdm:
|
||||
tqdm_obj.close()
|
||||
|
||||
from tqdm import tqdm
|
||||
tqdm_obj = tqdm(total=len(men), desc="Hommes")
|
||||
|
||||
self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)
|
||||
|
||||
if display_tqdm:
|
||||
tqdm_obj.close()
|
||||
|
||||
# Clear cache information after running algorithm
|
||||
WEISurvey2024.clear_cache()
|
||||
|
||||
def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
|
||||
free_surveys = surveys.copy() # Remaining surveys
|
||||
while free_surveys: # Some students are not affected
|
||||
survey = free_surveys[0]
|
||||
buses = survey.ordered_buses() # Preferences of the student
|
||||
for bus, current_score in buses:
|
||||
if self.get_bus_information(bus).has_free_seats(surveys, quotas):
|
||||
# Selected bus has free places. Put student in the bus
|
||||
survey.select_bus(bus)
|
||||
survey.save()
|
||||
free_surveys.remove(survey)
|
||||
break
|
||||
else:
|
||||
# Current bus has not enough places. Remove the least preferred student from the bus if existing
|
||||
least_preferred_survey = None
|
||||
least_score = -1
|
||||
# Find the least student in the bus that has a lower score than the current student
|
||||
for survey2 in surveys:
|
||||
if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
|
||||
continue
|
||||
score2 = survey2.score(bus)
|
||||
if current_score <= score2: # Ignore better students
|
||||
continue
|
||||
if least_preferred_survey is None or score2 < least_score:
|
||||
least_preferred_survey = survey2
|
||||
least_score = score2
|
||||
|
||||
if least_preferred_survey is not None:
|
||||
# Remove the least student from the bus and put the current student in.
|
||||
# If it does not exist, choose the next bus.
|
||||
least_preferred_survey.free()
|
||||
least_preferred_survey.save()
|
||||
free_surveys.append(least_preferred_survey)
|
||||
survey.select_bus(bus)
|
||||
survey.save()
|
||||
free_surveys.remove(survey)
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"User {survey.registration.user} has no free seat")
|
||||
|
||||
if tqdm_obj is not None:
|
||||
tqdm_obj.n = len(surveys) - len(free_surveys)
|
||||
tqdm_obj.refresh()
|
18
apps/wei/migrations/0009_weiregistration_specific_diet.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-28 20:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wei', '0008_auto_20240111_1545'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='weiregistration',
|
||||
name='specific_diet',
|
||||
field=models.TextField(blank=True, default='', verbose_name='specific diet'),
|
||||
),
|
||||
]
|
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-29 20:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wei', '0009_weiregistration_specific_diet'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='weiregistration',
|
||||
name='specific_diet',
|
||||
),
|
||||
]
|
@ -12,7 +12,7 @@
|
||||
<div class="card-body">
|
||||
{% render_table bus_repartition_table %}
|
||||
<hr>
|
||||
<a href="{% url 'wei:wei_bus_1A_next' pk=club.pk %}" class="btn btn-block btn-success">{% trans "Start attribution!" %}</a>
|
||||
<a href="{% url 'wei:wei_bus_1A_next' pk=club.pk %}" class="btn btn-block btn-success">{% trans "Start attribution !" %}</a>
|
||||
<hr>
|
||||
{% render_table table %}
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<dt class="col-xl-6">{% trans 'department'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ object.user.profile.get_department_display }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt>
|
||||
<dt class="col-xl-6">{% trans 'health issues or specific diet'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ object.health_issues|default:"—" }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'suggested bus'|capfirst %}</dt>
|
||||
|
@ -64,7 +64,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ registration.birth_date }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt>
|
||||
<dt class="col-xl-6">{% trans 'health issues or specific diet'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ registration.health_issues }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'emergency contact name'|capfirst %}</dt>
|
||||
|
@ -6,8 +6,6 @@ from datetime import date, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from note.models import NoteUser
|
||||
|
||||
from ..forms.surveys.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023
|
||||
from ..models import Bus, WEIClub, WEIRegistration
|
||||
@ -127,44 +125,3 @@ class TestWEIAlgorithm(TestCase):
|
||||
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
|
||||
|
||||
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
|
||||
|
||||
def test_register_1a(self):
|
||||
"""
|
||||
Test register a first year member to the WEI and complete the survey
|
||||
"""
|
||||
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
user = User.objects.create(username="toto", email="toto@example.com")
|
||||
NoteUser.objects.create(user=user)
|
||||
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||
user=user.id,
|
||||
soge_credit=True,
|
||||
birth_date=date(2000, 1, 1),
|
||||
gender='nonbinary',
|
||||
clothing_cut='female',
|
||||
clothing_size='XS',
|
||||
health_issues='I am a bot',
|
||||
emergency_contact_name='NoteKfet2020',
|
||||
emergency_contact_phone='+33123456789',
|
||||
))
|
||||
qs = WEIRegistration.objects.filter(user_id=user.id)
|
||||
self.assertTrue(qs.exists())
|
||||
registration = qs.get()
|
||||
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
|
||||
for question in WORDS:
|
||||
# Fill 1A Survey, 20 pages
|
||||
# be careful if questionnary form change (number of page, type of answer...)
|
||||
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), {
|
||||
question: "1"
|
||||
})
|
||||
registration.refresh_from_db()
|
||||
survey = WEISurvey2023(registration)
|
||||
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
|
||||
302 if survey.is_complete() else 200)
|
||||
self.assertIsNotNone(getattr(survey.information, question), "Survey page " + question + " failed")
|
||||
survey = WEISurvey2023(registration)
|
||||
self.assertTrue(survey.is_complete())
|
||||
survey.select_bus(self.buses[0])
|
||||
survey.save()
|
||||
self.assertIsNotNone(survey.information.get_selected_bus())
|
||||
|
172
apps/wei/tests/test_wei_algorithm_2024.py
Normal file
@ -0,0 +1,172 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import random
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from note.models import NoteUser
|
||||
|
||||
from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024
|
||||
from ..models import Bus, WEIClub, WEIRegistration
|
||||
|
||||
|
||||
class TestWEIAlgorithm(TestCase):
|
||||
"""
|
||||
Run some tests to ensure that the WEI algorithm is working well.
|
||||
"""
|
||||
fixtures = ('initial',)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create some test data, with one WEI and 10 buses with random score attributions.
|
||||
"""
|
||||
self.user = User.objects.create_superuser(
|
||||
username="weiadmin",
|
||||
password="admin",
|
||||
email="admin@example.com",
|
||||
)
|
||||
self.user.save()
|
||||
self.client.force_login(self.user)
|
||||
sess = self.client.session
|
||||
sess["permission_mask"] = 42
|
||||
sess.save()
|
||||
|
||||
self.wei = WEIClub.objects.create(
|
||||
name="WEI 2024",
|
||||
email="wei2024@example.com",
|
||||
parent_club_id=2,
|
||||
membership_fee_paid=12500,
|
||||
membership_fee_unpaid=5500,
|
||||
membership_start='2024-01-01',
|
||||
membership_end='2024-12-31',
|
||||
date_start=date.today() + timedelta(days=2),
|
||||
date_end='2024-12-31',
|
||||
year=2024,
|
||||
)
|
||||
|
||||
self.buses = []
|
||||
for i in range(10):
|
||||
bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
|
||||
self.buses.append(bus)
|
||||
information = WEIBusInformation2024(bus)
|
||||
for question in WORDS:
|
||||
information.scores[question] = {answer: random.randint(1, 5) for answer in WORDS[question][1]}
|
||||
information.save()
|
||||
bus.save()
|
||||
|
||||
def test_survey_algorithm_small(self):
|
||||
"""
|
||||
There are only a few people in each bus, ensure that each person has its best bus
|
||||
"""
|
||||
# Add a few users
|
||||
for i in range(10):
|
||||
user = User.objects.create(username=f"user{i}")
|
||||
registration = WEIRegistration.objects.create(
|
||||
user=user,
|
||||
wei=self.wei,
|
||||
first_year=True,
|
||||
birth_date='2000-01-01',
|
||||
)
|
||||
information = WEISurveyInformation2024(registration)
|
||||
for question in WORDS:
|
||||
options = list(WORDS[question][1].keys())
|
||||
setattr(information, question, random.choice(options))
|
||||
information.step = 20
|
||||
information.save(registration)
|
||||
registration.save()
|
||||
|
||||
# Run algorithm
|
||||
WEISurvey2024.get_algorithm_class()().run_algorithm()
|
||||
|
||||
# Ensure that everyone has its first choice
|
||||
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
||||
survey = WEISurvey2024(r)
|
||||
preferred_bus = survey.ordered_buses()[0][0]
|
||||
chosen_bus = survey.information.get_selected_bus()
|
||||
self.assertEqual(preferred_bus, chosen_bus)
|
||||
|
||||
def test_survey_algorithm_full(self):
|
||||
"""
|
||||
Buses are full of first year people, ensure that they are happy
|
||||
"""
|
||||
# Add a lot of users
|
||||
for i in range(95):
|
||||
user = User.objects.create(username=f"user{i}")
|
||||
registration = WEIRegistration.objects.create(
|
||||
user=user,
|
||||
wei=self.wei,
|
||||
first_year=True,
|
||||
birth_date='2000-01-01',
|
||||
)
|
||||
information = WEISurveyInformation2024(registration)
|
||||
for question in WORDS:
|
||||
options = list(WORDS[question][1].keys())
|
||||
setattr(information, question, random.choice(options))
|
||||
information.step = 20
|
||||
information.save(registration)
|
||||
registration.save()
|
||||
|
||||
# Run algorithm
|
||||
WEISurvey2024.get_algorithm_class()().run_algorithm()
|
||||
|
||||
penalty = 0
|
||||
# Ensure that everyone seems to be happy
|
||||
# We attribute a penalty for each user that didn't have its first choice
|
||||
# The penalty is the square of the distance between the score of the preferred bus
|
||||
# and the score of the attributed bus
|
||||
# We consider it acceptable if the mean of this distance is lower than 5 %
|
||||
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
||||
survey = WEISurvey2024(r)
|
||||
chosen_bus = survey.information.get_selected_bus()
|
||||
buses = survey.ordered_buses()
|
||||
score = min(v for bus, v in buses if bus == chosen_bus)
|
||||
max_score = buses[0][1]
|
||||
penalty += (max_score - score) ** 2
|
||||
|
||||
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
|
||||
|
||||
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
|
||||
|
||||
def test_register_1a(self):
|
||||
"""
|
||||
Test register a first year member to the WEI and complete the survey
|
||||
"""
|
||||
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
user = User.objects.create(username="toto", email="toto@example.com")
|
||||
NoteUser.objects.create(user=user)
|
||||
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||
user=user.id,
|
||||
soge_credit=True,
|
||||
birth_date=date(2000, 1, 1),
|
||||
gender='nonbinary',
|
||||
clothing_cut='female',
|
||||
clothing_size='XS',
|
||||
health_issues='I am a bot',
|
||||
emergency_contact_name='NoteKfet2020',
|
||||
emergency_contact_phone='+33123456789',
|
||||
))
|
||||
qs = WEIRegistration.objects.filter(user_id=user.id)
|
||||
self.assertTrue(qs.exists())
|
||||
registration = qs.get()
|
||||
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
|
||||
for question in WORDS:
|
||||
# Fill 1A Survey, 10 pages
|
||||
# be careful if questionnary form change (number of page, type of answer...)
|
||||
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), {
|
||||
question: "1"
|
||||
})
|
||||
registration.refresh_from_db()
|
||||
survey = WEISurvey2024(registration)
|
||||
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
|
||||
302 if survey.is_complete() else 200)
|
||||
self.assertIsNotNone(getattr(survey.information, question), "Survey page " + question + " failed")
|
||||
survey = WEISurvey2024(registration)
|
||||
self.assertTrue(survey.is_complete())
|
||||
survey.select_bus(self.buses[0])
|
||||
survey.save()
|
||||
self.assertIsNotNone(survey.information.get_selected_bus())
|
@ -439,7 +439,7 @@ class TestWEIRegistration(TestCase):
|
||||
emergency_contact_phone='+33123456789',
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue("This user can't be in her/his first year since he/she has already participated to a WEI."
|
||||
self.assertTrue("This user can't be in her/his first year since he/she has already participated to a WEI."
|
||||
in str(response.context["form"].errors))
|
||||
|
||||
# Check that if the WEI is started, we can't register anyone
|
||||
@ -635,7 +635,7 @@ class TestWEIRegistration(TestCase):
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.context["form"].is_valid())
|
||||
self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors))
|
||||
self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors))
|
||||
|
||||
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
|
||||
roles=[WEIRole.objects.get(name="GC WEI").id],
|
||||
@ -767,7 +767,7 @@ class TestDefaultWEISurvey(TestCase):
|
||||
WEISurvey.update_form(None, None)
|
||||
|
||||
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
|
||||
self.assertEqual(CurrentSurvey.get_year(), 2023)
|
||||
self.assertEqual(CurrentSurvey.get_year(), 2024)
|
||||
|
||||
|
||||
class TestWeiAPI(TestAPI):
|
||||
|
@ -900,6 +900,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
form.fields["last_name"].initial = registration.user.last_name
|
||||
form.fields["first_name"].initial = registration.user.first_name
|
||||
|
||||
if "caution_check" in form.fields:
|
||||
form.fields["caution_check"].initial = registration.caution_check
|
||||
|
||||
if registration.soge_credit:
|
||||
form.fields["credit_type"].disabled = True
|
||||
form.fields["credit_type"].initial = NoteSpecial.objects.get(special_type="Virement bancaire")
|
||||
@ -941,6 +944,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
club = registration.wei
|
||||
user = registration.user
|
||||
|
||||
if "caution_check" in form.data:
|
||||
registration.caution_check = form.data["caution_check"] == "on"
|
||||
registration.save()
|
||||
membership = form.instance
|
||||
membership.user = user
|
||||
membership.club = club
|
||||
|
4
apps/wrapped/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'activity.apps.WrappedConfig'
|
17
apps/wrapped/admin.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models import Bde, Wrapped
|
||||
|
||||
|
||||
@admin.register(Bde, site=admin_site)
|
||||
class BdeAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(Wrapped, site=admin_site)
|
||||
class WrappedAdmin(admin.ModelAdmin):
|
||||
pass
|
0
apps/wrapped/api/__init__.py
Normal file
28
apps/wrapped/api/serializers.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Wrapped, Bde
|
||||
|
||||
|
||||
class WrappedSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Wrapped.
|
||||
The djangorestframework plugin will analyse the model `Wrapped` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Wrapped
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BdeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Bde.
|
||||
The djangorestframework plugin will analyse the model `Bde` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Bde
|
||||
fields = '__all__'
|
12
apps/wrapped/api/urls.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import WrappedViewSet, BdeViewSet
|
||||
|
||||
|
||||
def register_wrapped_urls(router, path):
|
||||
"""
|
||||
Configure router for Wrapped REST API.
|
||||
"""
|
||||
router.register(path + '/wrapped', WrappedViewSet)
|
||||
router.register(path + '/bde', BdeViewSet)
|
35
apps/wrapped/api/views.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
from .serializers import WrappedSerializer, BdeSerializer
|
||||
from ..models import Wrapped, Bde
|
||||
|
||||
|
||||
class WrappedViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Wrapped` objects, serialize it to JSON with the given
|
||||
serializer, then render it on /api/wrapped/wrapped/
|
||||
"""
|
||||
queryset = Wrapped.objects.order_by('id')
|
||||
serializer_class = WrappedSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['note', 'bde', ]
|
||||
search_fields = ['$note', ]
|
||||
|
||||
|
||||
class BdeViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Bde` objects, serialize it to JSON with the given
|
||||
serializer, then render it on /api/wrapped/bde/
|
||||
"""
|
||||
queryset = Bde.objects.order_by('id')
|
||||
serializer_class = BdeSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', ]
|
||||
search_fields = ['$name', ]
|
10
apps/wrapped/apps.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class WrappedConfig(AppConfig):
|
||||
name = 'wrapped'
|
||||
verbose_name = _('wrapped')
|
592
apps/wrapped/management/commands/generate_wrapped.py
Normal file
@ -0,0 +1,592 @@
|
||||
# Copyright (C) 2028-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.db.models import Q
|
||||
from note.models import Note, Transaction
|
||||
from member.models import User, Club, Membership
|
||||
from activity.models import Activity, Entry
|
||||
from wei.models import WEIClub
|
||||
|
||||
from ...models import Bde, Wrapped
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate wrapper for the annual BDE change"
|
||||
|
||||
def add_arguments(self, parser: ArgumentParser):
|
||||
parser.add_argument(
|
||||
'-b', '--bde',
|
||||
type=str,
|
||||
required=False,
|
||||
help="A list of BDE name, BDE1,BDE2,... (a BDE name cannot have ',')",
|
||||
dest='bde',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i', '--id',
|
||||
type=str,
|
||||
required=False,
|
||||
help="A list of BDE id, id1,id2,...",
|
||||
dest='bde_id',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-u', '--users',
|
||||
type=str,
|
||||
required=False,
|
||||
help="""User will have their(s) wrapped generated,
|
||||
all = all users
|
||||
adh = all users who have a valid memberships to BDE during the BDE considered
|
||||
supersuser = all superusers
|
||||
custom user1,user2,... = a list of username,
|
||||
custom_id id1,id2,... = a list of user id""",
|
||||
dest='user',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--club',
|
||||
type=str,
|
||||
required=False,
|
||||
help="""Club will have their(s) wrapped generated,
|
||||
all = all clubs,
|
||||
active = all clubs with at least one transaction during the BDE mandate considered,
|
||||
custom club1,club2,... = a list of club name,
|
||||
custom_id id1,id2,... = a list of club id""",
|
||||
dest='club',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f', '--force-change',
|
||||
required=False,
|
||||
action='store_true',
|
||||
help="if wrapped already exist change data_json",
|
||||
dest='change',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--no-creation',
|
||||
required=False,
|
||||
action='store_false',
|
||||
help="if wrapped don't already exist, don't generate it",
|
||||
dest='create',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# useful string for output
|
||||
red = '\033[31;1m'
|
||||
yellow = '\033[33;1m'
|
||||
green = '\033[32;1m'
|
||||
abort = red + 'ABORT'
|
||||
warning = yellow + 'WARNING'
|
||||
success = green + 'SUCCESS'
|
||||
|
||||
# Traitement des paramètres
|
||||
verb = options['verbosity']
|
||||
bde = []
|
||||
if options['bde']:
|
||||
bde_list = options['bde'].split(',')
|
||||
bde = [Bde.objects.get(name=bde_name) for bde_name in bde_list]
|
||||
|
||||
if options['bde_id']:
|
||||
if bde:
|
||||
if verb >= 1:
|
||||
print(warning)
|
||||
print(yellow + 'You already defined bde with their name !')
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
bde_id = options['bde_id'].split(',')
|
||||
bde = [Bde.objects.get(pk=i) for i in bde_id]
|
||||
|
||||
user = []
|
||||
if options['user']:
|
||||
if options['user'] == 'all':
|
||||
user = ['all', None]
|
||||
elif options['user'] == 'adh':
|
||||
user = ['adh', None]
|
||||
elif options['user'] == 'superuser':
|
||||
user = ['superuser', None]
|
||||
elif options['user'].split(' ')[0] == 'custom':
|
||||
user_list = options['user'].split(' ')[1].split(',')
|
||||
user = ['custom', [User.objects.get(username=u) for u in user_list]]
|
||||
elif options['user'].split(' ')[0] == 'custom_id':
|
||||
user_id = options['user'].split(' ')[1].split(',')
|
||||
user = ['custom_id', [User.objects.get(pk=u) for u in user_id]]
|
||||
else:
|
||||
if verb >= 1:
|
||||
print(warning)
|
||||
print(yellow + 'You user option is not recognized')
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
|
||||
club = []
|
||||
if options['club']:
|
||||
if options['club'] == 'all':
|
||||
club = ['all', None]
|
||||
elif options['club'] == 'active':
|
||||
club = ['active', None]
|
||||
elif options['club'].split(' ')[0] == 'custom':
|
||||
club_list = options['club'].split(' ')[1].split(',')
|
||||
club = ['custom', [Club.objects.get(name=club_name) for club_name in club_list]]
|
||||
elif options['club'].split(' ')[0] == 'custom_id':
|
||||
club_id = options['club'].split(' ')[1].split(',')
|
||||
club = ['custom_id', [Club.objects.get(pk=c) for c in club_id]]
|
||||
else:
|
||||
if verb >= 1:
|
||||
print(warning)
|
||||
print(yellow + 'You club option is not recognized')
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
|
||||
change = options['change']
|
||||
create = options['create']
|
||||
|
||||
# check if parameters are sufficient for generate wrapped with the desired option
|
||||
if not bde:
|
||||
if verb >= 1:
|
||||
print(warning)
|
||||
print(yellow + 'You have not selectionned a BDE !')
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
if not (user or club):
|
||||
if verb >= 1:
|
||||
print(warning)
|
||||
print(yellow + 'No club or user selected !')
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
|
||||
if verb >= 3:
|
||||
print('\033[1mOptions:\033[m')
|
||||
bde_str = ''
|
||||
for b in bde:
|
||||
bde_str += str(b)
|
||||
print('BDE: ' + bde_str)
|
||||
if user:
|
||||
print('User: ' + user[0])
|
||||
if club:
|
||||
print('Club: ' + club[0])
|
||||
print('change: ' + str(change))
|
||||
print('create: ' + str(create))
|
||||
print('')
|
||||
if not (change or create):
|
||||
if verb >= 1:
|
||||
print(warning)
|
||||
print(yellow + 'change and create is set to false, none wrapped will be created')
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
if verb >= 1 and change:
|
||||
print(warning)
|
||||
print(yellow + 'change is set to true, some wrapped may be replaced !')
|
||||
if verb >= 1 and not create:
|
||||
print(warning)
|
||||
print(yellow + 'create is set to false, wrapped will not be created !')
|
||||
if verb >= 3 or change or not create:
|
||||
a = str(input('\033[mContinue ? (y/n) ')).lower()
|
||||
if a in ['n', 'no', 'non', '0']:
|
||||
if verb >= 0:
|
||||
print(abort)
|
||||
return
|
||||
|
||||
note = self.convert_to_note(change, create, bde=bde, user=user, club=club, verb=verb)
|
||||
if verb >= 1:
|
||||
print("\033[32mUser and/or Club given has successfully convert in their note\033[m")
|
||||
global_data = self.global_data(bde, verb=verb)
|
||||
if verb >= 1:
|
||||
print("\033[32mGlobal data has been successfully generated\033[m")
|
||||
|
||||
unique_data = self.unique_data(bde, note, global_data=global_data, verb=verb)
|
||||
if verb >= 1:
|
||||
print("\033[32mUnique data has been successfully generated\033[m")
|
||||
|
||||
self.make_wrapped(unique_data, note, bde, change, create, verb=verb)
|
||||
if verb >= 1:
|
||||
print(green + "The wrapped has been generated !")
|
||||
if verb >= 0:
|
||||
print(success)
|
||||
|
||||
return
|
||||
|
||||
def convert_to_note(self, change, create, bde=None, user=None, club=None, verb=1):
|
||||
notes = []
|
||||
for b in bde:
|
||||
note_for_bde = Note.objects.filter(pk__lte=-1)
|
||||
if user:
|
||||
if 'custom' in user[0]:
|
||||
for u in user[1]:
|
||||
query = Q(noteuser__user=u)
|
||||
note_for_bde |= Note.objects.filter(query)
|
||||
elif user[0] == 'all':
|
||||
query = Q(noteuser__user__pk__gte=-1)
|
||||
note_for_bde |= Note.objects.filter(query)
|
||||
elif user[0] == 'adh':
|
||||
m = Membership.objects.filter(club=1,
|
||||
date_start__lt=b.date_end,
|
||||
date_end__gt=b.date_start,
|
||||
).distinct('user')
|
||||
for membership in m:
|
||||
note_for_bde |= Note.objects.filter(noteuser__user=membership.user)
|
||||
|
||||
elif user[0] == 'superuser':
|
||||
query |= Q(noteuser__user__is_superuser=True)
|
||||
note_for_bde |= Note.objects.filter(query)
|
||||
|
||||
if club:
|
||||
if 'custom' in club[0]:
|
||||
for c in club[1]:
|
||||
query = Q(noteclub__club=c)
|
||||
note_for_bde |= Note.objects.filter(query)
|
||||
elif club[0] == 'all':
|
||||
query = Q(noteclub__club__pk__gte=-1)
|
||||
note_for_bde |= Note.objects.filter(query)
|
||||
elif club[0] == 'active':
|
||||
nc = Note.objects.filter(noteclub__club__pk__gte=-1)
|
||||
for noteclub in nc:
|
||||
if Transaction.objects.filter(
|
||||
Q(created_at__gte=b.date_start,
|
||||
created_at__lte=b.date_end) & (Q(source=noteclub) | Q(destination=noteclub))):
|
||||
note_for_bde |= Note.objects.filter(pk=noteclub.pk)
|
||||
|
||||
note_for_bde = self.filter_note(b, note_for_bde, change, create, verb=verb)
|
||||
notes.append(note_for_bde)
|
||||
if verb >= 2:
|
||||
print("\033[m{nb} note selectionned for bde {bde}".format(nb=len(note_for_bde), bde=b.name))
|
||||
return notes
|
||||
|
||||
def global_data(self, bde, verb=1):
|
||||
data = {}
|
||||
for b in bde:
|
||||
if b.name == 'Rave Part[list]':
|
||||
if verb >= 2:
|
||||
print("Begin to make global data")
|
||||
if verb >= 3:
|
||||
print('nb_transaction')
|
||||
# nb total de transactions
|
||||
data['nb_transaction'] = Transaction.objects.filter(
|
||||
created_at__gte=b.date_start,
|
||||
created_at__lte=b.date_end,
|
||||
valid=True).count()
|
||||
|
||||
if verb >= 3:
|
||||
print('nb_vieux_con')
|
||||
# nb total de vielleux con·ne·s derrière le bar
|
||||
button_id = [2884, 2585]
|
||||
transactions = Transaction.objects.filter(
|
||||
created_at__gte=b.date_start,
|
||||
created_at__lte=b.date_end,
|
||||
valid=True,
|
||||
recurrenttransaction__template__pk__in=button_id)
|
||||
|
||||
q = 0
|
||||
for t in transactions:
|
||||
q += t.quantity
|
||||
data['nb_vieux_con'] = q
|
||||
|
||||
if verb >= 3:
|
||||
print('nb_soiree')
|
||||
# nb total de soirée
|
||||
a_type_id = [1, 2, 4, 5, 7, 10]
|
||||
data['nb_soiree'] = Activity.objects.filter(
|
||||
date_end__gte=b.date_start,
|
||||
date_start__lte=b.date_end,
|
||||
valid=True,
|
||||
activity_type__pk__in=a_type_id).count()
|
||||
|
||||
if verb >= 3:
|
||||
print('pots, nb_entree_pot')
|
||||
# nb d'entrée totale aux pots
|
||||
pot_id = [1, 4, 10]
|
||||
pots = Activity.objects.filter(
|
||||
date_end__gte=b.date_start,
|
||||
date_start__lte=b.date_end,
|
||||
valid=True,
|
||||
activity_type__pk__in=pot_id)
|
||||
data['pots'] = pots # utile dans unique_data
|
||||
data['nb_entree_pot'] = 0
|
||||
for pot in pots:
|
||||
data['nb_entree_pot'] += Entry.objects.filter(activity=pot).count()
|
||||
|
||||
if verb >= 3:
|
||||
print('top3_buttons')
|
||||
# top 3 des boutons les plus cliqués
|
||||
transactions = Transaction.objects.filter(
|
||||
created_at__gte=b.date_start,
|
||||
created_at__lte=b.date_end,
|
||||
valid=True,
|
||||
amount__gt=0,
|
||||
recurrenttransaction__template__pk__gte=-1)
|
||||
|
||||
d = {}
|
||||
for t in transactions:
|
||||
if t.recurrenttransaction.template.name in d:
|
||||
d[t.recurrenttransaction.template.name] += t.quantity
|
||||
else:
|
||||
d[t.recurrenttransaction.template.name] = t.quantity
|
||||
|
||||
data['top3_buttons'] = list(sorted(d.items(), key=lambda item: item[1], reverse=True))[:3]
|
||||
|
||||
if verb >= 3:
|
||||
print('class_conso_all')
|
||||
# le classement des plus gros consommateurs (BDE + club)
|
||||
transactions = Transaction.objects.filter(
|
||||
created_at__gte=b.date_start,
|
||||
created_at__lte=b.date_end,
|
||||
valid=True,
|
||||
source__noteuser__user__pk__gte=-1,
|
||||
destination__noteclub__club__pk__gte=-1)
|
||||
|
||||
d = {}
|
||||
for t in transactions:
|
||||
if t.source in d:
|
||||
d[t.source] += t.total
|
||||
else:
|
||||
d[t.source] = t.total
|
||||
|
||||
data['class_conso_all'] = dict(sorted(d.items(), key=lambda item: item[1], reverse=True))
|
||||
|
||||
if verb >= 3:
|
||||
print('class_conso_bde')
|
||||
# le classement des plus gros consommateurs BDE
|
||||
transactions = Transaction.objects.filter(
|
||||
created_at__gte=b.date_start,
|
||||
created_at__lte=b.date_end,
|
||||
valid=True,
|
||||
source__noteuser__user__pk__gte=-1,
|
||||
destination=5)
|
||||
|
||||
d = {}
|
||||
for t in transactions:
|
||||
if t.source in d:
|
||||
d[t.source] += t.total
|
||||
else:
|
||||
d[t.source] = t.total
|
||||
|
||||
data['class_conso_bde'] = dict(sorted(d.items(), key=lambda item: item[1], reverse=True))
|
||||
|
||||
else:
|
||||
# make your wrapped or reuse previous wrapped
|
||||
raise NotImplementedError("The BDE: {bde_name} has not personalized wrapped, make it !"
|
||||
.format(bde_name=b.name))
|
||||
return data
|
||||
|
||||
def unique_data(self, bde, note, global_data=None, verb=1):
|
||||
data = []
|
||||
for i in range(len(bde)):
|
||||
data_bde = []
|
||||
if bde[i].name == 'Rave Part[list]':
|
||||
if verb >= 3:
|
||||
total = len(note[i])
|
||||
current = 0
|
||||
print('Make {nb} data for wrapped sponsored by {bde}'
|
||||
.format(nb=total, bde=bde[i].name))
|
||||
for n in note[i]:
|
||||
d = {}
|
||||
if 'user' in n.__dir__():
|
||||
# première conso du mandat
|
||||
transactions = Transaction.objects.filter(
|
||||
valid=True,
|
||||
recurrenttransaction__template__id__gte=-1,
|
||||
created_at__gte=bde[i].date_start,
|
||||
created_at__lte=bde[i].date_end,
|
||||
source=n,
|
||||
destination=5).order_by('created_at')
|
||||
if transactions:
|
||||
d['first_conso'] = transactions[0].template.name
|
||||
else:
|
||||
d['first_conso'] = ''
|
||||
# Wei + bus
|
||||
wei = WEIClub.objects.filter(
|
||||
date_start__lte=bde[i].date_end,
|
||||
date_end__gte=bde[i].date_start)
|
||||
if not wei:
|
||||
d['wei'] = ''
|
||||
d['bus'] = ''
|
||||
else:
|
||||
w = wei[0]
|
||||
memberships = Membership.objects.filter(club=w, user=n.user)
|
||||
if not memberships:
|
||||
d['wei'] = ''
|
||||
d['bus'] = ''
|
||||
else:
|
||||
alias = []
|
||||
for a in w.note.alias.iterator():
|
||||
alias.append(str(a))
|
||||
d['wei'] = alias[-1]
|
||||
d['bus'] = memberships[0].weimembership.bus.name
|
||||
# top3 conso
|
||||
transactions = Transaction.objects.filter(
|
||||
valid=True,
|
||||
created_at__gte=bde[i].date_start,
|
||||
created_at__lte=bde[i].date_end,
|
||||
source=n,
|
||||
amount__gt=0,
|
||||
recurrenttransaction__template__id__gte=-1)
|
||||
dt = {}
|
||||
dc = {}
|
||||
for t in transactions:
|
||||
if t.template.name in dt:
|
||||
dt[t.template.name] += t.quantity
|
||||
else:
|
||||
dt[t.template.name] = t.quantity
|
||||
if t.template.category.name in dc:
|
||||
dc[t.template.category.name] += t.quantity
|
||||
else:
|
||||
dc[t.template.category.name] = t.quantity
|
||||
|
||||
d['top3_conso'] = list(sorted(dt.items(), key=lambda item: item[1], reverse=True))[:3]
|
||||
# catégorie de bouton préférée
|
||||
if dc:
|
||||
d['top_category'] = list(sorted(dc.items(), key=lambda item: item[1], reverse=True))[0][0]
|
||||
else:
|
||||
d['top_category'] = ''
|
||||
# nombre de pot, et nombre d'entrée pot
|
||||
pots = global_data['pots']
|
||||
d['nb_pots'] = pots.count()
|
||||
|
||||
p = 0
|
||||
for pot in pots:
|
||||
if Entry.objects.filter(activity=pot, note=n):
|
||||
p += 1
|
||||
d['nb_pot_entry'] = p
|
||||
# ton nombre de rechargement
|
||||
d['nb_rechargement'] = Transaction.objects.filter(
|
||||
valid=True,
|
||||
created_at__gte=bde[i].date_start,
|
||||
created_at__lte=bde[i].date_end,
|
||||
destination=n,
|
||||
source__pk__in=[1, 2, 3, 4]).count()
|
||||
# ajout info globale spécifique user
|
||||
# classement et montant conso all
|
||||
d['class_part_all'] = len(global_data['class_conso_all'])
|
||||
if n in global_data['class_conso_all']:
|
||||
d['class_conso_all'] = list(global_data['class_conso_all']).index(n) + 1
|
||||
d['amount_conso_all'] = global_data['class_conso_all'][n] / 100
|
||||
else:
|
||||
d['class_conso_all'] = 0
|
||||
d['amount_conso_all'] = 0
|
||||
# classement et montant conso bde
|
||||
d['class_part_bde'] = len(global_data['class_conso_bde'])
|
||||
if n in global_data['class_conso_bde']:
|
||||
d['class_conso_bde'] = list(global_data['class_conso_bde']).index(n) + 1
|
||||
d['amount_conso_bde'] = global_data['class_conso_bde'][n] / 100
|
||||
else:
|
||||
d['class_conso_bde'] = 0
|
||||
d['amount_conso_bde'] = 0
|
||||
|
||||
if 'club' in n.__dir__():
|
||||
# plus gros consommateur
|
||||
transactions = Transaction.objects.filter(
|
||||
valid=True,
|
||||
created_at__lte=bde[i].date_end,
|
||||
created_at__gte=bde[i].date_start,
|
||||
destination=n,
|
||||
source__noteuser__user__pk__gte=-1)
|
||||
dt = {}
|
||||
|
||||
for t in transactions:
|
||||
if t.source.user.username in dt:
|
||||
dt[t.source.user.username] += t.total
|
||||
else:
|
||||
dt[t.source.user.username] = t.total
|
||||
if dt:
|
||||
d['big_consumer'] = list(sorted(dt.items(), key=lambda item: item[1], reverse=True))[0]
|
||||
d['big_consumer'] = (d['big_consumer'][0], d['big_consumer'][1] / 100)
|
||||
else:
|
||||
d['big_consumer'] = ''
|
||||
# plus gros créancier
|
||||
transactions = Transaction.objects.filter(
|
||||
valid=True,
|
||||
created_at__lte=bde[i].date_end,
|
||||
created_at__gte=bde[i].date_start,
|
||||
source=n,
|
||||
destination__noteuser__user__pk__gte=-1)
|
||||
dt = {}
|
||||
|
||||
for t in transactions:
|
||||
if t.destination.user.username in dt:
|
||||
dt[t.destination.user.username] += t.total
|
||||
else:
|
||||
dt[t.destination.user.username] = t.total
|
||||
if dt:
|
||||
d['big_creancier'] = list(sorted(dt.items(), key=lambda item: item[1], reverse=True))[0]
|
||||
d['big_creancier'] = (d['big_creancier'][0], d['big_creancier'][1] / 100)
|
||||
else:
|
||||
d['big_creancier'] = ''
|
||||
# nb de soirée organisée
|
||||
d['nb_soiree_orga'] = Activity.objects.filter(
|
||||
valid=True,
|
||||
date_start__lte=bde[i].date_end,
|
||||
date_end__gte=bde[i].date_start,
|
||||
organizer=n.club).count()
|
||||
# nb de membres cumulé
|
||||
d['nb_member'] = Membership.objects.filter(
|
||||
date_start__lte=bde[i].date_end,
|
||||
date_end__gte=bde[i].date_start,
|
||||
club=n.club).distinct('user').count()
|
||||
|
||||
# ajout info globale
|
||||
# top3 button
|
||||
d['glob_top3_conso'] = global_data['top3_buttons']
|
||||
# nb entree pot
|
||||
d['glob_nb_entree_pot'] = global_data['nb_entree_pot']
|
||||
# nb soiree
|
||||
d['glob_nb_soiree'] = global_data['nb_soiree']
|
||||
# nb vieux con
|
||||
d['glob_nb_vieux_con'] = global_data['nb_vieux_con']
|
||||
# nb transaction
|
||||
d['glob_nb_transaction'] = global_data['nb_transaction']
|
||||
|
||||
data_bde.append(json.dumps(d))
|
||||
if verb >= 3:
|
||||
current += 1
|
||||
print('\033[2K' + '({c}/{t})'.format(c=current, t=total) + '\033[1A')
|
||||
|
||||
else:
|
||||
# make your wrapped or reuse previous wrapped
|
||||
raise NotImplementedError("The BDE: {bde_name} has not personalized wrapped, make it !"
|
||||
.format(bde_name=bde[i].name))
|
||||
data.append(data_bde)
|
||||
return data
|
||||
|
||||
def make_wrapped(self, unique_data, note, bde, change, create, verb=1):
|
||||
if verb >= 3:
|
||||
current = 0
|
||||
total = 0
|
||||
for n in note:
|
||||
total += len(n)
|
||||
print('\033[mMake {nb} wrapped'.format(nb=total))
|
||||
for i in range(len(bde)):
|
||||
for j in range(len(note[i])):
|
||||
if create and not Wrapped.objects.filter(bde=bde[i], note=note[i][j]):
|
||||
Wrapped(bde=bde[i],
|
||||
note=note[i][j],
|
||||
data_json=unique_data[i][j],
|
||||
public=False,
|
||||
generated=True).save()
|
||||
elif change:
|
||||
w = Wrapped.objects.get(bde=bde[i], note=note[i][j])
|
||||
w.data_json = unique_data[i][j]
|
||||
w.save()
|
||||
if verb >= 3:
|
||||
current += 1
|
||||
print('\033[2K' + '({c}/{t})'.format(c=current, t=total) + '\033[1A')
|
||||
return
|
||||
|
||||
def filter_note(self, bde, note, change, create, verb=1):
|
||||
if change and create:
|
||||
return list(note)
|
||||
if change and not create:
|
||||
note_new = []
|
||||
for n in note:
|
||||
if Wrapped.objects.filter(bde=bde, note=n):
|
||||
note_new.append(n)
|
||||
return note_new
|
||||
if not change and create:
|
||||
note_new = []
|
||||
for n in note:
|
||||
if not Wrapped.objects.filter(bde=bde, note=n):
|
||||
note_new.append(n)
|
||||
return note_new
|
86
apps/wrapped/migrations/0001_initial.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Generated by Django 4.2.15 on 2025-02-13 01:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("note", "0007_alter_note_polymorphic_ctype_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Bde",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255, verbose_name="name")),
|
||||
("date_start", models.DateTimeField(verbose_name="date start")),
|
||||
("date_end", models.DateTimeField(verbose_name="date end")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "BDE",
|
||||
"verbose_name_plural": "BDE",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Wrapped",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"generated",
|
||||
models.BooleanField(default=False, verbose_name="generated"),
|
||||
),
|
||||
("public", models.BooleanField(default=False, verbose_name="public")),
|
||||
(
|
||||
"data_json",
|
||||
models.TextField(
|
||||
default="{}",
|
||||
help_text="data in the wrapped and generated by the script generate_wrapped",
|
||||
verbose_name="data json",
|
||||
),
|
||||
),
|
||||
(
|
||||
"bde",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="+",
|
||||
to="wrapped.bde",
|
||||
verbose_name="bde",
|
||||
),
|
||||
),
|
||||
(
|
||||
"note",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="+",
|
||||
to="note.note",
|
||||
verbose_name="note",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Wrapped",
|
||||
"verbose_name_plural": "Wrappeds",
|
||||
"unique_together": {("note", "bde")},
|
||||
},
|
||||
),
|
||||
]
|
0
apps/wrapped/migrations/__init__.py
Normal file
80
apps/wrapped/models.py
Normal file
@ -0,0 +1,80 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import Note
|
||||
|
||||
|
||||
class Bde(models.Model):
|
||||
"""
|
||||
describe a BDE
|
||||
"""
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_('name'),
|
||||
)
|
||||
|
||||
date_start = models.DateTimeField(
|
||||
verbose_name=_('date start'),
|
||||
)
|
||||
|
||||
date_end = models.DateTimeField(
|
||||
verbose_name=_('date end'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('BDE')
|
||||
verbose_name_plural = _('BDE')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Wrapped(models.Model):
|
||||
"""
|
||||
A Wrapped is associated to a note, a BDE year,
|
||||
"""
|
||||
generated = models.BooleanField(
|
||||
verbose_name=_('generated'),
|
||||
default=False,
|
||||
)
|
||||
|
||||
public = models.BooleanField(
|
||||
verbose_name=_('public'),
|
||||
default=False,
|
||||
)
|
||||
|
||||
bde = models.ForeignKey(
|
||||
Bde,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
verbose_name=_('bde'),
|
||||
)
|
||||
|
||||
note = models.ForeignKey(
|
||||
Note,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
verbose_name=_('note'),
|
||||
)
|
||||
|
||||
data_json = models.TextField(
|
||||
default='{}',
|
||||
verbose_name=_('data json'),
|
||||
help_text=_('data in the wrapped and generated by the script generate_wrapped'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Wrapped')
|
||||
verbose_name_plural = _('Wrappeds')
|
||||
unique_together = ('note', 'bde')
|
||||
|
||||
def __str__(self):
|
||||
return 'NoteKfet Wrapped of {note} sponsored by {bde}'.format(bde=str(self.bde), note=str(self.note))
|
||||
|
||||
def makepublic(self):
|
||||
self.public = not self.public
|
||||
self.save()
|
||||
return
|
73
apps/wrapped/static/wrapped/css/1/custom.css
Normal file
@ -0,0 +1,73 @@
|
||||
:root {
|
||||
--accent-primary: #FF0065;
|
||||
--accent-secondary: #FFCB20;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JEMROKtrial-Regular";
|
||||
src: url("/static/wrapped/fonts/1/JEMROKtrial-Regular.ttf");
|
||||
}
|
||||
body {
|
||||
font-family: "JEMROKtrial-Regular", sans-serif;
|
||||
background: url("/static/wrapped/img/1/bg.png");
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
}
|
||||
#name {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 15px var(--accent-secondary);
|
||||
}
|
||||
.wrap-container {
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.category {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.ranking-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.ranking-progress {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
border-radius: 10px;
|
||||
}
|
BIN
apps/wrapped/static/wrapped/favicon/1/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
apps/wrapped/static/wrapped/favicon/1/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
apps/wrapped/static/wrapped/favicon/1/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 44 KiB |
9
apps/wrapped/static/wrapped/favicon/1/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/static/favicon/1/mstile-150x150.png"/>
|
||||
<TileColor>#00a300</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
apps/wrapped/static/wrapped/favicon/1/favicon-16x16.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/wrapped/static/wrapped/favicon/1/favicon-32x32.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
apps/wrapped/static/wrapped/favicon/1/favicon.ico
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
apps/wrapped/static/wrapped/favicon/1/mstile-150x150.png
Normal file
After Width: | Height: | Size: 34 KiB |
503
apps/wrapped/static/wrapped/favicon/1/safari-pinned-tab.svg
Normal file
@ -0,0 +1,503 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="933.000000pt" height="933.000000pt" viewBox="0 0 933.000000 933.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,933.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M4285 8909 c-1611 -113 -3013 -1115 -3640 -2600 -322 -763 -413
|
||||
-1617 -259 -2439 160 -853 598 -1670 1217 -2266 689 -665 1526 -1060 2497
|
||||
-1181 206 -25 734 -25 940 0 539 67 996 204 1455 436 609 308 1121 746 1528
|
||||
1304 368 504 642 1152 745 1762 47 277 57 397 57 730 0 335 -6 419 -51 695
|
||||
-189 1175 -882 2234 -1884 2882 -772 499 -1702 741 -2605 677z m336 -175 c88
|
||||
-16 130 -33 167 -67 29 -27 30 -110 2 -192 -30 -87 -35 -175 -16 -283 19 -107
|
||||
45 -165 141 -312 97 -148 117 -198 123 -301 4 -84 3 -87 -24 -118 -39 -44
|
||||
-100 -51 -336 -40 -255 12 -305 25 -371 98 -47 52 -68 124 -66 226 1 63 -1 80
|
||||
-12 78 -8 -1 -51 -7 -97 -13 l-82 -12 2 -142 c3 -126 1 -146 -18 -187 -42 -91
|
||||
-145 -159 -314 -210 -197 -58 -280 -8 -267 162 7 87 32 158 127 357 127 270
|
||||
164 434 141 631 -15 123 -9 188 20 225 44 56 188 100 384 116 145 11 393 4
|
||||
496 -16z m849 -107 c172 -44 281 -89 332 -138 39 -37 41 -40 34 -85 -3 -27
|
||||
-27 -85 -56 -138 -59 -110 -74 -171 -90 -372 -15 -181 -9 -236 30 -276 32 -31
|
||||
58 -35 98 -14 35 19 62 54 154 202 100 158 123 205 153 311 51 175 80 209 168
|
||||
200 104 -10 335 -126 424 -213 67 -63 71 -96 17 -148 -44 -43 -81 -61 -192
|
||||
-93 -114 -32 -167 -62 -235 -131 -78 -79 -120 -155 -168 -301 -47 -144 -74
|
||||
-195 -129 -245 -89 -80 -176 -70 -506 57 -253 98 -339 173 -351 305 -7 74 14
|
||||
158 66 260 61 122 75 180 68 292 -8 140 -32 201 -128 325 -63 81 -79 116 -79
|
||||
169 0 35 5 48 23 61 36 27 202 15 367 -28z m-2409 -257 c92 -26 148 -98 156
|
||||
-204 11 -135 -61 -297 -205 -459 -24 -27 -42 -50 -40 -52 2 -2 29 2 61 7 63
|
||||
11 129 0 174 -29 29 -19 55 -68 78 -148 13 -49 26 -69 70 -110 154 -146 134
|
||||
-194 -155 -372 -214 -132 -295 -164 -353 -140 -55 24 -71 70 -64 193 7 131 -9
|
||||
207 -52 246 -17 16 -40 28 -51 28 -56 0 -183 -72 -280 -159 l-45 -40 25 -21
|
||||
c14 -12 71 -44 127 -72 155 -78 243 -175 243 -268 0 -62 -43 -124 -191 -272
|
||||
-175 -174 -263 -225 -347 -197 -59 19 -91 49 -122 114 -25 49 -33 85 -45 200
|
||||
-18 164 -34 225 -83 318 -56 109 -110 161 -217 212 -101 48 -169 104 -180 148
|
||||
-18 71 74 200 269 379 386 355 806 642 1012 692 71 17 165 20 215 6z m3941
|
||||
-474 c91 -39 319 -242 542 -483 78 -84 151 -172 164 -195 46 -86 22 -175 -85
|
||||
-308 -146 -180 -264 -199 -360 -57 -79 117 -56 264 48 313 24 12 63 42 87 68
|
||||
52 57 55 91 12 134 -69 69 -185 48 -316 -57 -82 -65 -104 -135 -64 -209 33
|
||||
-62 45 -72 91 -76 47 -4 69 -25 69 -64 0 -41 -49 -112 -77 -112 -8 0 -41 23
|
||||
-75 51 -64 53 -104 76 -114 66 -3 -3 -10 -22 -14 -43 -21 -93 41 -237 127
|
||||
-297 53 -36 91 -35 129 4 44 46 61 53 89 35 60 -40 72 -184 22 -282 -50 -98
|
||||
-113 -130 -239 -121 -114 8 -195 49 -328 168 -58 52 -171 149 -252 216 -227
|
||||
191 -260 238 -222 320 29 62 129 153 223 203 46 25 105 64 131 87 81 73 161
|
||||
250 195 430 20 107 37 149 77 191 36 36 81 42 140 18z m-2454 -617 c3 -19 -23
|
||||
-49 -43 -49 -18 0 -45 27 -45 45 0 27 83 31 88 4z m382 -79 c8 -14 8 -26 0
|
||||
-40 -19 -35 -80 -20 -80 20 0 40 61 55 80 20z m409 -11 c42 -12 134 -44 205
|
||||
-71 501 -195 938 -549 1235 -1002 83 -126 202 -359 252 -493 19 -51 37 -93 40
|
||||
-93 12 0 58 98 90 193 18 53 37 97 43 97 6 0 47 -7 90 -15 117 -23 322 -29
|
||||
463 -16 171 17 327 62 452 132 29 17 59 28 66 26 22 -9 107 -131 150 -217 142
|
||||
-286 151 -529 30 -887 -19 -56 -40 -142 -46 -191 -11 -79 -10 -95 5 -142 25
|
||||
-77 65 -130 140 -186 80 -59 80 -49 -7 -247 -147 -334 -310 -554 -479 -650
|
||||
-131 -75 -197 -91 -363 -92 -164 0 -130 -18 -340 175 -200 183 -319 290 -325
|
||||
290 -3 0 -13 -24 -23 -52 -34 -98 -165 -352 -244 -473 -416 -633 -1062 -1051
|
||||
-1804 -1170 -151 -24 -382 -34 -554 -24 -465 28 -886 169 -1280 430 -92 61
|
||||
-143 102 -161 129 -62 94 -135 369 -137 516 -1 68 3 89 24 129 32 64 69 90
|
||||
124 89 61 -1 115 -37 221 -146 49 -52 119 -113 153 -136 107 -71 157 -53 182
|
||||
66 14 69 4 119 -45 231 -33 75 -66 209 -66 269 0 22 -14 47 -49 89 -138 166
|
||||
-233 361 -283 584 -31 138 -33 407 -4 538 104 479 427 851 879 1011 43 15 80
|
||||
30 82 32 5 4 -17 96 -30 131 -9 23 10 22 43 -2 15 -10 54 -34 87 -52 l60 -33
|
||||
202 -1 c164 0 204 2 208 13 3 8 2 63 -1 122 -8 128 -2 169 32 210 33 39 44 38
|
||||
44 -5 0 -50 29 -118 88 -209 50 -75 52 -81 52 -148 0 -39 -7 -84 -15 -106 -25
|
||||
-58 -34 -113 -28 -159 l5 -41 41 46 c69 80 87 131 87 250 0 56 3 102 8 102 17
|
||||
0 114 -95 132 -130 26 -51 34 -127 21 -190 -6 -29 -10 -55 -7 -57 7 -7 125 87
|
||||
150 120 25 33 49 104 40 119 -3 4 -23 8 -44 8 -98 0 -136 17 -217 98 -71 70
|
||||
-77 74 -89 57 -8 -10 -14 -25 -14 -32 0 -24 -18 -14 -63 37 -62 69 -82 154
|
||||
-53 224 27 65 76 106 191 162 122 58 230 131 240 163 9 28 -11 60 -41 67 -35
|
||||
9 -119 -1 -179 -21 -27 -9 -117 -49 -200 -88 -175 -82 -220 -91 -289 -57 -152
|
||||
73 -93 290 102 377 53 23 61 24 191 17 136 -7 136 -7 167 19 18 16 41 52 57
|
||||
94 15 37 37 80 49 95 28 33 134 98 162 98 11 0 54 -10 95 -21z m-901 -195 c37
|
||||
-26 23 -74 -23 -74 -43 0 -63 58 -27 79 23 14 24 14 50 -5z m-3516 -1078 c80
|
||||
-44 254 -103 344 -116 99 -14 316 -12 479 5 197 20 182 23 199 -32 30 -102 67
|
||||
-186 110 -253 27 -42 43 -76 39 -86 -14 -38 -131 125 -173 243 -13 35 -27 60
|
||||
-32 57 -30 -19 -116 -190 -151 -300 -48 -152 -60 -243 -54 -419 5 -169 15
|
||||
-238 74 -505 34 -156 36 -175 36 -355 1 -135 -4 -218 -16 -285 -22 -130 -61
|
||||
-277 -91 -344 -14 -31 -22 -56 -18 -56 4 1 104 93 222 205 197 188 229 212
|
||||
230 174 0 -6 -326 -312 -452 -422 -47 -42 -98 -81 -113 -87 -39 -15 -219 -12
|
||||
-300 5 -38 8 -112 35 -164 61 -121 59 -227 159 -320 299 -77 117 -216 389
|
||||
-251 493 l-25 73 35 27 c203 157 227 254 135 542 -103 320 -111 513 -33 745
|
||||
43 126 185 365 218 365 5 0 38 -16 72 -34z m1564 -2071 c26 -39 -31 -86 -64
|
||||
-53 -18 18 -15 55 6 67 23 14 42 10 58 -14z m434 -265 c8 -14 8 -26 0 -40 -19
|
||||
-35 -80 -20 -80 20 0 40 61 55 80 20z m-392 -162 c4 -34 -27 -54 -58 -38 -21
|
||||
11 -25 27 -14 55 5 11 16 15 38 13 25 -2 32 -8 34 -30z m168 -223 c11 -43 -34
|
||||
-73 -64 -43 -13 13 -16 50 -5 61 3 4 19 7 34 7 22 0 30 -6 35 -25z m4258 -79
|
||||
c42 -57 100 -125 128 -152 99 -89 238 -168 428 -244 190 -76 276 -126 364
|
||||
-215 92 -92 128 -197 88 -256 -34 -50 -96 -119 -108 -119 -7 0 -28 22 -47 48
|
||||
-55 77 -61 91 -48 119 16 35 -13 95 -80 162 -68 68 -126 106 -319 216 -192
|
||||
109 -304 185 -398 271 -59 54 -77 65 -97 60 -18 -5 -28 0 -44 20 -36 46 -37
|
||||
85 -1 142 17 29 37 52 44 52 8 0 48 -47 90 -104z m-4627 -91 c88 -132 97 -158
|
||||
91 -246 -7 -87 -45 -167 -112 -234 -72 -72 -133 -98 -216 -93 -75 4 -100 19
|
||||
-156 93 -19 25 -38 45 -42 45 -4 0 -33 -33 -66 -74 -83 -107 -238 -253 -292
|
||||
-278 -39 -17 -50 -18 -75 -7 -38 15 -95 81 -110 127 -37 111 96 236 358 335
|
||||
209 80 308 154 413 309 77 114 100 140 119 136 9 -2 48 -53 88 -113z m4568
|
||||
-154 c89 -67 204 -184 220 -223 14 -35 14 -39 -5 -58 -24 -24 -69 -26 -113 -4
|
||||
-40 21 -77 54 -77 70 0 13 -46 54 -61 54 -17 0 -8 -37 19 -81 64 -101 201
|
||||
-197 362 -255 167 -59 279 -144 315 -237 24 -63 11 -107 -51 -176 -79 -88
|
||||
-133 -102 -200 -53 -50 36 -133 139 -219 271 -77 117 -131 174 -257 269 -48
|
||||
36 -85 68 -82 71 6 7 132 -81 195 -138 30 -27 98 -109 149 -181 52 -73 125
|
||||
-166 163 -206 89 -96 129 -108 193 -59 43 33 39 40 -9 20 -20 -8 -45 -15 -56
|
||||
-15 -33 0 -138 107 -229 233 -96 133 -213 253 -300 308 -63 39 -127 66 -116
|
||||
47 5 -7 -1 -9 -20 -4 -22 5 -26 3 -26 -16 0 -24 26 -58 46 -58 16 0 73 -65 85
|
||||
-98 20 -52 -11 -102 -64 -102 -98 0 -220 130 -241 255 -10 58 4 87 85 174 39
|
||||
42 99 111 132 153 73 92 87 96 162 39z m-4283 -208 c64 -61 108 -124 115 -168
|
||||
7 -46 -17 -113 -76 -204 -84 -131 -140 -252 -162 -347 -61 -265 -99 -371 -154
|
||||
-429 -26 -28 -40 -35 -72 -35 -70 0 -179 86 -216 169 -38 87 5 218 110 332
|
||||
l56 60 -23 24 c-13 14 -29 25 -35 25 -6 0 -55 -47 -109 -105 -54 -58 -115
|
||||
-115 -137 -125 -81 -42 -154 -16 -185 65 -16 43 -16 47 1 92 37 96 120 161
|
||||
345 271 193 93 259 148 363 305 93 139 101 142 179 70z m3835 -224 c72 -35
|
||||
133 -128 133 -203 0 -36 -5 -48 -26 -65 -32 -25 -55 -26 -94 -6 -34 18 -45 35
|
||||
-54 90 -8 44 -40 85 -67 85 -27 0 -59 -48 -59 -89 0 -48 42 -132 86 -171 45
|
||||
-39 92 -40 189 -1 140 57 189 52 298 -30 77 -58 139 -132 176 -209 21 -46 26
|
||||
-69 25 -135 0 -68 -4 -89 -29 -138 -36 -72 -131 -164 -210 -203 -160 -78 -292
|
||||
-16 -437 205 -27 41 -47 82 -46 90 2 12 21 18 76 22 130 12 237 89 212 153
|
||||
-18 49 -49 52 -136 13 -102 -45 -134 -51 -194 -37 -69 17 -130 73 -175 162
|
||||
-62 125 -45 241 51 347 43 48 128 108 180 126 51 18 51 18 101 -6z m-3403
|
||||
-138 c50 -32 129 -80 176 -106 153 -85 210 -162 210 -285 0 -117 -76 -226
|
||||
-174 -251 l-45 -11 27 -40 c36 -56 42 -149 13 -232 -12 -33 -21 -82 -21 -110
|
||||
0 -75 -19 -177 -39 -211 -34 -58 -115 -62 -230 -10 -79 37 -121 72 -142 120
|
||||
-33 74 -6 145 96 253 65 70 115 152 115 192 0 40 -32 90 -76 120 -38 27 -64
|
||||
37 -64 26 0 -46 -46 -282 -65 -332 -71 -188 -137 -228 -272 -165 -80 38 -147
|
||||
101 -169 157 -40 105 44 246 222 375 159 114 218 207 259 405 25 121 44 164
|
||||
72 164 9 0 57 -26 107 -59z m2891 -157 c26 -10 44 -56 61 -156 18 -106 63
|
||||
-210 119 -275 20 -23 73 -69 119 -102 141 -102 212 -199 223 -305 10 -94 -39
|
||||
-155 -174 -218 -81 -38 -156 -47 -201 -24 -72 37 -123 186 -139 404 -10 126
|
||||
-12 137 -51 215 -22 45 -70 120 -105 167 -36 47 -70 102 -77 124 -11 34 -10
|
||||
41 7 65 20 27 115 84 168 100 17 5 31 10 32 10 1 1 9 -2 18 -5z m-2249 -154
|
||||
c107 -37 231 -70 384 -100 83 -16 161 -37 173 -45 45 -29 71 -93 75 -186 3
|
||||
-51 0 -101 -7 -122 -17 -50 -73 -97 -115 -97 -68 0 -136 74 -136 148 0 19 6
|
||||
35 14 38 21 8 27 31 16 60 -7 18 -16 25 -32 22 -48 -7 -99 -156 -99 -289 0
|
||||
-47 9 -103 32 -181 61 -212 35 -383 -64 -435 -41 -21 -140 -21 -226 2 -160 41
|
||||
-212 104 -198 238 9 89 44 163 133 279 105 139 119 177 119 325 0 104 -2 114
|
||||
-23 135 -23 23 -23 23 -38 4 -17 -23 -18 -58 -3 -73 14 -14 -17 -88 -49 -113
|
||||
-13 -11 -40 -20 -59 -20 -104 0 -147 101 -107 253 20 78 47 130 80 156 33 26
|
||||
56 26 130 1z m1218 -43 c14 -54 -1 -87 -44 -101 -54 -17 -62 -43 -55 -176 9
|
||||
-168 53 -559 76 -675 32 -159 70 -225 128 -225 45 0 61 -31 61 -116 0 -98 -6
|
||||
-102 -163 -119 -185 -19 -230 -5 -268 81 -43 96 -45 360 -3 544 36 161 29 358
|
||||
-22 565 -27 112 -31 202 -10 230 17 23 80 33 201 31 l90 -1 9 -38z m406 -57
|
||||
c22 -22 22 -22 24 -200 2 -100 6 -136 23 -182 61 -163 189 -247 293 -193 35
|
||||
18 41 38 38 120 l-3 60 36 3 c73 6 177 -107 220 -241 27 -84 21 -166 -17 -244
|
||||
-35 -72 -70 -98 -182 -132 -231 -72 -428 -110 -497 -97 -52 10 -100 58 -122
|
||||
119 -26 75 -23 227 5 334 41 151 24 263 -65 434 -36 70 -44 94 -41 130 4 53
|
||||
29 70 133 92 98 21 131 21 155 -3z"/>
|
||||
<path d="M4450 8721 c14 -4 54 -14 90 -20 36 -7 88 -23 116 -35 41 -19 54 -32
|
||||
72 -70 12 -25 22 -57 22 -71 0 -40 16 -29 30 19 31 111 -57 174 -255 182 -65
|
||||
3 -91 1 -75 -5z"/>
|
||||
<path d="M3914 8686 c-144 -47 -171 -81 -160 -206 3 -42 12 -96 19 -121 l13
|
||||
-44 12 108 c7 65 19 122 31 145 21 41 103 103 164 122 20 6 37 13 37 16 0 10
|
||||
-50 1 -116 -20z"/>
|
||||
<path d="M4157 8572 c-26 -28 -45 -82 -67 -191 -48 -235 -66 -471 -37 -471 8
|
||||
0 58 7 113 15 54 8 104 15 111 15 17 0 53 115 60 190 9 109 -44 368 -89 433
|
||||
-23 32 -66 37 -91 9z"/>
|
||||
<path d="M4336 8308 c32 -150 3 -361 -53 -382 -9 -4 -58 -12 -109 -18 -127
|
||||
-16 -122 -33 7 -23 l102 7 -9 -39 c-5 -21 -9 -72 -8 -114 0 -62 6 -87 28 -132
|
||||
46 -95 129 -148 228 -147 33 1 33 1 -12 17 -69 24 -107 59 -142 131 -30 60
|
||||
-33 75 -32 152 0 58 9 118 27 187 37 139 35 214 -10 358 -23 74 -33 76 -17 3z"/>
|
||||
<path d="M3566 7668 c-31 -47 -78 -184 -83 -239 -7 -68 10 -124 42 -145 32
|
||||
-21 104 -20 162 2 68 25 74 34 17 28 -66 -8 -130 10 -151 43 -27 41 -22 150
|
||||
10 248 28 83 29 102 3 63z"/>
|
||||
<path d="M5163 8646 c-17 -8 -36 -23 -42 -35 -25 -46 -9 -84 95 -216 57 -73
|
||||
51 -41 -11 60 -48 78 -52 90 -42 121 9 26 48 44 119 53 l53 7 -50 8 c-27 4
|
||||
-59 9 -70 12 -11 2 -34 -3 -52 -10z"/>
|
||||
<path d="M6211 8246 c-12 -13 -37 -71 -55 -128 -41 -130 -64 -175 -176 -342
|
||||
-88 -132 -136 -184 -182 -198 -20 -5 -19 -6 8 -7 81 -3 211 152 331 394 22 44
|
||||
46 96 53 115 21 55 59 101 98 117 31 13 46 13 128 -1 101 -17 183 -46 229 -80
|
||||
39 -29 55 -38 55 -32 0 8 -111 81 -167 110 -79 39 -200 76 -253 76 -36 0 -51
|
||||
-5 -69 -24z"/>
|
||||
<path d="M5263 7797 c-63 -150 -77 -200 -69 -250 15 -92 64 -145 196 -212 91
|
||||
-45 126 -54 68 -16 -68 43 -168 149 -191 200 -25 56 -17 148 23 293 16 57 28
|
||||
104 26 105 -1 1 -25 -53 -53 -120z"/>
|
||||
<path d="M2820 8326 c-74 -26 -126 -52 -223 -109 -77 -45 -97 -66 -40 -41 193
|
||||
83 294 114 373 114 74 0 145 -26 174 -63 l26 -32 0 35 c0 52 -32 97 -77 109
|
||||
-62 18 -161 12 -233 -13z"/>
|
||||
<path d="M2400 7949 c-100 -41 -150 -172 -150 -395 0 -121 22 -279 41 -299 7
|
||||
-7 40 20 97 80 133 138 189 270 180 430 -5 87 -27 147 -63 171 -30 19 -76 25
|
||||
-105 13z"/>
|
||||
<path d="M2562 7933 c33 -64 42 -98 46 -164 7 -135 -36 -256 -135 -379 -34
|
||||
-41 -59 -77 -57 -79 2 -2 18 8 37 22 139 106 207 240 207 410 0 87 -15 134
|
||||
-56 172 -25 24 -50 34 -42 18z"/>
|
||||
<path d="M1698 7492 c-106 -109 -113 -211 -20 -276 20 -14 74 -39 119 -56 46
|
||||
-17 99 -43 119 -56 20 -14 38 -23 40 -21 6 6 -91 98 -166 157 -36 28 -73 66
|
||||
-83 83 -26 45 -18 115 18 167 15 22 26 42 23 45 -2 2 -25 -17 -50 -43z"/>
|
||||
<path d="M2710 7365 c0 -3 15 -19 34 -36 51 -44 66 -100 66 -243 0 -102 3
|
||||
-126 19 -153 35 -57 90 -56 184 3 32 20 57 38 55 40 -2 2 -32 -4 -66 -14 -112
|
||||
-30 -146 3 -136 129 11 127 -29 224 -108 265 -28 14 -48 18 -48 9z"/>
|
||||
<path d="M2086 6674 c10 -194 22 -236 83 -287 76 -64 161 -39 279 81 99 102
|
||||
110 125 32 72 -130 -88 -213 -109 -277 -70 -41 24 -45 33 -81 169 -17 62 -33
|
||||
116 -36 118 -3 3 -3 -34 0 -83z"/>
|
||||
<path d="M6902 7869 c-19 -6 -40 -22 -51 -39 -18 -31 -57 -208 -47 -217 3 -4
|
||||
19 25 36 63 62 143 59 139 117 142 64 4 123 -24 209 -97 63 -54 93 -68 53 -25
|
||||
-32 36 -164 141 -197 158 -40 20 -83 26 -120 15z"/>
|
||||
<path d="M7470 7455 c0 -3 31 -42 70 -88 38 -45 80 -102 92 -127 28 -56 33
|
||||
-139 14 -210 -8 -30 -13 -56 -11 -58 1 -1 17 19 34 47 20 30 34 67 37 97 7 58
|
||||
-1 72 -138 237 -84 100 -98 115 -98 102z"/>
|
||||
<path d="M6560 7189 c-14 -11 -56 -36 -95 -56 -77 -40 -190 -144 -201 -187 -3
|
||||
-14 -3 -38 1 -53 8 -33 85 -108 146 -142 l43 -25 -22 30 c-105 136 -98 179 52
|
||||
334 107 111 135 147 76 99z"/>
|
||||
<path d="M6980 7095 c0 -28 6 -60 15 -71 19 -28 75 -54 114 -54 27 0 32 -4 38
|
||||
-31 7 -38 23 -19 23 29 0 31 -1 32 -43 32 -49 0 -78 23 -121 100 l-26 45 0
|
||||
-50z"/>
|
||||
<path d="M6850 6833 c0 -194 171 -375 302 -320 19 8 44 27 56 42 28 35 54 29
|
||||
69 -15 l12 -35 0 40 c1 88 -47 120 -96 64 -15 -17 -39 -35 -51 -40 -34 -13
|
||||
-96 6 -137 43 -40 34 -97 132 -122 204 -23 71 -33 76 -33 17z"/>
|
||||
<path d="M4656 6262 c-14 -28 -16 -56 -14 -177 l3 -145 32 -6 c36 -7 50 -31
|
||||
30 -51 -7 -7 -33 -19 -57 -26 -42 -11 -47 -17 -80 -81 -30 -60 -34 -79 -34
|
||||
-140 1 -44 8 -93 21 -129 22 -64 14 -89 -10 -29 -29 74 -40 153 -27 212 6 30
|
||||
14 67 17 82 7 39 -15 44 -76 17 -48 -20 -51 -24 -52 -58 0 -20 -4 -44 -8 -54
|
||||
-4 -12 7 -45 31 -95 21 -42 41 -99 44 -127 8 -63 -8 -51 -25 19 -17 68 -67
|
||||
165 -106 203 l-33 32 -39 -29 c-39 -29 -40 -32 -47 -103 -7 -81 1 -115 47
|
||||
-189 16 -27 27 -54 25 -60 -3 -7 -17 6 -32 30 -25 40 -38 68 -65 135 -12 31
|
||||
-58 54 -81 39 -8 -5 -19 -38 -25 -76 -9 -55 -9 -72 4 -98 13 -27 73 -80 104
|
||||
-91 7 -2 9 -8 5 -12 -12 -12 -98 44 -119 78 -10 18 -24 50 -30 72 l-11 40 -22
|
||||
-27 c-11 -15 -33 -49 -48 -76 l-28 -49 28 -54 c20 -40 38 -60 70 -77 38 -21
|
||||
55 -42 34 -42 -22 0 -101 64 -118 95 -22 42 -31 44 -40 8 -10 -40 41 -133 83
|
||||
-153 40 -18 43 -35 6 -25 -38 9 -50 21 -83 79 l-29 50 -11 -30 c-16 -41 -50
|
||||
-185 -50 -211 0 -23 45 -56 93 -67 15 -4 25 -11 22 -16 -6 -10 -58 3 -91 25
|
||||
-12 8 -25 11 -27 7 -8 -12 1 -95 12 -124 9 -24 16 -28 51 -28 25 0 39 -4 35
|
||||
-10 -3 -5 -19 -10 -36 -10 -17 0 -29 -5 -29 -12 0 -7 20 -44 44 -82 l43 -69 6
|
||||
119 c6 142 22 209 81 329 96 194 252 330 461 401 115 39 295 45 415 15 274
|
||||
-68 483 -268 570 -546 34 -110 39 -278 11 -398 -66 -287 -275 -508 -558 -589
|
||||
-113 -33 -306 -32 -413 1 -100 31 -211 90 -287 152 -66 54 -157 166 -192 237
|
||||
l-22 43 -30 -26 c-17 -14 -41 -28 -55 -31 -13 -3 -24 -8 -24 -9 0 -2 9 -24 20
|
||||
-49 l19 -46 37 15 c21 8 43 20 50 26 8 7 14 7 18 0 4 -6 -16 -22 -43 -36 l-50
|
||||
-25 20 -35 c24 -40 54 -45 122 -21 20 8 37 9 37 4 0 -12 -63 -38 -95 -38 -14
|
||||
0 -25 -6 -25 -12 0 -7 24 -44 53 -83 l53 -70 49 3 c35 1 65 11 99 33 27 16 51
|
||||
27 55 24 8 -8 -34 -40 -87 -66 -64 -32 -68 -49 -23 -93 37 -36 40 -37 87 -31
|
||||
70 11 81 19 96 72 7 25 20 49 27 51 10 3 12 0 6 -14 -4 -11 -13 -41 -21 -69
|
||||
-9 -35 -24 -59 -49 -82 l-37 -32 69 -45 c37 -25 73 -46 79 -46 7 0 34 17 61
|
||||
38 52 41 97 114 107 175 4 20 11 37 16 37 11 0 4 -48 -15 -105 -7 -22 -16 -60
|
||||
-19 -83 -4 -24 -15 -61 -26 -82 l-19 -38 37 -15 c36 -14 38 -14 79 15 50 36
|
||||
52 41 63 137 6 53 4 86 -5 118 -15 51 -3 67 16 21 17 -41 14 -149 -6 -234 -24
|
||||
-102 -19 -109 83 -128 47 -9 88 -14 90 -11 3 3 0 16 -6 30 -19 41 -14 85 19
|
||||
191 30 96 33 149 14 197 -4 9 -3 17 3 17 24 0 35 -70 22 -145 -12 -70 -12 -75
|
||||
10 -107 46 -66 99 -113 84 -73 -10 25 17 114 43 145 25 30 25 30 8 86 -9 32
|
||||
-27 73 -40 93 -27 39 -25 53 4 27 40 -36 63 -113 67 -225 3 -58 9 -120 14
|
||||
-137 13 -40 74 -99 125 -120 44 -18 95 -34 95 -30 0 2 -15 19 -33 38 -33 35
|
||||
-57 94 -57 139 0 14 9 57 20 97 37 135 20 239 -56 339 -18 23 -21 33 -11 33
|
||||
34 0 107 -148 107 -218 0 -21 7 -48 16 -60 18 -25 74 -56 121 -67 l33 -7 -15
|
||||
43 c-9 24 -15 77 -15 127 0 79 -2 89 -31 134 -31 48 -105 98 -144 98 -8 0 -15
|
||||
5 -15 10 0 28 105 -13 146 -57 14 -15 45 -63 69 -108 49 -89 88 -136 131 -158
|
||||
16 -8 71 -17 126 -20 53 -3 106 -10 117 -16 25 -14 26 -9 7 27 -19 36 -63 76
|
||||
-111 100 -76 38 -99 77 -134 227 -33 136 -66 177 -169 204 -55 15 -46 32 10
|
||||
18 74 -18 114 -51 145 -117 23 -49 35 -62 66 -75 39 -16 94 -19 146 -9 36 7
|
||||
40 20 10 29 -52 17 -116 97 -133 168 -9 37 -62 84 -110 98 -23 6 -63 8 -95 4
|
||||
-44 -5 -53 -4 -44 6 13 13 114 18 154 7 15 -4 62 -37 105 -73 121 -100 166
|
||||
-105 310 -34 59 29 96 41 133 42 28 1 51 4 51 7 0 9 -70 61 -100 74 -21 8 -47
|
||||
10 -87 4 -32 -5 -81 -11 -110 -14 l-52 -6 -58 55 c-32 30 -76 64 -98 75 -44
|
||||
21 -129 42 -175 42 -17 0 -30 5 -30 11 0 19 128 0 195 -28 33 -14 70 -25 82
|
||||
-25 41 0 153 99 153 135 0 3 -18 0 -40 -5 -55 -14 -95 -4 -170 42 -36 22 -75
|
||||
40 -88 40 -12 0 -46 -16 -74 -35 -29 -19 -54 -32 -57 -29 -10 10 50 54 114 85
|
||||
54 26 70 29 162 29 144 0 173 14 239 117 18 29 52 66 75 83 66 47 20 36 -92
|
||||
-22 -111 -58 -99 -57 -249 -14 -86 25 -196 21 -255 -9 -27 -13 -51 -22 -53
|
||||
-20 -11 11 24 35 71 51 29 9 61 25 73 36 12 10 63 30 115 44 52 14 104 31 116
|
||||
39 22 15 43 69 43 113 0 26 -2 26 -38 8 -15 -7 -47 -17 -72 -20 -25 -4 -67
|
||||
-13 -95 -21 -27 -8 -77 -22 -109 -31 -44 -11 -69 -25 -93 -52 -18 -20 -33 -42
|
||||
-33 -49 0 -7 -4 -13 -10 -13 -33 0 21 84 70 109 19 10 71 28 115 40 142 39
|
||||
177 74 206 208 19 88 58 184 86 215 15 17 14 18 -8 18 -80 0 -154 -53 -226
|
||||
-162 -30 -45 -60 -77 -88 -93 -38 -23 -65 -32 -186 -65 -20 -5 -57 -26 -82
|
||||
-45 -54 -41 -64 -32 -14 12 18 16 42 44 54 63 12 20 37 46 55 60 75 55 118
|
||||
102 124 137 7 35 -10 114 -24 112 -4 0 -21 -19 -37 -42 -40 -55 -108 -100
|
||||
-186 -123 -56 -17 -64 -23 -80 -59 -11 -22 -22 -57 -25 -77 -7 -39 -24 -53
|
||||
-24 -20 0 28 37 129 62 171 12 20 52 68 89 107 93 98 104 122 102 216 -1 42
|
||||
-9 100 -17 129 -18 60 -20 107 -6 143 12 33 2 33 -47 -3 -47 -34 -73 -90 -73
|
||||
-160 0 -59 -20 -130 -50 -171 -12 -17 -53 -54 -91 -81 -92 -67 -137 -122 -157
|
||||
-191 -19 -64 -38 -79 -27 -21 6 35 18 60 64 141 11 19 25 65 32 103 18 100 2
|
||||
162 -58 229 -24 27 -49 50 -53 50 -4 0 -10 -42 -12 -92 -4 -107 -23 -155 -95
|
||||
-239 -48 -56 -49 -61 -28 -139 18 -66 17 -60 5 -60 -13 0 -38 88 -51 185 -10
|
||||
78 -9 86 15 157 17 48 26 96 26 134 0 52 -5 69 -36 120 -70 112 -86 145 -98
|
||||
192 l-13 47 -17 -33z"/>
|
||||
<path d="M4060 6014 c0 -2 5 -25 10 -52 l11 -48 54 11 c29 5 56 13 59 15 2 3
|
||||
-27 21 -65 41 -38 20 -69 35 -69 33z"/>
|
||||
<path d="M5650 5960 c-19 -5 -70 -14 -112 -21 l-76 -13 1 -97 2 -97 -42 -62
|
||||
c-24 -34 -43 -72 -43 -85 0 -29 -24 -49 -50 -42 -13 3 -25 -2 -35 -16 -24 -35
|
||||
-18 -37 44 -16 72 25 143 84 175 147 l23 46 16 -25 c9 -13 21 -45 28 -71 19
|
||||
-71 2 -116 -68 -175 -113 -95 -138 -127 -82 -104 13 5 57 19 97 31 l72 21 0
|
||||
47 c0 67 34 130 90 167 27 18 93 44 160 63 183 52 224 79 237 155 7 45 -30 96
|
||||
-92 127 -44 22 -64 25 -180 27 -71 1 -146 -2 -165 -7z"/>
|
||||
<path d="M4315 5909 c-520 -53 -966 -396 -1139 -877 -73 -203 -91 -472 -47
|
||||
-681 52 -243 173 -471 345 -646 196 -199 467 -342 741 -389 119 -21 353 -21
|
||||
474 0 110 19 197 45 194 59 -1 6 -23 13 -48 17 -275 40 -649 307 -843 602 -64
|
||||
98 -132 241 -132 281 0 11 10 15 35 15 45 0 92 23 100 49 11 36 -14 98 -75
|
||||
183 -33 45 -75 112 -93 148 -31 63 -32 68 -31 185 1 86 7 142 22 197 74 275
|
||||
235 506 467 670 67 48 221 129 295 155 21 8 31 16 25 22 -15 15 -189 21 -290
|
||||
10z m194 -23 c-2 -2 -38 -22 -79 -43 -405 -209 -664 -603 -665 -1009 0 -96 2
|
||||
-104 37 -175 21 -41 64 -112 97 -158 62 -85 83 -143 62 -164 -6 -6 -42 -15
|
||||
-79 -20 -37 -4 -86 -10 -109 -13 -52 -7 -66 13 -40 58 21 36 21 48 3 48 -15 0
|
||||
-46 -58 -46 -87 0 -37 34 -56 88 -51 l49 5 18 -55 c9 -30 34 -88 55 -128 l38
|
||||
-74 -42 0 c-29 0 -65 11 -107 33 -61 31 -64 33 -61 66 5 42 -17 39 -31 -5 -5
|
||||
-16 -20 -40 -33 -55 -13 -14 -24 -28 -24 -32 0 -14 24 -7 45 13 l22 20 62 -32
|
||||
c46 -24 80 -33 127 -36 l64 -4 45 -63 c164 -231 436 -436 695 -526 l95 -33
|
||||
-55 -13 c-343 -76 -732 -3 -1030 193 -460 303 -676 860 -535 1382 142 526 610
|
||||
908 1170 955 64 6 169 7 164 3z"/>
|
||||
<path d="M3481 5234 c-50 -25 -71 -47 -57 -61 3 -3 23 6 44 20 74 52 151 48
|
||||
207 -10 19 -20 38 -33 42 -29 13 13 -22 60 -62 83 -54 30 -108 29 -174 -3z"/>
|
||||
<path d="M3630 5144 c0 -3 11 -20 25 -38 24 -32 50 -129 39 -147 -3 -5 -15 -7
|
||||
-28 -3 -27 6 -74 -11 -103 -38 l-22 -21 -31 27 c-31 25 -70 35 -70 17 0 -5 15
|
||||
-18 34 -29 38 -23 122 -120 162 -188 15 -25 30 -42 35 -37 7 7 22 80 49 243
|
||||
12 71 -8 157 -48 203 -14 16 -42 24 -42 11z m48 -222 c19 -13 4 -67 -28 -97
|
||||
l-27 -26 -31 37 c-30 36 -31 39 -14 57 28 32 74 45 100 29z"/>
|
||||
<path d="M3406 4541 c-4 -7 6 -32 23 -59 82 -128 130 -278 134 -412 3 -107 21
|
||||
-109 25 -1 4 107 -23 214 -83 336 -51 103 -88 155 -99 136z"/>
|
||||
<path d="M8240 5861 c-66 -35 -213 -81 -304 -96 -109 -18 -448 -20 -579 -4
|
||||
-49 6 -91 8 -94 6 -3 -3 8 -25 25 -49 79 -115 152 -309 177 -473 13 -80 16
|
||||
-147 12 -269 -4 -142 -9 -179 -41 -316 -21 -85 -45 -186 -53 -225 -24 -101
|
||||
-24 -476 -1 -597 30 -154 60 -248 112 -352 l51 -101 85 -3 c103 -4 224 12 298
|
||||
40 137 51 324 212 427 368 58 89 139 246 184 359 l32 82 -69 67 c-78 76 -117
|
||||
148 -127 232 -7 60 23 208 71 343 108 311 80 633 -77 892 -79 129 -73 125
|
||||
-129 96z m76 -75 c64 -64 29 -136 -47 -96 -20 11 -49 60 -49 84 0 19 27 46 47
|
||||
46 8 0 30 -15 49 -34z m-855 -115 c41 -41 49 -70 24 -91 -36 -30 -125 33 -125
|
||||
88 0 35 6 42 38 42 15 0 39 -15 63 -39z m544 -106 c99 -28 219 -124 263 -212
|
||||
72 -141 1 -334 -158 -435 -66 -42 -111 -51 -177 -38 -47 10 -65 21 -108 64
|
||||
-84 84 -122 218 -112 398 8 148 43 209 134 234 50 13 75 12 158 -11z m212
|
||||
-760 c72 -30 113 -86 113 -153 0 -49 -68 -395 -109 -557 -62 -242 -110 -332
|
||||
-220 -408 -42 -29 -53 -32 -121 -32 -64 0 -85 5 -140 32 -134 66 -238 177
|
||||
-292 312 -20 49 -23 74 -23 191 1 150 14 201 77 308 100 166 294 300 467 322
|
||||
87 11 202 4 248 -15z m271 -557 c17 -17 15 -79 -4 -106 -21 -30 -61 -29 -75 2
|
||||
-14 30 -5 70 21 96 23 23 41 25 58 8z m-898 -635 c31 -21 48 -41 61 -73 40
|
||||
-97 -89 -92 -136 6 -32 68 14 109 75 67z"/>
|
||||
<path d="M7860 5458 c-48 -32 -64 -70 -68 -159 -6 -115 42 -253 110 -316 36
|
||||
-33 98 -39 154 -13 97 42 139 92 164 193 34 134 -103 290 -275 312 -43 6 -55
|
||||
3 -85 -17z"/>
|
||||
<path d="M7980 4739 c-171 -49 -284 -126 -386 -262 -58 -79 -84 -163 -84 -278
|
||||
0 -176 81 -321 217 -389 37 -19 64 -24 119 -24 63 0 76 3 123 33 62 40 86 70
|
||||
139 176 49 100 95 248 132 425 39 190 26 245 -69 298 -48 27 -137 37 -191 21z
|
||||
m45 -203 c27 -12 36 -23 46 -58 31 -115 -67 -339 -172 -394 -79 -40 -166 3
|
||||
-194 99 -19 64 -19 81 1 147 12 39 30 69 72 113 92 96 169 125 247 93z"/>
|
||||
<path d="M4550 5820 c0 -5 5 -10 10 -10 6 0 10 5 10 10 0 6 -4 10 -10 10 -5 0
|
||||
-10 -4 -10 -10z"/>
|
||||
<path d="M4358 5736 c-23 -17 -23 -29 1 -50 17 -15 19 -15 25 -1 8 23 8 65 -1
|
||||
65 -5 0 -16 -7 -25 -14z"/>
|
||||
<path d="M7205 5720 c-9 -45 -65 -166 -98 -215 -24 -35 -25 -40 -12 -76 22
|
||||
-61 63 -254 81 -384 12 -84 17 -185 17 -340 0 -292 -33 -521 -111 -772 l-29
|
||||
-89 195 -185 c107 -101 198 -185 203 -187 5 -2 -2 20 -15 50 -56 127 -93 312
|
||||
-105 533 -11 194 -1 306 42 490 104 438 88 759 -55 1051 -21 45 -54 99 -72
|
||||
120 l-33 39 -8 -35z"/>
|
||||
<path d="M4170 5590 l-35 -29 28 -10 c43 -17 47 -14 47 29 0 22 -1 40 -2 40
|
||||
-2 0 -19 -14 -38 -30z"/>
|
||||
<path d="M4065 5470 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M4610 5384 c-232 -44 -437 -200 -541 -409 -62 -125 -82 -225 -76
|
||||
-364 15 -328 211 -584 527 -691 89 -30 275 -38 380 -16 113 23 261 101 348
|
||||
181 159 148 241 335 242 551 0 217 -70 387 -224 539 -87 86 -192 149 -310 187
|
||||
-60 19 -285 33 -346 22z m319 -45 c119 -30 223 -91 321 -189 144 -143 213
|
||||
-309 213 -510 0 -130 -18 -204 -80 -330 -88 -179 -260 -318 -460 -371 -107
|
||||
-29 -259 -29 -366 0 -204 54 -376 196 -467 386 -52 109 -70 190 -70 315 0 125
|
||||
18 206 70 315 71 148 186 263 335 335 166 80 321 95 504 49z"/>
|
||||
<path d="M4320 4910 c-89 -41 -160 -140 -160 -222 0 -34 3 -38 38 -47 20 -6
|
||||
119 -11 220 -11 104 0 182 -4 182 -9 0 -5 -7 -37 -16 -71 -30 -119 -1 -163
|
||||
113 -174 76 -7 180 7 200 27 31 31 30 128 -2 210 -7 16 6 17 191 17 236 0 250
|
||||
4 247 68 -2 57 -49 139 -101 179 -146 111 -345 39 -398 -145 -16 -53 -16 -57
|
||||
6 -110 13 -30 25 -77 28 -103 4 -44 1 -51 -27 -80 -31 -30 -34 -31 -103 -27
|
||||
-59 3 -73 8 -89 27 -25 31 -25 96 1 161 26 66 25 114 -4 175 -44 96 -118 146
|
||||
-221 152 -45 3 -71 -2 -105 -17z"/>
|
||||
<path d="M4976 4334 c-3 -9 -6 -26 -6 -39 0 -13 -10 -31 -22 -40 -57 -40 -256
|
||||
-77 -307 -58 -14 6 -28 23 -35 43 -18 55 -36 50 -29 -9 4 -38 2 -57 -11 -75
|
||||
-20 -31 -20 -46 -2 -46 8 0 20 13 27 28 12 27 14 28 98 28 90 0 187 21 254 53
|
||||
36 18 38 18 47 1 11 -20 47 -36 58 -26 3 4 -7 19 -23 36 -24 25 -27 35 -23 74
|
||||
4 33 2 46 -7 46 -7 0 -16 -7 -19 -16z"/>
|
||||
<path d="M5890 5289 c-24 -16 -49 -29 -56 -29 -6 0 -18 -8 -25 -17 -13 -15
|
||||
-11 -16 20 -10 19 4 49 18 66 32 18 13 35 22 37 20 9 -9 -11 -135 -26 -163
|
||||
-18 -35 -58 -55 -152 -77 -40 -9 -80 -23 -90 -31 -18 -15 -18 -15 6 -9 14 3
|
||||
59 15 101 25 101 25 126 38 149 75 20 33 46 176 36 201 -7 19 -14 17 -66 -17z"/>
|
||||
<path d="M5720 4771 c0 -5 33 -29 73 -51 63 -36 78 -40 119 -36 26 3 58 12 72
|
||||
21 33 22 39 6 15 -42 -24 -49 -77 -103 -124 -129 -19 -10 -35 -22 -35 -27 0
|
||||
-13 58 24 112 72 54 48 73 78 83 133 8 44 -8 58 -38 30 -68 -61 -131 -55 -252
|
||||
24 -14 9 -25 12 -25 5z"/>
|
||||
<path d="M5780 4285 c0 -3 6 -20 14 -38 19 -47 74 -95 128 -115 27 -9 48 -20
|
||||
48 -24 0 -4 -25 -15 -56 -24 -55 -16 -139 -15 -191 1 -13 4 -23 3 -23 -3 0
|
||||
-17 88 -35 151 -29 66 6 154 44 154 67 0 9 -14 17 -37 21 -60 9 -129 58 -153
|
||||
107 -17 36 -35 55 -35 37z"/>
|
||||
<path d="M5537 3858 c-9 -33 8 -129 28 -168 9 -18 14 -35 10 -38 -11 -12 -119
|
||||
18 -157 43 -29 19 -38 22 -38 10 0 -21 62 -50 149 -69 81 -19 108 -11 78 22
|
||||
-28 31 -47 89 -47 144 0 62 -14 94 -23 56z"/>
|
||||
<path d="M4165 3851 c-6 -11 9 -23 19 -14 9 9 7 23 -3 23 -6 0 -12 -4 -16 -9z"/>
|
||||
<path d="M4301 3721 c-11 -7 -10 -11 7 -20 16 -9 25 -7 44 9 l23 19 -30 1
|
||||
c-16 0 -36 -4 -44 -9z"/>
|
||||
<path d="M5135 3640 c-14 -26 -19 -52 -18 -103 l1 -67 -39 31 c-21 17 -53 51
|
||||
-71 75 -36 51 -44 46 -54 -30 l-7 -46 51 0 c45 0 59 -5 104 -40 38 -29 54 -36
|
||||
56 -26 2 8 -2 24 -7 35 -14 25 -14 64 0 78 5 5 9 37 7 69 l-3 59 -20 -35z"/>
|
||||
<path d="M4585 3605 c-29 -66 -31 -83 -11 -65 17 13 51 108 42 117 -3 3 -17
|
||||
-20 -31 -52z"/>
|
||||
<path d="M4710 3500 c-19 -15 -21 -20 -8 -20 9 0 24 7 32 16 31 30 13 33 -24
|
||||
4z"/>
|
||||
<path d="M4913 3338 c-29 -11 -53 -25 -53 -30 0 -5 10 -28 22 -51 11 -23 24
|
||||
-67 27 -97 l7 -55 31 49 c49 74 69 199 33 202 -8 1 -39 -7 -67 -18z"/>
|
||||
<path d="M4080 3287 c0 -6 11 -30 25 -53 34 -57 102 -99 149 -90 50 10 86 42
|
||||
86 77 l0 29 -63 -35 c-85 -47 -118 -47 -82 1 28 37 19 49 -50 66 -41 10 -65
|
||||
11 -65 5z"/>
|
||||
<path d="M4229 3243 c-5 -13 -9 -26 -9 -28 0 -8 37 7 59 24 l24 18 -32 5 c-28
|
||||
4 -34 1 -42 -19z"/>
|
||||
<path d="M796 5833 c-45 -62 -118 -210 -144 -293 -31 -98 -42 -172 -42 -291 0
|
||||
-129 18 -227 70 -390 54 -168 74 -267 65 -335 -10 -82 -56 -164 -131 -232
|
||||
l-64 -59 15 -44 c32 -90 135 -289 203 -390 198 -295 449 -437 729 -414 l82 7
|
||||
39 81 c52 106 97 242 119 359 13 72 17 143 17 313 0 236 3 214 -70 515 -37
|
||||
152 -38 159 -38 355 -1 224 7 284 61 439 38 110 80 198 125 264 17 24 28 46
|
||||
25 48 -3 3 -49 0 -104 -6 -54 -7 -188 -12 -298 -13 -168 0 -216 3 -301 21
|
||||
-102 22 -203 56 -274 92 -22 11 -42 20 -45 20 -2 0 -20 -21 -39 -47z m88 -29
|
||||
c9 -8 16 -22 16 -30 0 -24 -29 -73 -49 -84 -76 -40 -111 32 -47 96 38 38 57
|
||||
42 80 18z m874 -132 c3 -25 -3 -39 -26 -62 -35 -35 -76 -47 -97 -30 -25 21
|
||||
-17 50 25 92 30 30 45 39 67 36 23 -2 29 -8 31 -36z m-482 -97 c54 -16 80 -39
|
||||
110 -95 16 -29 19 -57 19 -175 0 -196 -30 -291 -118 -372 -98 -90 -233 -74
|
||||
-350 44 -80 79 -123 192 -111 288 16 133 151 264 314 307 81 21 76 21 136 3z
|
||||
m-23 -780 c103 -35 173 -81 263 -171 97 -96 155 -202 181 -329 19 -94 9 -201
|
||||
-27 -297 -88 -235 -361 -407 -523 -329 -95 46 -176 167 -226 336 -50 170 -131
|
||||
571 -131 648 0 84 60 145 162 167 69 15 224 2 301 -25z m-563 -555 c39 -39 35
|
||||
-111 -6 -127 -27 -10 -64 40 -64 86 0 59 32 79 70 41z m920 -625 c26 -31 -19
|
||||
-117 -73 -140 -65 -27 -95 16 -57 83 35 62 101 91 130 57z"/>
|
||||
<path d="M1145 5471 c-97 -24 -189 -91 -225 -166 -72 -149 54 -350 219 -347
|
||||
55 2 84 20 121 80 94 149 93 364 -2 422 -34 21 -61 23 -113 11z"/>
|
||||
<path d="M986 4736 c-86 -32 -126 -86 -126 -169 0 -120 89 -445 161 -589 44
|
||||
-88 70 -120 130 -159 45 -29 60 -33 118 -33 51 -1 77 5 115 24 62 32 147 126
|
||||
189 210 28 57 32 76 35 162 3 78 0 112 -17 167 -25 81 -91 176 -172 247 -128
|
||||
111 -333 177 -433 140z m251 -205 c64 -30 132 -98 164 -166 81 -170 -23 -348
|
||||
-169 -290 -97 39 -192 218 -192 362 0 47 4 59 29 84 25 24 38 29 78 29 26 0
|
||||
67 -9 90 -19z"/>
|
||||
<path d="M6831 3169 c-27 -43 -27 -100 -1 -130 22 -27 24 -20 10 32 -8 27 -6
|
||||
46 5 79 19 54 12 63 -14 19z"/>
|
||||
<path d="M7575 2578 c70 -52 169 -156 191 -201 19 -36 23 -56 18 -82 -5 -29
|
||||
-1 -42 20 -70 15 -19 36 -40 47 -45 20 -11 20 -11 0 29 -12 22 -21 53 -21 69
|
||||
0 62 -32 124 -103 198 -71 75 -112 107 -157 118 -24 7 -24 6 5 -16z"/>
|
||||
<path d="M2191 3077 c-26 -43 -43 -87 -32 -87 5 0 24 20 42 45 36 47 47 53 67
|
||||
33 15 -15 16 -3 2 23 -17 31 -55 24 -79 -14z"/>
|
||||
<path d="M2141 2810 c-68 -54 -126 -101 -129 -104 -11 -11 32 -46 56 -46 87 0
|
||||
232 144 232 230 0 37 -34 21 -159 -80z"/>
|
||||
<path d="M2295 2825 c-27 -56 -126 -154 -164 -161 -17 -4 -31 -11 -31 -16 0
|
||||
-13 64 -1 100 17 44 23 108 100 115 139 11 54 0 65 -20 21z"/>
|
||||
<path d="M1629 2646 c-78 -32 -125 -62 -187 -120 -81 -76 -100 -128 -68 -185
|
||||
13 -24 14 -24 21 17 15 93 134 225 248 278 53 23 68 34 50 34 -5 0 -33 -11
|
||||
-64 -24z"/>
|
||||
<path d="M6770 2840 c-36 -46 -32 -51 12 -15 35 30 40 30 98 4 l45 -20 -34 35
|
||||
c-48 49 -80 48 -121 -4z"/>
|
||||
<path d="M6898 2704 c12 -8 28 -30 36 -48 10 -23 28 -40 60 -55 25 -13 51 -21
|
||||
58 -19 7 3 -6 13 -27 24 -24 11 -46 31 -54 49 -21 46 -42 65 -70 65 l-25 0 22
|
||||
-16z"/>
|
||||
<path d="M6581 2625 c-45 -49 -50 -76 -28 -148 11 -34 74 -126 88 -127 3 0
|
||||
-13 36 -34 79 -39 78 -39 80 -27 130 6 28 21 62 32 76 35 45 13 37 -31 -10z"/>
|
||||
<path d="M2500 2650 c-24 -46 -19 -50 22 -15 23 19 34 23 48 15 35 -19 45 -9
|
||||
17 15 -41 36 -63 32 -87 -15z"/>
|
||||
<path d="M2494 2468 c-32 -40 -108 -124 -168 -186 l-110 -113 35 -35 36 -35
|
||||
51 43 c69 58 122 130 182 250 47 93 62 148 41 148 -5 0 -35 -33 -67 -72z"/>
|
||||
<path d="M2455 2253 c-35 -56 -142 -163 -163 -163 -8 0 -32 14 -54 32 -29 23
|
||||
-38 26 -30 12 5 -10 20 -28 31 -38 12 -11 21 -24 21 -30 0 -6 -29 -40 -65 -76
|
||||
-85 -86 -135 -181 -135 -257 0 -65 18 -108 66 -154 l35 -34 -30 59 c-24 45
|
||||
-31 71 -31 115 0 78 33 131 176 282 111 117 193 226 208 277 11 33 4 27 -29
|
||||
-25z"/>
|
||||
<path d="M2000 2226 c-126 -63 -217 -133 -246 -187 -31 -61 -10 -135 47 -165
|
||||
34 -17 36 -11 9 23 -25 32 -25 66 -1 119 28 61 72 105 171 174 117 80 121 87
|
||||
20 36z"/>
|
||||
<path d="M6239 2366 c-80 -59 -139 -174 -139 -270 0 -58 36 -144 78 -189 65
|
||||
-69 163 -91 256 -58 80 28 90 40 24 30 -144 -24 -273 56 -308 191 -24 89 8
|
||||
184 92 276 28 30 48 54 47 54 -2 0 -25 -16 -50 -34z"/>
|
||||
<path d="M6380 2342 c0 -4 5 -13 11 -19 6 -6 19 -35 29 -64 10 -32 27 -61 43
|
||||
-73 23 -17 30 -18 54 -6 15 7 24 14 18 15 -5 2 -22 6 -37 9 -20 5 -29 15 -38
|
||||
43 -7 21 -18 48 -26 60 -15 24 -54 49 -54 35z"/>
|
||||
<path d="M6606 1752 c-29 -33 -110 -71 -179 -84 -37 -7 -67 -17 -67 -23 0 -24
|
||||
64 -118 114 -170 133 -136 286 -141 426 -15 l45 41 -48 -31 c-143 -94 -298
|
||||
-73 -416 56 -69 76 -67 98 11 123 45 15 113 63 128 92 15 28 7 34 -14 11z"/>
|
||||
<path d="M2915 2300 c-15 -17 -20 -39 -23 -98 l-3 -77 20 35 c11 19 20 42 21
|
||||
51 0 9 8 27 18 40 19 22 19 22 68 5 87 -30 90 -29 32 13 -72 52 -106 60 -133
|
||||
31z"/>
|
||||
<path d="M3260 2146 c0 -3 18 -22 39 -41 26 -24 50 -61 70 -108 30 -70 30 -71
|
||||
31 -34 0 47 -22 95 -58 129 -32 30 -82 63 -82 54z"/>
|
||||
<path d="M3105 2099 c-35 -31 -165 -216 -165 -236 0 -15 46 -33 83 -33 63 0
|
||||
126 65 153 158 19 63 12 117 -16 132 -15 8 -26 4 -55 -21z"/>
|
||||
<path d="M3181 1949 c-30 -66 -84 -126 -121 -134 -15 -3 -47 -1 -70 5 -54 15
|
||||
-59 8 -18 -21 42 -30 127 -32 159 -3 32 29 88 154 89 197 0 21 -18 1 -39 -44z"/>
|
||||
<path d="M2586 1739 c-140 -108 -169 -147 -174 -234 -5 -76 13 -119 69 -170
|
||||
41 -36 75 -49 47 -17 -9 9 -26 37 -38 61 -19 37 -22 56 -18 112 7 89 36 137
|
||||
139 230 69 62 110 109 96 109 -2 0 -56 -41 -121 -91z"/>
|
||||
<path d="M2948 1426 c-103 -110 -131 -173 -108 -242 12 -34 86 -101 99 -88 3
|
||||
3 -1 11 -9 17 -34 26 -50 62 -50 115 0 63 20 106 84 181 95 109 83 122 -16 17z"/>
|
||||
<path d="M5835 2098 c-79 -40 -105 -65 -105 -100 0 -36 13 -62 56 -111 19 -20
|
||||
34 -42 34 -49 0 -6 2 -9 5 -6 3 3 -8 31 -25 63 -34 65 -36 84 -15 125 15 29
|
||||
82 82 127 100 21 8 21 9 3 9 -11 0 -47 -14 -80 -31z"/>
|
||||
<path d="M5978 1422 c15 -152 25 -197 54 -258 40 -87 112 -104 226 -54 62 27
|
||||
62 36 1 20 -63 -17 -121 -5 -160 34 -40 38 -69 117 -99 266 -11 57 -24 108
|
||||
-28 112 -4 4 -2 -50 6 -120z"/>
|
||||
<path d="M3591 1977 c-38 -19 -59 -56 -81 -145 -15 -59 -20 -97 -15 -131 10
|
||||
-72 20 -76 21 -7 2 80 34 155 85 201 39 35 58 40 132 36 20 -2 37 -1 37 2 0 4
|
||||
-94 40 -135 51 -11 3 -31 0 -44 -7z"/>
|
||||
<path d="M4040 1861 c8 -5 49 -16 90 -26 65 -15 81 -24 118 -62 45 -47 55 -41
|
||||
21 14 -27 44 -63 62 -152 73 -88 12 -96 12 -77 1z"/>
|
||||
<path d="M3781 1648 c0 -85 -19 -175 -47 -230 -9 -17 -45 -68 -80 -114 -77
|
||||
-101 -88 -122 -113 -211 -13 -49 -17 -82 -12 -109 10 -51 36 -101 61 -114 18
|
||||
-10 17 -7 -4 25 -35 51 -40 86 -21 151 27 95 73 179 132 245 73 83 114 170
|
||||
121 259 4 66 -11 165 -28 175 -5 3 -8 -32 -9 -77z"/>
|
||||
<path d="M4109 1701 c18 -33 13 -78 -10 -94 -19 -14 -19 -18 -8 -57 14 -47 50
|
||||
-80 86 -80 24 0 24 0 -6 25 -38 33 -46 59 -27 87 29 42 17 106 -25 129 -19 9
|
||||
-19 9 -10 -10z"/>
|
||||
<path d="M4665 1958 c-50 -27 -57 -76 -25 -163 l18 -50 1 87 c1 78 3 88 23
|
||||
102 12 9 42 16 66 16 26 0 41 4 37 10 -8 13 -95 12 -120 -2z"/>
|
||||
<path d="M4650 875 c0 -178 11 -236 52 -270 27 -23 38 -25 118 -25 102 0 128
|
||||
15 43 25 -92 10 -110 15 -128 35 -22 24 -41 107 -50 208 -10 130 -17 172 -26
|
||||
172 -5 0 -9 -65 -9 -145z"/>
|
||||
<path d="M5138 1866 c-98 -21 -119 -61 -78 -151 22 -49 46 -73 30 -30 -6 15
|
||||
-10 46 -10 70 0 52 23 73 110 100 30 9 60 18 65 20 28 9 -68 2 -117 -9z"/>
|
||||
<path d="M5717 1463 c-4 -3 -7 -33 -7 -65 0 -70 -23 -100 -90 -118 -36 -10
|
||||
-50 -9 -85 5 -24 8 -51 25 -60 35 -10 11 -23 20 -29 20 -14 0 54 -69 84 -85
|
||||
52 -27 96 -29 145 -5 55 27 68 48 75 125 l5 60 35 -3 35 -2 -25 20 c-24 19
|
||||
-70 27 -83 13z"/>
|
||||
<path d="M5147 1215 c-46 -206 2 -360 114 -371 l44 -4 -41 19 c-58 27 -80 60
|
||||
-94 143 -9 53 -10 92 -1 158 13 104 14 130 3 130 -5 0 -16 -34 -25 -75z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 34 KiB |
19
apps/wrapped/static/wrapped/favicon/1/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/favicon/1/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon/1/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
BIN
apps/wrapped/static/wrapped/fonts/1/JEMROKtrial-Regular.ttf
Normal file
BIN
apps/wrapped/static/wrapped/img/1/bg.png
Normal file
After Width: | Height: | Size: 732 KiB |
87
apps/wrapped/tables.py
Normal file
@ -0,0 +1,87 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_request
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import Wrapped
|
||||
|
||||
|
||||
class WrappedTable(tables.Table):
|
||||
"""
|
||||
List all wrapped
|
||||
"""
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover',
|
||||
'id': 'wrapped_table'
|
||||
}
|
||||
row_attrs = {
|
||||
'class': lambda record: 'bg-danger' if not record.generated else '',
|
||||
}
|
||||
model = Wrapped
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('note', 'bde', 'public', )
|
||||
|
||||
view = tables.LinkColumn(
|
||||
'wrapped:wrapped_detail',
|
||||
args=[A('pk')],
|
||||
attrs={
|
||||
'td': {'class': 'col-sm-2'},
|
||||
'a': {
|
||||
'class': 'btn btn-sm btn-primary',
|
||||
'data-turbolinks': 'false',
|
||||
}
|
||||
},
|
||||
text=_('view the wrapped'),
|
||||
accessor='pk',
|
||||
verbose_name=_('View'),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
public = tables.Column(
|
||||
accessor="pk",
|
||||
orderable=False,
|
||||
attrs={
|
||||
"td": {
|
||||
"id": lambda record: "makepublic_" + str(record.pk),
|
||||
"class": 'col-sm-1',
|
||||
"data-toggle": "tooltip",
|
||||
"title": lambda record:
|
||||
(_("Click to make this wrapped private") if record.public else
|
||||
_("Click to make this wrapped public")) if PermissionBackend.check_perm(
|
||||
get_current_request(), "wrapped.change_wrapped_public", record) else None,
|
||||
"onclick": lambda record:
|
||||
'makepublic(' + str(record.id) + ', ' + str(not record.public).lower() + ')'
|
||||
if PermissionBackend.check_perm(get_current_request(), "wrapped.change_wrapped_public",
|
||||
record) else None
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
share = tables.Column(
|
||||
verbose_name=_("Share"),
|
||||
accessor="pk",
|
||||
orderable=False,
|
||||
attrs={
|
||||
"td": {
|
||||
"class": 'col-sm-2',
|
||||
"title": _("Click to copy the link in the press paper"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def render_share(self, value, record):
|
||||
val = '<a class="btn btn-sm btn-primary" data-turbolinks="false" '
|
||||
val += 'onclick="copylink(' + str(record.id) + ')">'
|
||||
val += _('Copy link')
|
||||
val += '</a>'
|
||||
return format_html(val)
|
||||
|
||||
def render_public(self, value, record):
|
||||
val = "✔" if record.public else "✖"
|
||||
return val
|
82
apps/wrapped/templates/wrapped/1/wrapped_base.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% load static i18n pretty_money getenv %}
|
||||
{% comment %}
|
||||
Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
<!DOCTYPE html>
|
||||
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %} class="postition-relative h-100">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>
|
||||
{% block title %}{{ title }}{% endblock title %} - {{ request.site.name }}
|
||||
</title>
|
||||
<meta name="description" content="{% trans "The ENS Paris-Saclay BDE note." %}">
|
||||
|
||||
{# Favicon #}
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static "wrapped/favicon/1/apple-touch-icon.png" %}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{% static "wrapped/favicon/1/favicon-32x32.png" %}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{% static "wrapped/favicon/1/favicon-16x16.png" %}">
|
||||
<link rel="manifest" href="{% static "wrapped/favicon/1/site.webmanifest" %}">
|
||||
<link rel="mask-icon" href="{% static "wrapped/favicon/1/safari-pinned-tab.svg" %}" color="#5bbad5">
|
||||
<link rel="shorcut icon" href="{% static "wrapped/favicon/1/favicon.ico" %}">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="msapplication-config" content="{% static "wrapped/favicon/1/browserconfig.xml" %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{# Bootstrap, Font Awesome and custom CSS #}
|
||||
<link rel="stylesheet" href="{% static "bootstrap4/css/bootstrap.min.css" %}">
|
||||
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
|
||||
<link rel="stylesheet" href="{% static "wrapped/css/1/custom.css" %}">
|
||||
|
||||
{# JQuery, Bootstrap and Turbolinks JavaScript #}
|
||||
<script src="{% static "jquery/jquery.min.js" %}"></script>
|
||||
<script src="{% static "popper.js/umd/popper.min.js" %}"></script>
|
||||
<script src="{% static "bootstrap4/js/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "js/turbolinks.js" %}"></script>
|
||||
<script src="{% static "js/base.js" %}"></script>
|
||||
<script src="{% static "js/konami.js" %}"></script>
|
||||
|
||||
{# Translation in javascript files #}
|
||||
<script src="{% static "js/jsi18n/"|add:LANGUAGE_CODE|add:".js" %}"></script>
|
||||
|
||||
{# If extra ressources are needed for a form, load here #}
|
||||
{% if form.media %}
|
||||
{{ form.media }}
|
||||
{% endif %}
|
||||
|
||||
{% block extracss %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}
|
||||
<p>Default content...</p>
|
||||
{% endblock %}
|
||||
<br>
|
||||
<div class="wrap-container">
|
||||
<h2>{% trans "The NoteKfet this year it's also" %}</h2>
|
||||
<ul class="list" id="glob_top3_conso">
|
||||
<li>{{ glob_nb_transaction }} {% trans " transactions" %}</li>
|
||||
<li>{{ glob_nb_soiree }} {% trans " parties" %}</li>
|
||||
<li>{{ glob_nb_entree_pot }} {% trans " Pot entries" %}</li>
|
||||
<script>
|
||||
let liste = {{ glob_top3_conso | safe }};
|
||||
let ul = document.getElementById("glob_top3_conso");
|
||||
liste.forEach(item => {
|
||||
let li = document.createElement("li");
|
||||
li.textContent = item[1] + " " + item[0];
|
||||
ul.appendChild(li);
|
||||
});
|
||||
</script>
|
||||
<li>{{ glob_nb_vieux_con }} {% trans " old dickhead behind the bar" %} </li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
CSRF_TOKEN = "{{ csrf_token }}";
|
||||
$(".invalid-feedback").addClass("d-block");
|
||||
</script>
|
||||
|
||||
{% block extrajavascript %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
31
apps/wrapped/templates/wrapped/1/wrapped_view_club.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "wrapped/1/wrapped_base.html" %}
|
||||
{% comment %}
|
||||
COPYRIGHT (C) 2018-2024 BDE ENS Paris-Saclay
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n pretty_money %}
|
||||
{% block content %}
|
||||
<div class="wrap-container">
|
||||
<h2>{% trans "NoteKfet Wrapped" %}</h2>
|
||||
<h1 id="name">{{ wrapped.note.club.name }}</h1>
|
||||
{% trans "Your best consumer:" %}
|
||||
<div class="category" id="consumer"></div>
|
||||
{% trans "Your worst creditor:" %}
|
||||
<div class="category" id="creditor"></div>
|
||||
<ul class="list">
|
||||
<li>{{ nb_soiree_orga }} {% trans "party·ies organised" %}</li>
|
||||
<li>{{ nb_member }} {% trans "distinct members" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
let con = Boolean({{ big_consumer | safe }});
|
||||
let cre = Boolean({{ big_creancier | safe }});
|
||||
let d1 = document.getElementById("consumer");
|
||||
let d2 = document.getElementById("creditor");
|
||||
if (con) { d1.textContent = {{ big_consumer | safe }}[0] + " " + gettext("with") + " " + {{ big_consumer | safe}}[1] + "€";}
|
||||
else { d1.textContent = gettext("Infortunately, you doesn't have consumer this year");};
|
||||
if (cre) { d2.textContent = {{ big_creancier | safe}}[0] + " " + gettext("with") + " " + {{ big_creancier | safe}}[1] + "€";}
|
||||
else { d2.textContent = gettext("Congratulations you are a real rat !"); };
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
69
apps/wrapped/templates/wrapped/1/wrapped_view_user.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% extends "wrapped/1/wrapped_base.html" %}
|
||||
{% comment %}
|
||||
COPYRIGHT (C) 2018-2024 BDE ENS Paris-Saclay
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n pretty_money %}
|
||||
{% block content %}
|
||||
<div class="wrap-container">
|
||||
<h2>{% trans "NoteKfet Wrapped" %}</h2>
|
||||
<h1 id="name">{{ wrapped.note.user.username }}</h1>
|
||||
{% if wei %}
|
||||
<div class="category" id="wei">
|
||||
{% trans "You participate to the wei: " %} {{ wei }} {% trans "in the" %} {{ bus }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ranking-bar">
|
||||
<div class="ranking-progress" id="pot_bar">
|
||||
{{ nb_pot_entry }}/{{ nb_pots }} {% trans "pots !" %}
|
||||
</div>
|
||||
<script>
|
||||
const percentage = ({{ nb_pot_entry }} / {{ nb_pots }}) *100;
|
||||
document.getElementById("pot_bar").style.width = percentage + '%';
|
||||
</script>
|
||||
</div>
|
||||
{% if first_conso %}
|
||||
<ul class="list" id="user_conso">
|
||||
<li>{% trans "Your first conso of the year: " %} {{ first_conso }}</li>
|
||||
<li>{% trans "Your prefered consumtion category: " %} {{ top_category }}</li>
|
||||
<script>
|
||||
let top3 = {{ top3_conso | safe }};
|
||||
let l = document.getElementById("user_conso");
|
||||
top3.forEach(item => {
|
||||
let li = document.createElement("li");
|
||||
li.textContent = item[1] + " " + item[0];
|
||||
l.appendChild(li);
|
||||
});
|
||||
</script>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="category">
|
||||
{{ nb_rechargement }} {% trans ": it's the number of time your reload your note" %}
|
||||
</div>
|
||||
{% if class_conso_all > 0 %}
|
||||
{% trans "Your overall expenses: " %}
|
||||
<div class="ranking-bar">
|
||||
<div class="ranking-progress" id="all_bar">
|
||||
{{ class_conso_all }}/{{ class_part_all }} {% trans "with" %} {{ amount_conso_all }}€
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const p_all = 100 - (({{ class_conso_all }} - 1) / {{ class_part_all }}) * 100;
|
||||
document.getElementById("all_bar").style.width = p_all + '%';
|
||||
</script>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if class_conso_bde > 0 %}
|
||||
{% trans "Your expenses to BDE: " %}
|
||||
<div class="ranking-bar">
|
||||
<div class="ranking-progress" id="bde_bar">
|
||||
{{ class_conso_bde }}/{{ class_part_bde }} {% trans "with" %} {{ amount_conso_bde }}€
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const p_bde = 100 - (({{ class_conso_bde }} - 1) / {{ class_part_all }}) * 100;
|
||||
document.getElementById("bde_bar").style.width = p_bde + '%';
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
69
apps/wrapped/templates/wrapped/wrapped_list.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card card-border shadow">
|
||||
<div class="card-header text-center">
|
||||
<h5> {{ title }}</h5>
|
||||
</div>
|
||||
<div class="card-body px-0 py-0" id="wrapped_table">
|
||||
{% render_table table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script type="text/javascript">
|
||||
let club_not_public = {{ club_not_public }};
|
||||
if (club_not_public) { (addMsg("{% trans "Do not forget to ask permission to people who are in your wrapped before to make them public" %}", 'warning'));}
|
||||
function refreshTable() {
|
||||
$("#wrapped_table").load(location.pathname + " #wrapped_table");
|
||||
}
|
||||
|
||||
function copylink(id) {
|
||||
navigator.clipboard.writeText({{ request.get_full_path }} + id)
|
||||
.then(() => { addMsg("{% trans "Link copied" %}", 'success', 1000);});
|
||||
}
|
||||
|
||||
function makepublic(id, isprivate) {
|
||||
const makepublic_obj = $('#makepublic_'+id)
|
||||
|
||||
if (makepublic_obj.data('pending'))
|
||||
// The button is already clicked
|
||||
{ return }
|
||||
|
||||
makepublic_obj.html('<strong style="font-size: 16pt;">⟳</strong>')
|
||||
makepublic_obj.data('pending', true)
|
||||
|
||||
$.ajax({
|
||||
url: '/api/wrapped/wrapped/' + id + '/',
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-CSRFTOKEN': CSRF_TOKEN
|
||||
},
|
||||
data: {
|
||||
public: isprivate
|
||||
},
|
||||
success: function() {
|
||||
if(!isprivate)
|
||||
addMsg("{% trans "Wrapped is private" %}", 'success', 2000)
|
||||
else addMsg("{% trans "Wrapped is public" %}", 'success', 2000)
|
||||
refreshTable()
|
||||
},
|
||||
error: function (err) {
|
||||
addMsg("{% trans "An error occured" %}", 'danger')
|
||||
refreshTable()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
13
apps/wrapped/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'wrapped'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.WrappedListView.as_view(), name='wrapped_list'),
|
||||
path('<int:pk>/', views.WrappedDetailView.as_view(), name='wrapped_detail'),
|
||||
]
|
62
apps/wrapped/views.py
Normal file
@ -0,0 +1,62 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView
|
||||
from django_tables2.views import SingleTableView
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .models import Wrapped
|
||||
from .tables import WrappedTable
|
||||
|
||||
|
||||
class WrappedListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
Display all Wrapped, and classify by year
|
||||
"""
|
||||
model = Wrapped
|
||||
table_class = WrappedTable
|
||||
template_name = 'wrapped/wrapped_list.html'
|
||||
extra_context = {'title': _("List of wrapped")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).distinct()
|
||||
|
||||
def get_table_data(self):
|
||||
return Wrapped.objects.filter(PermissionBackend.filter_queryset(
|
||||
self.request, Wrapped, "change", field='public')).distinct().order_by("-bde__date_start")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
w = self.object_list.filter(note__noteclub__club__pk__gte=-1, public=False)
|
||||
if w:
|
||||
context['club_not_public'] = 'true'
|
||||
else:
|
||||
context['club_not_public'] = 'false'
|
||||
return context
|
||||
|
||||
|
||||
class WrappedDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View a wrapped
|
||||
"""
|
||||
model = Wrapped
|
||||
template_name = 'wrapped/0/wrapped_view.html' # by default
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
bde_id = Wrapped.objects.get(pk=kwargs['pk']).bde.id
|
||||
note_type = 'user' if 'user' in Wrapped.objects.get(pk=kwargs['pk']).note.__dir__() else 'club'
|
||||
self.template_name = 'wrapped/' + str(bde_id) + '/wrapped_view_' + note_type + '.html'
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
d = json.loads(self.object.data_json)
|
||||
for key in d:
|
||||
context[key] = d[key]
|
||||
context['title'] = str(self.object)
|
||||
return context
|
83
docs/apps/food.rst
Normal file
@ -0,0 +1,83 @@
|
||||
Application Food
|
||||
================
|
||||
|
||||
L'application ``food`` s'occupe de la traçabilité et permet notamment l'obtention de la liste des allergènes.
|
||||
|
||||
Modèles
|
||||
-------
|
||||
|
||||
L'application comporte 5 modèles : Allergen, QRCode, Food, BasicFood, TransformedFood.
|
||||
|
||||
Food
|
||||
~~~~
|
||||
|
||||
Ce modèle est un PolymorphicModel et ne sert uniquement à créer BasicFood et TransformedFood.
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Nom du produit
|
||||
* Propriétaire (doit-être un Club)
|
||||
* Allergènes (ManyToManyField)
|
||||
* date d'expiration
|
||||
* a été mangé (booléen)
|
||||
* est prêt (booléen)
|
||||
|
||||
BasicFood
|
||||
~~~~~~~~~
|
||||
|
||||
Les BasicFood correspondent aux produits non modifiés à la Kfet. Ils peuvent correspondre à la fois à des produits achetés en magasin ou à des produits Terre à Terre. Ces produits seront les ingrédients de tous les plats préparés et en conséquent sont les seuls produits à nécessité une saisie manuelle des allergènes.
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Type de date (DLC = date limite de consommation, DDM = date de durabilité minimale)
|
||||
* Date d'arrivée
|
||||
* Champs de Food
|
||||
|
||||
TransformedFood
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuvent être composés de BasicFood et/ou de TransformedFood. La date d'expiration et les allergènes sont automatiquement mis à jour par update (qui doit être exécuté après modification des ingrédients dans les forms par exemple).
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Durée de consommation (par défaut 3 jours)
|
||||
* Ingrédients (ManyToManyField vers Food)
|
||||
* Date de création
|
||||
* Champs de Food
|
||||
|
||||
Allergen
|
||||
~~~~~~~~
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Nom
|
||||
|
||||
QRCode
|
||||
~~~~~~
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* nombre (unique, entier positif)
|
||||
* food (OneToOneField vers Food)
|
||||
|
||||
Création de BasicFood
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Un BasicFood a toujours besoin d'un QRCode (depuis l'interface web). Il convient donc de coller le QRCode puis de le scanner et de compléter le formulaire.
|
||||
|
||||
Création de TransformedFood
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pour créer un TransformedFood, il suffit d'aller dans l'onglet ``traçabilité`` et de cliquer sur l'onglet.
|
||||
|
||||
Ajouter un ingrédient
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Un ingrédient a forcément un QRCode. Il convient donc de scanner le QRCode de l'ingrédient et de sélectionner le produit auquel il doit être ajouté.
|
||||
|
||||
Remarque : Un produit fini doit avoir un QRCode et inversement.
|
||||
|
||||
Terminer un plat
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Il suffit de coller le QRCode sur le plat, de le scanner et de sélectionner le produit.
|
@ -32,7 +32,7 @@ Applications indispensables
|
||||
* `Note <note>`_ :
|
||||
Les notes associées à des utilisateur⋅rices ou des clubs.
|
||||
* `Activity <activity>`_ :
|
||||
La gestion des activités (créations, gestion, entrées,…)
|
||||
La gestion des activités (créations, gestion, entrées, ...)
|
||||
* `Permission <permission>`_ :
|
||||
Backend de droits, limites les pouvoirs des utilisateur⋅rices
|
||||
* `API <../api>`_ :
|
||||
@ -64,9 +64,9 @@ Applications facultatives
|
||||
* ``cas-server``
|
||||
Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client.
|
||||
* `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_
|
||||
Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc…
|
||||
Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc...
|
||||
* `Treasury <treasury>`_ :
|
||||
Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques ...
|
||||
Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques...
|
||||
* `WEI <wei>`_ :
|
||||
Interface de gestion du WEI.
|
||||
|
||||
|