From cd942779ca7c4175d6f989bf5524516e13ab15b4 Mon Sep 17 00:00:00 2001 From: quark Date: Tue, 11 Feb 2025 18:19:24 +0100 Subject: [PATCH] Wrapped apps --- apps/api/urls.py | 4 + apps/wrapped/__init__.py | 4 + apps/wrapped/admin.py | 16 ++++ apps/wrapped/api/__init__.py | 0 apps/wrapped/api/serializers.py | 28 ++++++ apps/wrapped/api/urls.py | 11 +++ apps/wrapped/api/views.py | 33 +++++++ apps/wrapped/apps.py | 10 +++ apps/wrapped/migrations/0001_initial.py | 86 ++++++++++++++++++ apps/wrapped/migrations/__init__.py | 0 apps/wrapped/models.py | 89 +++++++++++++++++++ apps/wrapped/tables.py | 72 +++++++++++++++ .../templates/wrapped/1/wrapped_view.html | 8 ++ .../templates/wrapped/wrapped_list.html | 62 +++++++++++++ apps/wrapped/urls.py | 13 +++ apps/wrapped/views.py | 63 +++++++++++++ note_kfet/settings/base.py | 1 + note_kfet/templates/base.html | 6 ++ note_kfet/urls.py | 1 + 19 files changed, 507 insertions(+) create mode 100644 apps/wrapped/__init__.py create mode 100644 apps/wrapped/admin.py create mode 100644 apps/wrapped/api/__init__.py create mode 100644 apps/wrapped/api/serializers.py create mode 100644 apps/wrapped/api/urls.py create mode 100644 apps/wrapped/api/views.py create mode 100644 apps/wrapped/apps.py create mode 100644 apps/wrapped/migrations/0001_initial.py create mode 100644 apps/wrapped/migrations/__init__.py create mode 100644 apps/wrapped/models.py create mode 100644 apps/wrapped/tables.py create mode 100644 apps/wrapped/templates/wrapped/1/wrapped_view.html create mode 100644 apps/wrapped/templates/wrapped/wrapped_list.html create mode 100644 apps/wrapped/urls.py create mode 100644 apps/wrapped/views.py diff --git a/apps/api/urls.py b/apps/api/urls.py index ef631004..ad2daf5f 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -47,6 +47,10 @@ if "wei" in settings.INSTALLED_APPS: from wei.api.urls import register_wei_urls register_wei_urls(router, 'wei') +if "wrapped" in settings.INSTALLED_APPS: + from wrapped.api.urls import register_wrapped_urls + register_wrapped_urls(router, 'wrapped') + app_name = 'api' # Wire up our API using automatic URL routing. diff --git a/apps/wrapped/__init__.py b/apps/wrapped/__init__.py new file mode 100644 index 00000000..e9c45ef0 --- /dev/null +++ b/apps/wrapped/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'activity.apps.WrappedConfig' diff --git a/apps/wrapped/admin.py b/apps/wrapped/admin.py new file mode 100644 index 00000000..96a2b397 --- /dev/null +++ b/apps/wrapped/admin.py @@ -0,0 +1,16 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib import admin +from note_kfet.admin import admin_site + +from .models import Bde, Wrapped + + +@admin.register(Bde, site=admin_site) +class BdeAdmin(admin.ModelAdmin): + pass + +@admin.register(Wrapped, site=admin_site) +class WrappedAdmin(admin.ModelAdmin): + pass diff --git a/apps/wrapped/api/__init__.py b/apps/wrapped/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/wrapped/api/serializers.py b/apps/wrapped/api/serializers.py new file mode 100644 index 00000000..d156ae75 --- /dev/null +++ b/apps/wrapped/api/serializers.py @@ -0,0 +1,28 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers + +from ..models import Wrapped, Bde + + +class WrappedSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Wrapped. + The djangorestframework plugin will analyse the model `Wrapped` and parse all fields in the API. + """ + + class Meta: + model = Wrapped + fields = '__all__' + + +class BdeSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Bde. + The djangorestframework plugin will analyse the model `Bde` and parse all fields in the API. + """ + + class Meta: + model = Bde + fields = '__all__' diff --git a/apps/wrapped/api/urls.py b/apps/wrapped/api/urls.py new file mode 100644 index 00000000..a989bb3c --- /dev/null +++ b/apps/wrapped/api/urls.py @@ -0,0 +1,11 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import WrappedViewSet, BdeViewSet + +def register_wrapped_urls(router, path): + """ + Configure router for Wrapped REST API. + """ + router.register(path + '/wrapped', WrappedViewSet) + router.register(path + '/bde', BdeViewSet) diff --git a/apps/wrapped/api/views.py b/apps/wrapped/api/views.py new file mode 100644 index 00000000..5fef0c66 --- /dev/null +++ b/apps/wrapped/api/views.py @@ -0,0 +1,33 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from api.viewsets import ReadProtectedModelViewSet +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import SearchFilter + +from .serializers import WrappedSerializer, BdeSerializer +from ..models import Wrapped, Bde + +class WrappedViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Wrapped` objects, serialize it to JSON with the given + serializer, then render it on /api/wrapped/wrapped/ + """ + queryset = Wrapped.objects.order_by('id') + serializer_class = WrappedSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['note', 'bde', ] + search_fields = ['$note', ] + +class BdeViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Bde` objects, serialize it to JSON with the given + serializer, then render it on /api/wrapped/bde/ + """ + queryset = Bde.objects.order_by('id') + serializer_class = BdeSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['name', ] + search_fields = ['$name', ] diff --git a/apps/wrapped/apps.py b/apps/wrapped/apps.py new file mode 100644 index 00000000..83630287 --- /dev/null +++ b/apps/wrapped/apps.py @@ -0,0 +1,10 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class WrappedConfig(AppConfig): + name = 'wrapped' + verbose_name = _('wrapped') diff --git a/apps/wrapped/migrations/0001_initial.py b/apps/wrapped/migrations/0001_initial.py new file mode 100644 index 00000000..15ffbd30 --- /dev/null +++ b/apps/wrapped/migrations/0001_initial.py @@ -0,0 +1,86 @@ +# Generated by Django 4.2.15 on 2025-02-10 12:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("note", "0007_alter_note_polymorphic_ctype_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Bde", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, verbose_name="name")), + ("date_start", models.DateTimeField(verbose_name="date start")), + ("date_end", models.DateTimeField(verbose_name="date end")), + ], + options={ + "verbose_name": "BDE", + "verbose_name_plural": "BDE", + }, + ), + migrations.CreateModel( + name="Wrapped", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "generated", + models.BooleanField(default=False, verbose_name="generated"), + ), + ("public", models.BooleanField(default=False, verbose_name="public")), + ( + "data_json", + models.TextField( + default="{}", + help_text="data in the wrapped and generated by the script generate_wrapped", + verbose_name="data json", + ), + ), + ( + "bde", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="wrapped.bde", + verbose_name="bde", + ), + ), + ( + "note", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="note.note", + verbose_name="note", + ), + ), + ], + options={ + "verbose_name": "Wrapped", + "verbose_name_plural": "Wrappeds", + "unique_together": {("note", "bde")}, + }, + ), + ] diff --git a/apps/wrapped/migrations/__init__.py b/apps/wrapped/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/wrapped/models.py b/apps/wrapped/models.py new file mode 100644 index 00000000..ea6f0efd --- /dev/null +++ b/apps/wrapped/models.py @@ -0,0 +1,89 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import os + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from note.models import Note + + +class Bde(models.Model): + """ + describe a BDE + """ + + name = models.CharField( + max_length=255, + verbose_name=_('name'), + ) + + date_start = models.DateTimeField( + verbose_name=_('date start'), + ) + + date_end = models.DateTimeField( + verbose_name=_('date end'), + ) + + class Meta: + verbose_name=_('BDE') + verbose_name_plural=_('BDE') + + def __str__(self): + return self.name + + +class Wrapped(models.Model): + """ + A Wrapped is associated to a note, a BDE year, + """ + generated = models.BooleanField( + verbose_name=_('generated'), + default=False, + ) + + public = models.BooleanField( + verbose_name=_('public'), + default=False, + ) + + bde = models.ForeignKey( + Bde, + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('bde'), + ) + + note = models.ForeignKey( + Note, + on_delete=models.PROTECT, + related_name='+', + verbose_name=_('note'), + ) + + data_json = models.TextField( + default='{}', + verbose_name=_('data json'), + help_text=_('data in the wrapped and generated by the script generate_wrapped'), + ) + + class Meta: + verbose_name=_('Wrapped') + verbose_name_plural=_('Wrappeds') + unique_together=('note','bde') + + def __str__(self): + return 'NoteKfet Wrapped of {note} sponsored by {bde}'.format(bde=str(self.bde),note=str(self.note)) + def makepublic(self): + self.public = not self.public + self.save() + return + + @property + def data(self): + return json.load(self.data_json) + + @data.setter + def data(self, data): + self.data_json = json.dumps(data, indent=2) diff --git a/apps/wrapped/tables.py b/apps/wrapped/tables.py new file mode 100644 index 00000000..4c3cc107 --- /dev/null +++ b/apps/wrapped/tables.py @@ -0,0 +1,72 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.utils import timezone +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ +from note_kfet.middlewares import get_current_request +import django_tables2 as tables +from django_tables2 import A +from permission.backends import PermissionBackend +from note.templatetags.pretty_money import pretty_money + +from .models import Wrapped, Bde + +class WrappedTable(tables.Table): + """ + List all wrapped + """ + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover', + 'id': 'wrapped_table' + } + row_attrs = { + 'class': lambda record: 'bg-danger' if not record.generated else '', + } + model = Wrapped + template_name = 'django_tables2/bootstrap4.html' + fields = ('note', 'bde', 'public', ) + + view = tables.LinkColumn( + 'wrapped:wrapped_detail', + args=[A('pk')], + + attrs={ + 'td': {'class': 'col-sm-2'}, + 'a': { + 'class': 'btn btn-sm btn-primary', + 'data-turbolinks': 'false', + } + }, + text=_('view the wrapped'), + accessor='pk', + verbose_name=_('View'), + orderable=False, + ) + + public = tables.Column( + accessor="pk", + orderable=False, + attrs={ + "td": { + "id": lambda record: "makepublic_"+ str(record.pk), + "class" : 'col-sm-1', + "data-toggle": "tooltip", + "title": lambda record: + (_("Click to make this wrapped private") if record.public else + _("Click to make this wrapped public")) if PermissionBackend.check_perm( + get_current_request(), "wrapped.change_wrapped_public", record) else None, + "onclick" : lambda record: + 'makepublic(' + str(record.id) + ', ' + str(not record.public).lower() + ')' + if PermissionBackend.check_perm(get_current_request(), "wrapped.change_wrapped_public", + record) else None + } + }, + ) + + def render_public(self, value, record): + val = "✔" if record.public else "✖" + return val + diff --git a/apps/wrapped/templates/wrapped/1/wrapped_view.html b/apps/wrapped/templates/wrapped/1/wrapped_view.html new file mode 100644 index 00000000..48ad8efe --- /dev/null +++ b/apps/wrapped/templates/wrapped/1/wrapped_view.html @@ -0,0 +1,8 @@ +{% comment %} +Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% block content %} +{{ wrapped.data_json }} +{% endblock %} + diff --git a/apps/wrapped/templates/wrapped/wrapped_list.html b/apps/wrapped/templates/wrapped/wrapped_list.html new file mode 100644 index 00000000..fd6bcb7e --- /dev/null +++ b/apps/wrapped/templates/wrapped/wrapped_list.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+
+
+
+
{{ title }}
+
+
+ {% render_table table %} +
+
+
+
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/apps/wrapped/urls.py b/apps/wrapped/urls.py new file mode 100644 index 00000000..dde4458b --- /dev/null +++ b/apps/wrapped/urls.py @@ -0,0 +1,13 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from . import views + +app_name = 'wrapped' + +urlpatterns = [ + path('', views.WrappedListView.as_view(), name='wrapped_list'), + path('/', views.WrappedDetailView.as_view(), name='wrapped_detail'), +] diff --git a/apps/wrapped/views.py b/apps/wrapped/views.py new file mode 100644 index 00000000..f6ec97c9 --- /dev/null +++ b/apps/wrapped/views.py @@ -0,0 +1,63 @@ +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from hashlib import md5 + +from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied +from django.db import transaction +from django.db.models import F, Q +from django.http import HttpResponse +from django.urls import reverse_lazy +from django.utils import timezone +from django.utils.decorators import method_decorator +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, SingleTableMixin, SingleTableView +from api.viewsets import is_regex +from note.models import Alias, NoteSpecial, NoteUser +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin, ProtectedCreateView + +from .models import Wrapped +from .tables import WrappedTable + +class WrappedListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + Display all Wrapped, and classify by year + """ + model = Wrapped + table_class = WrappedTable + template_name = 'wrapped/wrapped_list.html' + extra_context = {'title': _("List of wrapped")} + + def get_queryset(self, **kwargs): + return super().get_queryset(**kwargs).distinct() + + def get_table_data(self): + return Wrapped.objects.filter(PermissionBackend.filter_queryset( + self.request, Wrapped, "change", field='public')).distinct().order_by("-bde__date_start") + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) + +class WrappedDetailView(ProtectQuerysetMixin, DetailView): + """ + View a wrapped + """ + model = Wrapped + template_name = 'wrapped/0/wrapped_view.html' #by default + + def get(self, *args, **kwargs): + bde_id = Wrapped.objects.get(pk=kwargs['pk']).bde.id + self.template_name = 'wrapped/' + str(bde_id) + '/wrapped_view.html' + return super().get(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + return context diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 8378448d..113cf626 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -79,6 +79,7 @@ INSTALLED_APPS = [ 'scripts', 'treasury', 'wei', + 'wrapped', ] MIDDLEWARE = [ diff --git a/note_kfet/templates/base.html b/note_kfet/templates/base.html index a84f29b2..4f87228a 100644 --- a/note_kfet/templates/base.html +++ b/note_kfet/templates/base.html @@ -107,6 +107,12 @@ SPDX-License-Identifier: GPL-3.0-or-later {% url 'wei:current_wei_detail' as url %} {% trans 'WEI' %} + {% endif %} + {% if "wrapped.wrapped"|model_list_length >= 1 %} + {% endif %} {% if request.user.is_authenticated %}