diff --git a/apps/activity/templates/activity/activity_detail.html b/apps/activity/templates/activity/activity_detail.html index 1a8d01ee..bb0fc57a 100644 --- a/apps/activity/templates/activity/activity_detail.html +++ b/apps/activity/templates/activity/activity_detail.html @@ -37,6 +37,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% render_table guests %}
+ {% endif %} {% endblock %} diff --git a/apps/activity/templates/activity/activity_list.html b/apps/activity/templates/activity/activity_list.html index bf5ba70f..79da6d97 100644 --- a/apps/activity/templates/activity/activity_list.html +++ b/apps/activity/templates/activity/activity_list.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "base_search.html" %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} @@ -44,6 +44,8 @@ SPDX-License-Identifier: GPL-3.0-or-later

{% trans "All activities" %}

- {% render_table table %} + {% render_table all %} + +{{ block.super }} {% endblock %} diff --git a/apps/activity/templates/activity/includes/activity_info.html b/apps/activity/templates/activity/includes/activity_info.html index f9ea634b..4565a086 100644 --- a/apps/activity/templates/activity/includes/activity_info.html +++ b/apps/activity/templates/activity/includes/activity_info.html @@ -1,7 +1,7 @@ {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} -{% load i18n perms pretty_money %} +{% load i18n perms pretty_money dict_get %} {% url 'activity:activity_detail' activity.pk as activity_detail_url %}
@@ -53,6 +53,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans 'opened'|capfirst %}
{{ activity.open|yesno }}
+ {% if show_entries|dict_get:activity %} +

+ {{ entries_count|dict_get:activity }} + {% if entries_count|dict_get:activity >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %} +

+ {% endif %}
+
+ +
@@ -114,7 +120,26 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} + {% endblock %} \ No newline at end of file diff --git a/apps/member/views.py b/apps/member/views.py index bf6245f5..72ad446e 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, UpdateView, TemplateView from django.views.generic.edit import FormMixin from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView +from django_tables2.export.views import ExportMixin from rest_framework.authtoken.models import Token from api.viewsets import is_regex from note.models import Alias, NoteClub, NoteUser, Trust @@ -950,11 +951,12 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id}) -class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): +class ClubMembersListView(ExportMixin, ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = Membership table_class = MembershipTable template_name = "member/club_members.html" extra_context = {"title": _("Members of the club")} + export_formats = ["csv"] def get_queryset(self, **kwargs): qs = super().get_queryset().filter(club_id=self.kwargs["pk"]) @@ -986,6 +988,14 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV return qs.distinct() + def get_export_filename(self, export_format): + return "members.csv" + + def get_export_content_type(self, export_format): + if export_format == "csv": + return "text/csv" + return super().get_export_content_type(export_format) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = Club.objects.filter( diff --git a/apps/note/static/note/js/consos.js b/apps/note/static/note/js/consos.js index d08d93bd..99bdf610 100644 --- a/apps/note/static/note/js/consos.js +++ b/apps/note/static/note/js/consos.js @@ -228,7 +228,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + 'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000) } - if (source.membership && source.membership.date_end < new Date().toISOString()) { + if (source.membership && source.membership.date_end <= new Date().toISOString()) { addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]), 'danger', 30000) } diff --git a/apps/note/static/note/js/transfer.js b/apps/note/static/note/js/transfer.js index 509d9b48..1c8797f4 100644 --- a/apps/note/static/note/js/transfer.js +++ b/apps/note/static/note/js/transfer.js @@ -66,6 +66,8 @@ $(document).ready(function () { arr.push(last) last.quantity = 1 + + if (last.note.club) { $('#last_name').val(last.note.name) @@ -111,7 +113,8 @@ $(document).ready(function () { dest.removeClass('d-none') $('#dest_note_list').removeClass('d-none') $('#debit_type').addClass('d-none') - + $('#reason').val('') + $('#source_note_label').text(select_emitters_label) $('#dest_note_label').text(select_receveirs_label) @@ -134,6 +137,7 @@ $(document).ready(function () { dest.val('') dest.tooltip('hide') $('#debit_type').addClass('d-none') + $('#reason').val('Rechargement note') $('#source_note_label').text(transfer_type_label) $('#dest_note_label').text(select_receveir_label) @@ -162,6 +166,7 @@ $(document).ready(function () { dest.addClass('d-none') dest.tooltip('hide') $('#debit_type').removeClass('d-none') + $('#reason').val('') $('#source_note_label').text(select_emitter_label) $('#dest_note_label').text(transfer_type_label) @@ -305,10 +310,10 @@ $('#btn_transfer').click(function () { destination: dest.note.id, destination_alias: dest.name }).done(function () { - if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) { + if (source.note.membership && source.note.membership.date_end <= new Date().toISOString()) { addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000) } - if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) { + if (dest.note.membership && dest.note.membership.date_end <= new Date().toISOString()) { addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000) } @@ -409,7 +414,7 @@ $('#btn_transfer').click(function () { bank: $('#bank').val() }).done(function () { addMsg(gettext('Credit/debit succeed!'), 'success', 10000) - if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) } + if (user_note.membership && user_note.membership.date_end <= new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) } reset() }).fail(function (err) { const errObj = JSON.parse(err.responseText) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index e642c4e6..0acd11c4 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -4430,6 +4430,22 @@ "description": "Modifier le type de caution de mon inscription WEI tant qu'elle n'est pas validée" } }, + { + "model": "permission.permission", + "pk": 298, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "{\"pk\": [\"membership\", \"weimembership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 2, + "field": "information_json", + "permanent": false, + "description": "Modifier les informations du bus" + } + }, { "model": "permission.permission", "pk": 311, @@ -4686,6 +4702,22 @@ "description": "Supprimer un succès" } }, + { + "model": "permission.permission", + "pk": 330, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"memberships__club\": [\"club\"]}", + "type": "view", + "mask": 2, + "field": "email", + "permanent": false, + "description": "Voir l'adresse mail des membres de son club" + } + }, { "model": "permission.role", "pk": 1, @@ -4833,7 +4865,11 @@ 221, 247, 258, - 259 + 259, + 260, + 263, + 265, + 330 ] } }, @@ -4845,7 +4881,6 @@ "name": "Pr\u00e9sident\u22c5e de club", "permissions": [ 62, - 135, 142 ] } @@ -5122,7 +5157,8 @@ 289, 290, 291, - 293 + 293, + 298 ] } }, @@ -5182,6 +5218,7 @@ "permissions": [ 37, 41, + 42, 53, 54, 55, @@ -5233,7 +5270,9 @@ 168, 176, 177, - 197 + 197, + 311, + 319 ] } }, @@ -5313,7 +5352,8 @@ 289, 290, 291, - 293 + 293, + 298 ] } }, diff --git a/apps/treasury/migrations/0011_sogecredit_valid.py b/apps/treasury/migrations/0011_sogecredit_valid.py new file mode 100644 index 00000000..44ef6c90 --- /dev/null +++ b/apps/treasury/migrations/0011_sogecredit_valid.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-09-28 20:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('treasury', '0010_alter_invoice_bde'), + ] + + operations = [ + migrations.AddField( + model_name='sogecredit', + name='valid', + field=models.BooleanField(blank=True, default=False, verbose_name='Valid'), + ), + ] diff --git a/apps/treasury/models.py b/apps/treasury/models.py index 48709801..63e1d7cd 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -308,6 +308,12 @@ class SogeCredit(models.Model): null=True, ) + valid = models.BooleanField( + default=False, + verbose_name=_("Valid"), + blank=True, + ) + class Meta: verbose_name = _("Credit from the Société générale") verbose_name_plural = _("Credits from the Société générale") @@ -338,7 +344,7 @@ class SogeCredit(models.Model): credit_transaction.save() credit_transaction.refresh_from_db() self.credit_transaction = credit_transaction - elif not self.valid: + elif not self.valid_legacy: self.credit_transaction.amount = self.amount self.credit_transaction._force_save = True self.credit_transaction.save() @@ -346,12 +352,12 @@ class SogeCredit(models.Model): return super().save(*args, **kwargs) @property - def valid(self): + def valid_legacy(self): return self.credit_transaction and self.credit_transaction.valid @property def amount(self): - if self.valid: + if self.valid_legacy: return self.credit_transaction.total amount = 0 transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False) @@ -365,7 +371,7 @@ class SogeCredit(models.Model): The Sogé credit may be created after the user already paid its memberships. We query transactions and update the credit, if it is unvalid. """ - if self.valid or not self.pk: + if self.valid_legacy or not self.pk: return # Soge do not pay BDE and kfet memberships since 2022 @@ -405,7 +411,7 @@ class SogeCredit(models.Model): Invalidating a Société générale delete the transaction of the bank if it was already created. Treasurers must know what they do, With Great Power Comes Great Responsibility... """ - if self.valid: + if self.valid_legacy: self.credit_transaction.valid = False self.credit_transaction.save() for tr in self.transactions.all(): @@ -414,7 +420,7 @@ class SogeCredit(models.Model): tr.save() def validate(self, force=False): - if self.valid and not force: + if self.valid_legacy and not force: # The credit is already done return diff --git a/apps/treasury/static/img/Diolistos_bg.jpg b/apps/treasury/static/img/Diolistos_bg.jpg index e9453dbd..3bbc7b2e 100755 Binary files a/apps/treasury/static/img/Diolistos_bg.jpg and b/apps/treasury/static/img/Diolistos_bg.jpg differ diff --git a/apps/treasury/tests/test_treasury.py b/apps/treasury/tests/test_treasury.py index 8feb5485..d1d5a414 100644 --- a/apps/treasury/tests/test_treasury.py +++ b/apps/treasury/tests/test_treasury.py @@ -359,7 +359,7 @@ class TestSogeCredits(TestCase): )) self.assertRedirects(response, reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), 302, 200) soge_credit.refresh_from_db() - self.assertTrue(soge_credit.valid) + self.assertTrue(soge_credit.valid_legacy) self.user.note.refresh_from_db() self.assertEqual( Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3) diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index 67439b6e..758776b8 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -17,7 +17,7 @@ from ...models import WEIMembership, Bus WORDS = { 'list': [ - 'Fiesta', 'Graillance', 'Move it move it', 'Calme', 'Nert et geek', 'Jeux de rôles et danse rock', + 'Fiesta', 'Graillance', 'Move it move it', 'Calme', 'Nerd et geek', 'Jeux de rôles et danse rock', 'Strass et paillettes', 'Spectaculaire', 'Splendide', 'Flow inégalable', 'Rap', 'Battles légendaires', 'Techno', 'Alcool', 'Kiffeur·euse', 'Rugby', 'Médiéval', 'Festif', 'Stylé', 'Chipie', 'Rétro', 'Vache', 'Farfadet', 'Fanfare', @@ -57,7 +57,7 @@ WORDS = { 42: "Un burgouzz de valouzz", 47: "Un ocarina (pour me téléporter hors de ce bourbier)", 48: "Des paillettes, un micro de karaoké et une enceinte bluetooth", - 45: "", + 45: "Un kebab", 44: "Une 86 et un caisson pour taper du pied", 46: "Une épée, un ballon et une tireuse", 43: "Des lunettes de soleil", @@ -176,7 +176,33 @@ WORDS = { 49: "Soirée raclette !" } ] - } + }, + 'stats': [ + { + "question": """Le WEI est structuré par bus, et au sein de chaque bus, par équipes. + Pour toi, être dans une équipe où tout le monde reste sobre (primo-entrants comme encadrants) c'est :""", + "answers": [ + (1, "Inenvisageable"), + (2, "À contre cœur"), + (3, "Pourquoi pas"), + (4, "Souhaitable"), + (5, "Nécessaire"), + ], + "help_text": "(De toute façon aucun alcool n'est consommé pendant les trajets du bus, ni aller, ni retour.)", + }, + { + "question": "Faire partie d'un bus qui n'apporte pas de boisson alcoolisée pour ses membres, pour toi c'est :", + "answers": [ + (1, "Inenvisageable"), + (2, "À contre cœur"), + (3, "Pourquoi pas"), + (4, "Souhaitable"), + (5, "Nécessaire"), + ], + "help_text": """(Tout les bus apportent de l'alcool cette année, cette question sert à l'organisation pour l'année prochaine. + De plus il y aura de toute façon de l'alcool commun au WEI et aucun alcool n'est consommé pendant les trajets en bus.)""", + }, + ] } IMAGES = { @@ -235,7 +261,7 @@ class WEISurveyForm2025(forms.Form): all_preferred_words = WORDS['list'] rng.shuffle(all_preferred_words) self.fields["words"].choices = [(w, w) for w in all_preferred_words] - else: + elif information.step <= len(WORDS['questions']): questions = list(WORDS['questions'].items()) idx = information.step - 1 if idx < len(questions): @@ -251,6 +277,15 @@ class WEISurveyForm2025(forms.Form): widget=OptionalImageRadioSelect(images=IMAGES.get(q, {})), required=True, ) + elif information.step == len(WORDS['questions']) + 1: + for i, v in enumerate(WORDS['stats']): + self.fields[f'stat_{i}'] = forms.ChoiceField( + label=v['question'], + choices=v['answers'], + widget=forms.RadioSelect(), + required=False, + help_text=_(v.get('help_text', '')) + ) def clean_words(self): data = self.cleaned_data['words'] @@ -377,7 +412,7 @@ class WEISurvey2025(WEISurvey): setattr(self.information, "word" + str(i), word) self.information.step += 1 self.save() - else: + elif 1 <= self.information.step <= len(WORDS['questions']): questions = list(WORDS['questions'].keys()) idx = self.information.step - 1 if idx < len(questions): @@ -385,6 +420,13 @@ class WEISurvey2025(WEISurvey): setattr(self.information, q, form.cleaned_data[q]) self.information.step += 1 self.save() + else: + for i, __ in enumerate(WORDS['stats']): + ans = form.cleaned_data.get(f'stat_{i}') + if ans is not None: + setattr(self.information, f'stat_{i}', ans) + self.information.step += 1 + self.save() @classmethod def get_algorithm_class(cls): @@ -394,7 +436,7 @@ class WEISurvey2025(WEISurvey): """ The survey is complete once the bus is chosen. """ - return self.information.step > len(WORDS['questions']) + return self.information.step > len(WORDS['questions']) + 1 @classmethod @lru_cache() diff --git a/apps/wei/static/wei/img/logo_auvergne_rhone_alpes.jpg b/apps/wei/static/wei/img/logo_auvergne_rhone_alpes.jpg new file mode 100644 index 00000000..d95f496b Binary files /dev/null and b/apps/wei/static/wei/img/logo_auvergne_rhone_alpes.jpg differ diff --git a/apps/wei/templates/wei/weiregistration_form.html b/apps/wei/templates/wei/weiregistration_form.html index fae85e0f..dc5f66e5 100644 --- a/apps/wei/templates/wei/weiregistration_form.html +++ b/apps/wei/templates/wei/weiregistration_form.html @@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {{ title }}
-
+ {% csrf_token %} {{ form|crispy }} {{ membership_form|crispy }} @@ -22,6 +22,46 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblock %} {% block extrajavascript %} + + + {% if not object.membership %} @@ -41,6 +43,8 @@ SPDX-License-Identifier: GPL-3.0-or-later {# Translation in javascript files #} + + {# If extra ressources are needed for a form, load here #} {% if form.media %} {{ form.media }} diff --git a/note_kfet/templates/registration/signup.html b/note_kfet/templates/registration/signup.html index 7bd503eb..71fe2511 100644 --- a/note_kfet/templates/registration/signup.html +++ b/note_kfet/templates/registration/signup.html @@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblocktrans %}
- + {% csrf_token %} {{ form|crispy }} {{ profile_form|crispy }} @@ -31,3 +31,45 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblock %} + +{% block extrajavascript %} + + +{% endblock %} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c40bf0bb..464cd4ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,10 +12,11 @@ django-filter~=25.1 django-mailer~=2.3.2 django-oauth-toolkit~=3.0.1 django-phonenumber-field~=8.1.0 -django-polymorphic~=3.1.0 +django-polymorphic~=4.1.0 djangorestframework~=3.16.0 django-rest-polymorphic~=0.1.10 django-tables2~=2.7.5 python-memcached~=1.62 phonenumbers~=9.0.8 +tablib~=3.8.0 Pillow>=11.3.0