1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-06 23:44:01 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
466cbd9878 Replace Font Awesome with inline SVG icons
Font Awesome 4 adds 106kB of dependencies on each page and require to
query multiple assets. It also sometimes causes icons to appear after
page loading. Font Awesome 4 is deprecated and replaced by version 5
which is not packaged in every GNU/Linux distributions.

This commit replaces icons with inline SVG which does not require
external assets, does not require an additionnal dependency and is
widely supported by modern browsers. It makes the page loading faster
and enables us to no longer require fonts-font-awesome Debian package.
2021-10-06 17:15:33 +02:00
251 changed files with 2939 additions and 8891 deletions

1
.gitignore vendored
View File

@ -42,7 +42,6 @@ map.json
backups/
/static/
/media/
/tmp/
# Virtualenv
env/

View File

@ -8,19 +8,19 @@ 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
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:
@ -56,7 +56,7 @@ py39-django22:
linters:
stage: quality-assurance
image: debian:bullseye
image: debian:buster-backports
before_script:
- apt-get update && apt-get install -y tox
script: tox -e linters

View File

@ -12,7 +12,7 @@ RUN apt-get update && \
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \
texlive-xetex gettext libjs-bootstrap4 && \
rm -rf /var/lib/apt/lists/*
# Instal PyPI requirements

View File

@ -1,8 +1,8 @@
# NoteKfet 2020
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/main/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/main)
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/main/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/main)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
## Table des matières
@ -23,7 +23,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
$ sudo apt update
$ sudo apt install --no-install-recommends -y \
ipython3 python3-setuptools python3-venv python3-dev \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
texlive-xetex gettext libjs-bootstrap4 git
```
2. **Clonage du dépot** là où vous voulez :
@ -115,7 +115,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools python3-docutils \
memcached uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
texlive-xetex gettext libjs-bootstrap4 \
nginx python3-venv git acl
```

View File

@ -7,7 +7,7 @@
prompt: "Password of the database (leave it blank to skip database init)"
private: yes
vars:
mirror: eclats.crans.org
mirror: mirror.crans.org
roles:
- 1-apt-basic
- 2-nk20

View File

@ -1,7 +1,7 @@
---
note:
server_name: note.crans.org
git_branch: main
git_branch: master
serve_static: true
cron_enabled: true
email: notekfet2020@lists.crans.org

View File

@ -1,15 +1,14 @@
---
- name: Add buster-backports to apt sources if needed
- name: Add buster-backports to apt sources
apt_repository:
repo: deb http://{{ mirror }}/debian buster-backports main
state: present
when:
- ansible_distribution == "Debian"
- ansible_distribution_major_version | int == 10
when: ansible_facts['distribution'] == "Debian"
- name: Install note_kfet APT dependencies
apt:
update_cache: true
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
install_recommends: false
name:
# Common tools
@ -18,7 +17,6 @@
- ipython3
# Front-end dependencies
- fonts-font-awesome
- libjs-bootstrap4
# Python dependencies

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -6,7 +6,7 @@
"name": "Pot",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 1000
"guest_entry_fee": 500
}
},
{
@ -28,25 +28,5 @@
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 5,
"fields": {
"name": "Soir\u00e9e avec entrées",
"manage_entries": true,
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 7,
"fields": {
"name": "Soir\u00e9e avec invitations",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 0
}
}
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-03-23 13:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('activity', '0002_auto_20200904_2341'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='description',
field=models.TextField(blank=True, default='', verbose_name='description'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os
@ -66,8 +66,6 @@ class Activity(models.Model):
description = models.TextField(
verbose_name=_('description'),
blank=True,
default="",
)
location = models.CharField(
@ -125,14 +123,6 @@ class Activity(models.Model):
verbose_name=_('open'),
)
class Meta:
verbose_name = _("activity")
verbose_name_plural = _("activities")
unique_together = ("name", "date_start", "date_end",)
def __str__(self):
return self.name
@transaction.atomic
def save(self, *args, **kwargs):
"""
@ -154,6 +144,14 @@ class Activity(models.Model):
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
return ret
def __str__(self):
return self.name
class Meta:
verbose_name = _("activity")
verbose_name_plural = _("activities")
unique_together = ("name", "date_start", "date_end",)
class Entry(models.Model):
"""
@ -254,13 +252,14 @@ class Guest(models.Model):
verbose_name=_("inviter"),
)
class Meta:
verbose_name = _("guest")
verbose_name_plural = _("guests")
unique_together = ("activity", "last_name", "first_name", )
def __str__(self):
return self.first_name + " " + self.last_name
@property
def has_entry(self):
try:
if self.entry:
return True
return False
except AttributeError:
return False
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
@ -291,14 +290,13 @@ class Guest(models.Model):
return super().save(force_insert, force_update, using, update_fields)
@property
def has_entry(self):
try:
if self.entry:
return True
return False
except AttributeError:
return False
def __str__(self):
return self.first_name + " " + self.last_name
class Meta:
verbose_name = _("guest")
verbose_name_plural = _("guests")
unique_together = ("activity", "last_name", "first_name", )
class GuestTransaction(Transaction):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone

View File

@ -17,27 +17,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
var date_end = document.getElementById("id_date_end");
var date_start = document.getElementById("id_date_start");
function update_date_end (){
if(date_end.value=="" || date_end.value<date_start.value){
date_end.value = date_start.value;
};
};
function update_date_start (){
if(date_start.value=="" || date_end.value<date_start.value){
date_start.value = date_end.value;
};
};
date_start.addEventListener('focusout', update_date_end);
date_end.addEventListener('focusout', update_date_start);
</script>
{% endblock %}
{% endblock %}

View File

@ -34,7 +34,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
<div class="card-footer">
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
<i class="fa fa-calendar-plus-o" aria-hidden="true"></i>
<svg class="bi bi-calendar-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM8.5 8.5V10H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V11H6a.5.5 0 0 1 0-1h1.5V8.5a.5.5 0 0 1 1 0z"/>
</svg>
{% trans 'New activity' %}
</a>
</div>
@ -46,4 +48,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
</h3>
{% render_table table %}
</div>
{% endblock %}
{% endblock %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5
@ -17,8 +17,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin
from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -58,40 +57,26 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
"""
model = Activity
tables = [ActivityTable, ActivityTable]
table_class = ActivityTable
ordering = ('-date_start',)
extra_context = {"title": _("Activities")}
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 = "upcoming-"
return tables
def get_tables_data(self):
# first table = all activities, second table = upcoming
return [
self.get_queryset().order_by("-date_start"),
Activity.objects.filter(date_end__gt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
.distinct()
.order_by("date_start")
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables):
context[name] = table
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
prefix='upcoming-',
)
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities
@ -183,9 +168,6 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
it is closed or doesn't manage entries.
"""
if not self.request.user.is_authenticated:
return self.handle_no_permission()
activity = Activity.objects.get(pk=self.kwargs["pk"])
sample_entry = Entry(activity=activity, note=self.request.user.note)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,5 +0,0 @@
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
@ -7,11 +7,8 @@ from django.contrib.auth.models import User
from django.utils import timezone
from rest_framework import serializers
from member.api.serializers import ProfileSerializer, MembershipSerializer
from member.models import Membership
from note.api.serializers import NoteSerializer
from note.models import Alias
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
class UserSerializer(serializers.ModelSerializer):
@ -48,30 +45,18 @@ class OAuthSerializer(serializers.ModelSerializer):
"""
normalized_name = serializers.SerializerMethodField()
profile = serializers.SerializerMethodField()
profile = ProfileSerializer()
note = serializers.SerializerMethodField()
note = NoteSerializer()
memberships = serializers.SerializerMethodField()
def get_normalized_name(self, obj):
return Alias.normalize(obj.username)
def get_profile(self, obj):
# Display the profile of the user only if we have rights to see it.
return ProfileSerializer().to_representation(obj.profile) \
if PermissionBackend.check_perm(get_current_request(), 'member.view_profile', obj.profile) else None
def get_note(self, obj):
# Display the note of the user only if we have rights to see it.
return NoteSerializer().to_representation(obj.note) \
if PermissionBackend.check_perm(get_current_request(), 'note.view_note', obj.note) else None
def get_memberships(self, obj):
# Display only memberships that we are allowed to see.
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
class Meta:
model = User

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
@ -14,33 +14,29 @@ router = routers.DefaultRouter()
router.register('models', ContentTypeViewSet)
router.register('user', UserViewSet)
if "activity" in settings.INSTALLED_APPS:
from activity.api.urls import register_activity_urls
register_activity_urls(router, 'activity')
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 "member" 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 "treasury" in settings.INSTALLED_APPS:
from treasury.api.urls import register_treasury_urls
register_treasury_urls(router, 'treasury')
if "logs" in settings.INSTALLED_APPS:
from logs.api.urls import register_logs_urls
register_logs_urls(router, 'logs')
if "wei" in settings.INSTALLED_APPS:
from wei.api.urls import register_wei_urls

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType

View File

View File

@ -1,37 +0,0 @@
# 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

View File

@ -1,50 +0,0 @@
# 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__'

View File

@ -1,14 +0,0 @@
# 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)

View File

@ -1,61 +0,0 @@
# 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', ]

View File

@ -1,11 +0,0 @@
# 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')

View File

@ -1,111 +0,0 @@
# 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 note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import BasicFood, QRCode, TransformedFood, Food
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(is_ready=False, is_active=True, was_eaten=False)
class Meta:
model = TransformedFood
fields = ('ingredient',)
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")})
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_ready=False)
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(),
}
class FoodForms(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['was_eaten'].initial = True
class Meta:
model = Food
fields = ('was_eaten',)

View File

@ -1,84 +0,0 @@
# 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',),
),
]

View File

@ -1,19 +0,0 @@
# 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'),
),
]

View File

@ -1,62 +0,0 @@
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),
]

View File

@ -1,28 +0,0 @@
# 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'),
),
]

View File

@ -1,222 +0,0 @@
# 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 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')

View File

@ -1,19 +0,0 @@
# 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")

View File

@ -1,20 +0,0 @@
{% 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 %}

View File

@ -1,27 +0,0 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>{% trans 'Name' %} : {{ food.name }}</p>
<p>{% trans 'Owner' %} : {{ food.owner }}</p>
<p>{% trans 'Arrival date' %} : {{ food.arrival_date }}</p>
<p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p>
<p>{% trans 'Allergens' %} :</p>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<a href="{% url "food:basic_update" pk=food.pk %}">{% trans 'Update' %}</a>
</div>
</div>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% 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 %}

View File

@ -1,21 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-block">
<a href="{% url "food:basic_create" %}" class="btn btn-sm btn-outline-primary">Basic</a>
<a href="{% url "food:transformed_create" %}" class="btn btn-sm btn-outline-primary">Transformed</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body" id="form">
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}" data-turbolinks="false">
New basic food
</a>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,34 +0,0 @@
{% 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">
HTML finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>{% trans 'QR-code number' %} : {{ qrcode.qr_code_number }}</p>
<p>{% trans 'Name' %} : {{ qrcode.food_container.name }}</p>
<p>{% trans 'Owner' %} : {{ qrcode.food_container.owner }}</p>
<p>{% trans 'Expiry date' %} : {{ qrcode.food_container.expiry_date }}</p>
{% 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_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">
{% trans 'Add the ingredient' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% 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">
HTML not finished <br>
{{ 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 %}

View File

@ -1,33 +0,0 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>{% trans 'Name' %} : {{ food.name }}</p>
<p>{% trans 'Owner' %} : {{ food.owner }}</p>
<p>{% trans 'Creation date' %} : {{ food.creation_date }}</p>
<p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p>
<p>{% trans 'Allergens' %} :</p>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<p>{% trans 'Ingredients' %} :</p>
<ul>
{% for ingredient in food.ingredient.iterator %}
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
{% endfor %}
</ul>
<a href="{% url "food:transformed_update" pk=food.pk %}">{% trans 'Update' %}</a>
</div>
</div>
{% endblock %}

View File

@ -1,60 +0,0 @@
{% 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 %}

View File

@ -1,3 +0,0 @@
# from django.test import TestCase
# Create your tests here.

View File

@ -1,21 +0,0 @@
# 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'),
]

View File

@ -1,383 +0,0 @@
# 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, TemplateView
from django.views.generic.list import ListView
from django.forms import HiddenInput
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from member.models import Club
from note_kfet.middlewares import get_current_request
from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms, FoodForms
from .models import BasicFood, Food, QRCode, TransformedFood
from .tables import TransformedFoodTable
class AddIngredientView(ProtectQuerysetMixin, UpdateView):
"""
A view to add an ingredient
"""
# TO DO : ajouter un champ fully_used dans le form et changer was_eaten en conséquence + mieux filtrer les plat dispo avec des perms
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)
food_form = FoodForms(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)
# 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()
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": _("Add a new 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)
form = context['form']
# TO DO : Add perms here
if 1==0:
form.fields['is_active'].widget = HiddenInput()
if 1==0:
form.fields['was_eaten'].widget = HiddenInput()
form.fields['is_active'].help_text = _("Uncheck if the food doesn't exist anymore")
form.fields['was_eaten'].help_text = _("Check if the food has been entirely eaten")
return context
class FoodCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView):
"""
A view to add a new aliment
"""
template_name = 'food/create_food_form.html'
extra_context = {"title": _("Add a new aliment")}
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to see a food
"""
model = Food
extra_context = {"title": _("Details")}
context_object_name = "food"
class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
#####################################################################
# TO DO
# - 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):
return BasicFood(
name="",
expiry_date=timezone.now(),
)
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()
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))
elif not TransformedFood.objects.filter(is_ready=False, was_eaten=False, is_active=True).count() > 0:
return HttpResponseRedirect(reverse("food:qrcode_basic_create", 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"]
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.is_ready = True
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"],
)
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)
# TO DO : Add perms here
context["can_update_basic"]=True
context["can_update_transformed"]=True
context["can_add_ingredient"] = True
return context
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add a tranformed food
"""
# TO DO : fix the "NotImplementedError" (╯°□°)╯︵ ┻━┻ ...
model = TransformedFood
template_name = 'food/transformed_food_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_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()
# Field shelf life is only display for authorized user
# TO DO : Add permission here
if not True:
form.fields['shelf_life'].widget = HiddenInput()
return context
class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to update transformed product
"""
model = TransformedFood
template_name = 'food/transformed_food_form.html'
form_class = TransformedFoodForms
extra_context = {'title' : _('Update 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)
form = context['form']
fields = ['is_active','is_ready','was_eaten','shelf_life']
# TO DO : Add permissions here
permissions = [True]*len(fields)
for i in range(len(fields)):
if not permissions[i] : form[fields[i]].widget = HiddenInput()
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)
# context["can_create_meal"] = PermissionBackend.check_perm(self.request, "food.add_transformedfood", TransformedFood(creation_date = timezone.now(), name = "", expiry_date = timezone.now(), owner = )) <- défi prendre un club qui fonctionne (s'il existe) pour l'utilisateur
context["can_create_meal"] = True
tables = context["tables"]
for name, table in zip(["table", "open", "served"], tables):
context[name] = table
return context

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'logs.apps.LogsConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ChangelogViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
@ -76,6 +76,9 @@ class Changelog(models.Model):
verbose_name=_('timestamp'),
)
def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed."))
class Meta:
verbose_name = _("changelog")
verbose_name_plural = _("changelogs")
@ -83,6 +86,3 @@ class Changelog(models.Model):
def __str__(self):
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed."))

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import io
@ -47,13 +47,6 @@ class ProfileForm(forms.ModelForm):
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
VSS_charter_read = forms.BooleanField(
required=True,
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
help_text=_("Tick after having read and accepted the anti-VSS charter \
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
)
def clean_promotion(self):
promotion = self.cleaned_data["promotion"]
if promotion > timezone.now().year:
@ -121,7 +114,7 @@ class ImageForm(forms.Form):
frame = frame.crop((x, y, x + w, y + h))
frame = frame.resize(
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
Image.LANCZOS,
Image.ANTIALIAS,
)
frames.append(frame)
@ -138,9 +131,6 @@ class ImageForm(forms.Form):
return cleaned_data
def is_valid(self):
return super().is_valid() or super().clean().get('image') is None
class ClubForm(forms.ModelForm):
def clean(self):
@ -154,7 +144,7 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
exclude = ("add_registration_form",)
fields = '__all__'
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import hashlib

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.24 on 2021-10-05 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0007_auto_20210313_1235'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='department',
field=models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ("A''2", "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.26 on 2022-09-04 21:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0008_auto_20211005_1544'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2023-08-23 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0009_auto_20220904_2325'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2023-08-31 09:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0010_new_default_year'),
]
operations = [
migrations.AddField(
model_name='profile',
name='VSS_charter_read',
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-07-15 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0011_profile_vss_charter_read'),
]
operations = [
migrations.AddField(
model_name='club',
name='add_registration_form',
field=models.BooleanField(default=False, verbose_name='add to registration form'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
@ -28,6 +28,7 @@ class Profile(models.Model):
We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
@ -133,22 +134,6 @@ class Profile(models.Model):
default=False,
)
VSS_charter_read = models.BooleanField(
verbose_name=_("VSS charter read"),
default=False
)
class Meta:
verbose_name = _('user profile')
verbose_name_plural = _('user profile')
indexes = [models.Index(fields=['user'])]
def __str__(self):
return str(self.user)
def get_absolute_url(self):
return reverse('member:user_detail', args=(self.user_id,))
@property
def ens_year(self):
"""
@ -173,6 +158,17 @@ class Profile(models.Model):
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
return False
class Meta:
verbose_name = _('user profile')
verbose_name_plural = _('user profile')
indexes = [models.Index(fields=['user'])]
def get_absolute_url(self):
return reverse('member:user_detail', args=(self.user_id,))
def __str__(self):
return str(self.user)
def send_email_validation_link(self):
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
token = email_validation_token.make_token(self.user)
@ -204,11 +200,9 @@ class Club(models.Model):
max_length=255,
unique=True,
)
email = models.EmailField(
verbose_name=_('email'),
)
parent_club = models.ForeignKey(
'self',
null=True,
@ -259,17 +253,23 @@ class Club(models.Model):
help_text=_('Maximal date of a membership, after which members must renew it.'),
)
add_registration_form = models.BooleanField(
verbose_name=_("add to registration form"),
default=False,
)
def update_membership_dates(self):
"""
This function is called each time the club detail view is displayed.
Update the year of the membership dates.
"""
if not self.membership_start:
return
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
today = datetime.date.today()
def __str__(self):
return self.name
if (today - self.membership_start).days >= 365:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True)
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None,
@ -282,29 +282,16 @@ class Club(models.Model):
self.membership_end = None
super().save(force_insert, force_update, update_fields)
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk,))
def update_membership_dates(self):
"""
This function is called each time the club detail view is displayed.
Update the year of the membership dates.
"""
if not self.membership_start or not self.membership_end:
return
today = datetime.date.today()
while (today - self.membership_start).days >= 365:
if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
if self.membership_end:
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True)
class Membership(models.Model):
"""
@ -344,66 +331,6 @@ class Membership(models.Model):
verbose_name=_('fee'),
)
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
indexes = [models.Index(fields=['user'])]
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
@transaction.atomic
def save(self, *args, **kwargs):
"""
Calculate fee and end date before saving the membership and creating the transaction if needed.
"""
# Ensure that club membership dates are valid
old_membership_start = self.club.membership_start
self.club.update_membership_dates()
if self.club.membership_start != old_membership_start:
self.club.save()
created = not self.pk
if not created:
for role in self.roles.all():
club = role.for_club
if club is not None:
if club.pk != self.club_id:
raise ValidationError(_('The role {role} does not apply to the club {club}.')
.format(role=role.name, club=club.name))
else:
if Membership.objects.filter(
user=self.user,
club=self.club,
date_start__lte=self.date_start,
date_end__gte=self.date_start,
).exists():
raise ValidationError(_('User is already a member of the club'))
if self.club.parent_club is not None:
# Check that the user is already a member of the parent club if the membership is created
if not Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
date_start__gte=self.club.parent_club.membership_start,
).exists():
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
self.renew_parent()
else:
raise ValidationError(_('User is not a member of the parent club')
+ ' ' + self.club.parent_club.name)
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
self.date_end = self.club.membership_end
super().save(*args, **kwargs)
self.make_transaction()
@property
def valid(self):
"""
@ -481,6 +408,58 @@ class Membership(models.Model):
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
@transaction.atomic
def save(self, *args, **kwargs):
"""
Calculate fee and end date before saving the membership and creating the transaction if needed.
"""
# Ensure that club membership dates are valid
old_membership_start = self.club.membership_start
self.club.update_membership_dates()
if self.club.membership_start != old_membership_start:
self.club.save()
created = not self.pk
if not created:
for role in self.roles.all():
club = role.for_club
if club is not None:
if club.pk != self.club_id:
raise ValidationError(_('The role {role} does not apply to the club {club}.')
.format(role=role.name, club=club.name))
else:
if Membership.objects.filter(
user=self.user,
club=self.club,
date_start__lte=self.date_start,
date_end__gte=self.date_start,
).exists():
raise ValidationError(_('User is already a member of the club'))
if self.club.parent_club is not None:
# Check that the user is already a member of the parent club if the membership is created
if not Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
date_start__gte=self.club.parent_club.membership_start,
).exists():
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
self.renew_parent()
else:
raise ValidationError(_('User is not a member of the parent club')
+ ' ' + self.club.parent_club.name)
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
self.date_end = self.club.membership_end
super().save(*args, **kwargs)
self.make_transaction()
def make_transaction(self):
"""
Create Membership transaction associated to this membership.
@ -518,3 +497,11 @@ class Membership(models.Model):
soge_credit.save()
else:
transaction.save(force_insert=True)
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
indexes = [models.Index(fields=['user'])]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,64 +0,0 @@
/**
* On form submit, create a new friendship
*/
function form_create_trust (e) {
// Do not submit HTML form
e.preventDefault()
// Get data and send to API
const formData = new FormData(e.target)
$.getJSON('/api/note/alias/'+formData.get('trusted') + '/',
function (trusted_alias) {
if ((trusted_alias.note == formData.get('trusting')))
{
addMsg(gettext("You can't add yourself as a friend"), "danger")
return
}
create_trust(formData.get('trusting'), trusted_alias.note)
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* Create a trust between users
* @param trusting:Integer trusting note id
* @param trusted:Integer trusted note id
*/
function create_trust(trusting, trusted) {
$.post('/api/note/trust/', {
trusting: trusting,
trusted: trusted,
csrfmiddlewaretoken: CSRF_TOKEN
}).done(function () {
// Reload tables
$('#trust_table').load(location.pathname + ' #trust_table')
$('#trusted_table').load(location.pathname + ' #trusted_table')
addMsg(gettext('Friendship successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* On click of "delete", delete the trust
* @param button_id:Integer Trust id to remove
*/
function delete_button (button_id) {
$.ajax({
url: '/api/note/trust/' + button_id + '/',
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg(gettext('Friendship successfully deleted'), 'success')
$('#trust_table').load(location.pathname + ' #trust_table')
$('#trusted_table').load(location.pathname + ' #trusted_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
$(document).ready(function () {
// Attach event
document.getElementById('form_trust').addEventListener('submit', form_create_trust)
})

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
@ -120,7 +120,7 @@ class MembershipTable(tables.Table):
club=record.club,
user=record.user,
date_start__gte=record.club.membership_start,
date_end__lte=record.club.membership_end or date(9999, 12, 31),
date_end__lte=record.club.membership_end,
).exists(): # If the renew is not yet performed
empty_membership = Membership(
club=record.club,

View File

@ -45,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-footer">
{% if user_object %}
<a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}">
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Update Profile' %}
</a>
{% url 'member:user_detail' user_object.pk as user_profile_url %}
{% if request.path_info != user_profile_url %}
@ -59,7 +62,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if ".change_"|has_perm:club %}
<a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}"
data-turbolinks="false">
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Update Profile' %}
</a>
{% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %}

View File

@ -10,7 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold">
<i class="fa fa-users"></i> {% trans "Club managers" %}
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
{% trans "Club managers" %}
</a>
</div>
{% render_table managers %}
@ -23,7 +26,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Club members" %}
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>
{% trans "Club members" %}
</a>
</div>
{% render_table member_list %}
@ -37,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
</svg>
{% trans "Transaction history" %}
</a>
</div>
<div id="history_list">

View File

@ -47,7 +47,9 @@
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
<i class="fa fa-edit"></i>
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
</a>
</dd>

View File

@ -11,7 +11,9 @@
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'password_change' %}">
<i class="fa fa-lock"></i>
<svg class="bi bi-lock" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
{% trans 'Change password' %}
</a>
</dd>
@ -20,19 +22,13 @@
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
<i class="fa fa-edit"></i>
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
</a>
</dd>
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
<i class="fa fa-edit"></i>
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
</a>
</dd>
{% if "member.view_profile"|has_perm:user_object.profile %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
@ -60,7 +56,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' %}
<svg class="bi bi-cogs" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>
{% trans 'API token' %}
</a>
</div>
{% endif %}

View File

@ -14,9 +14,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<form method="post" enctype="multipart/form-data" id="formUpload">
{% csrf_token %}
{{ form |crispy }}
{% if user.note.display_image != "pic/default.png" %}
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
{% endif %}
</form>
</div>
<!-- MODAL TO CROP THE IMAGE -->

View File

@ -18,7 +18,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card bg-light mb-3">
<div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold">
<i class="fa fa-users"></i> {% trans "View my memberships" %}
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
{% trans "View my memberships" %}
</a>
</div>
{% render_table club_list %}
@ -29,7 +32,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="stretched-link font-weight-bold text-decoration-none"
{% if "note.view_note"|has_perm:user_object.note %}
href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
</svg>
{% trans "Transaction history" %}
</a>
</div>
<div id="history_list">

View File

@ -1,48 +0,0 @@
{% extends "member/base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load static django_tables2 i18n %}
{% block profile_content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Add friends" %}
</h3>
<div class="card-body">
{% if can_create %}
<form class="input-group" method="POST" id="form_trust">
{% csrf_token %}
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
{%include "autocomplete_model.html" %}
<div class="input-group-append">
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
</div>
</form>
{% endif %}
</div>
{% render_table trusting %}
</div>
<div class="alert alert-warning card mb-3">
{% blocktrans trimmed %}
Adding someone as a friend enables them to initiate transactions coming
from your account (while keeping your balance positive). This is
designed to simplify using note kfet transfers to transfer money between
users. The intent is that one person can make all transfers for a group of
friends without needing additional rights among them.
{% endblocktrans %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "People having you as a friend" %}
</h3>
{% render_table trusted_by %}
</div>
{% endblock %}
{% block extrajavascript %}
<script src="{% static "member/js/trust.js" %}"></script>
<script src="{% static "js/autocomplete_model.js" %}"></script>
{% endblock%}

View File

@ -7,7 +7,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block content %}
{% if can_manage_registrations %}
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
<svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
</svg>
{% trans "Registrations" %}
</a>
{% endif %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date

View File

@ -183,7 +183,7 @@ class TestMemberships(TestCase):
club = Club.objects.get(name="Kfet")
else:
club = Club.objects.create(
name="Second club without BDE",
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
parent_club=None,
email="newclub@example.com",
require_memberships=True,
@ -335,7 +335,6 @@ class TestMemberships(TestCase):
ml_sports_registration=True,
ml_art_registration=True,
report_frequency=7,
VSS_charter_read=True
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.assertTrue(User.objects.filter(username="toto changed").exists())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
@ -23,6 +23,5 @@ urlpatterns = [
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
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'),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta, date
@ -18,15 +18,15 @@ from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from note.models import Alias, NoteClub, NoteUser, Trust
from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
from note.tables import HistoryTable, AliasTable
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 .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\
CustomAuthenticationForm, MembershipRolesForm
from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
@ -174,7 +174,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note = NoteUser.objects.get(pk=user.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = False
modified_note.is_active = True
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
@ -183,14 +183,14 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
.check_perm(self.request, "note.change_note_is_active", modified_note)
old_note._force_save = True
old_note._no_signal = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
.check_perm(self.request, "note.change_note_is_active", modified_note)
return context
@ -243,40 +243,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return context
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user trust relationships
"""
model = User
template_name = 'member/profile_trust.html'
context_object_name = 'user_object'
extra_context = {"title": _("Note friendships")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["trusting"] = TrustTable(
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["trusted_by"] = TrustedTable(
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note,
trusted=context["object"].note
))
context["widget"] = {
"name": "trusted",
"resetable": True,
"attrs": {
"class": "autocomplete form-control",
"id": "trusted",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
}
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user aliases.
@ -290,8 +256,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
.order_by('normalized_name').all())
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
@ -326,15 +291,12 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
"""Save image to note"""
image = form.cleaned_data['image']
if image is None:
image = "pic/default.png"
# Rename as a PNG or GIF
extension = image.name.split(".")[-1]
if extension == "gif":
image.name = "{}_pic.gif".format(self.object.note.pk)
else:
# Rename as a PNG or GIF
extension = image.name.split(".")[-1]
if extension == "gif":
image.name = "{}_pic.gif".format(self.object.note.pk)
else:
image.name = "{}_pic.png".format(self.object.note.pk)
image.name = "{}_pic.png".format(self.object.note.pk)
# Save
self.object.note.display_image = image
@ -441,12 +403,9 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
context = super().get_context_data(**kwargs)
club = self.object
context["note"] = club.note
club = context["club"]
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates()
# managers list
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
date_start__lte=date.today(), date_end__gte=date.today())\
@ -484,29 +443,6 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_add_members"] = PermissionBackend()\
.has_perm(self.request.user, "member.add_membership", empty_membership)
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteClub.objects.get(pk=club.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = False
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note._force_save = True
old_note._no_signal = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
return context
@ -756,10 +692,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
club = old_membership.club
user = old_membership.user
# Update club membership date
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates()
form.instance.club = club
# Get form data

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction, SpecialTransaction
from .templatetags.pretty_money import pretty_money
@ -21,16 +21,6 @@ class AliasInlines(admin.TabularInline):
model = Alias
class TrustInlines(admin.TabularInline):
"""
Define trusts when editing the trusting note
"""
model = Trust
fk_name = "trusting"
extra = 0
readonly_fields = ("trusted",)
@admin.register(Note, site=admin_site)
class NoteAdmin(PolymorphicParentModelAdmin):
"""
@ -102,7 +92,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
"""
Child for an user note, see NoteAdmin
"""
inlines = (AliasInlines, TrustInlines)
inlines = (AliasInlines,)
# We can't change user after creation or the balance
readonly_fields = ('user', 'balance')

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
@ -11,9 +11,8 @@ from member.models import Membership
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
from rest_framework.validators import UniqueTogetherValidator
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
RecurrentTransaction, SpecialTransaction
@ -78,20 +77,6 @@ class NoteUserSerializer(serializers.ModelSerializer):
return str(obj)
class TrustSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Trusts.
The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
"""
class Meta:
model = Trust
fields = '__all__'
validators = [UniqueTogetherValidator(
queryset=Trust.objects.all(), fields=('trusting', 'trusted'),
message=_("This friendship already exists"))]
class AliasSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Aliases.

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \
TrustViewSet
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
def register_note_urls(router, path):
@ -12,7 +11,6 @@ def register_note_urls(router, path):
"""
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import re
@ -13,10 +13,9 @@ from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
TrustSerializer
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
@ -57,41 +56,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
return queryset.order_by("id")
class TrustViewSet(ReadProtectedModelViewSet):
"""
REST Trust View set.
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
then render it on /api/note/trust/
"""
queryset = Trust.objects
serializer_class = TrustSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
'$trusted__alias__name', '$trusted__alias__normalized_name']
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
ordering_fields = ['trusting', 'trusted', ]
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# trust relationship can't change people involved
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
class AliasViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/note/aliases/
then render it on /api/aliases/
"""
queryset = Alias.objects
serializer_class = AliasSerializer

Some files were not shown because too many files have changed in this diff Show More