mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 17:12:28 +01:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			family
			...
			translatio
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7e664c2d4e | ||
| 
						 | 
					95fa3c1bc9 | ||
| 
						 | 
					4c40566513 | ||
| 
						 | 
					7c45b59298 | ||
| 
						 | 
					418268db27 | ||
| 
						 | 
					73045586a3 | ||
| 
						 | 
					22d668a75c | ||
| 
						 | 
					5dfa12fad2 | ||
| 
						 | 
					5af69f719d | ||
| 
						 | 
					4f6b1d5b6c | ||
| 
						 | 
					d4cb464169 | ||
| 
						 | 
					27a1f36183 | ||
| 
						 | 
					83c8b9a3d0 | ||
| 
						 | 
					cb3b34f874 | ||
| 
						 | 
					0962a3735e | ||
| 
						 | 
					9907cfbd86 | ||
| 
						 | 
					ad90887691 | ||
| 
						 | 
					47d2476b51 | ||
| 
						 | 
					5d8720cf46 | ||
| 
						 | 
					8700144dea | ||
| 
						 | 
					d17ab26f2f | ||
| 
						 | 
					297f289d7e | ||
| 
						 | 
					034ad9a4ce | ||
| 
						 | 
					897d37f74d | ||
| 
						 | 
					42fb0aa2d6 | ||
| 
						 | 
					4bc43ec3cb | ||
| 
						 | 
					00737da69f | ||
| 
						 | 
					0934b8fa34 | ||
| 
						 | 
					7633c9ab4b | 
@@ -37,6 +37,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    <div id="guests_table">
 | 
					    <div id="guests_table">
 | 
				
			||||||
        {% render_table guests %}
 | 
					        {% render_table guests %}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="card-footer text-center">
 | 
				
			||||||
 | 
					        <button class="btn btn-block btn-primary mb-3" onclick="window.location.href='?_export=1&table=guests'">
 | 
				
			||||||
 | 
					            {% trans "Export to CSV" %}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base_search.html" %}
 | 
				
			||||||
{% comment %}
 | 
					{% comment %}
 | 
				
			||||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
					SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
@@ -44,6 +44,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    <h3 class="card-header text-center">
 | 
					    <h3 class="card-header text-center">
 | 
				
			||||||
        {% trans "All activities" %}
 | 
					        {% trans "All activities" %}
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
    {% render_table table %}
 | 
					    {% render_table all %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{ block.super }}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
{% comment %}
 | 
					{% comment %}
 | 
				
			||||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
					SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
{% load i18n perms pretty_money %}
 | 
					{% load i18n perms pretty_money dict_get %}
 | 
				
			||||||
{% url 'activity:activity_detail' activity.pk as activity_detail_url %}
 | 
					{% url 'activity:activity_detail' activity.pk as activity_detail_url %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div id="activity_info" class="card bg-light shadow mb-3">
 | 
					<div id="activity_info" class="card bg-light shadow mb-3">
 | 
				
			||||||
@@ -53,6 +53,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
            <dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
 | 
					            <dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
 | 
				
			||||||
            <dd class="col-xl-6">{{ activity.open|yesno }}</dd>
 | 
					            <dd class="col-xl-6">{{ activity.open|yesno }}</dd>
 | 
				
			||||||
        </dl>
 | 
					        </dl>
 | 
				
			||||||
 | 
					        {% if show_entries|dict_get:activity %}
 | 
				
			||||||
 | 
					            <h2 class="text-center">
 | 
				
			||||||
 | 
					                {{ entries_count|dict_get:activity }}
 | 
				
			||||||
 | 
					                {% if entries_count|dict_get:activity >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %}
 | 
				
			||||||
 | 
					            </h2>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="card-footer text-center">
 | 
					    <div class="card-footer text-center">
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										0
									
								
								apps/activity/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/activity/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										12
									
								
								apps/activity/templatetags/dict_get.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								apps/activity/templatetags/dict_get.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django import template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def dict_get(d, key):
 | 
				
			||||||
 | 
					    return d.get(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					register = template.Library()
 | 
				
			||||||
 | 
					register.filter('dict_get', dict_get)
 | 
				
			||||||
@@ -67,32 +67,65 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
 | 
				
			|||||||
    tables = [
 | 
					    tables = [
 | 
				
			||||||
        lambda data: ActivityTable(data, prefix="all-"),
 | 
					        lambda data: ActivityTable(data, prefix="all-"),
 | 
				
			||||||
        lambda data: ActivityTable(data, prefix="upcoming-"),
 | 
					        lambda data: ActivityTable(data, prefix="upcoming-"),
 | 
				
			||||||
 | 
					        lambda data: ActivityTable(data, prefix="search-"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    extra_context = {"title": _("Activities")}
 | 
					    extra_context = {"title": _("Activities")}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self, **kwargs):
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
        return super().get_queryset(**kwargs).distinct()
 | 
					        """
 | 
				
			||||||
 | 
					        Filter the user list with the given pattern.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return super().get_queryset().distinct()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_tables_data(self):
 | 
					    def get_tables_data(self):
 | 
				
			||||||
        # first table = all activities, second table = upcoming
 | 
					        # first table = all activities, second table = upcoming, third table = search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # table search
 | 
				
			||||||
 | 
					        qs = self.get_queryset().order_by('-date_start')
 | 
				
			||||||
 | 
					        if "search" in self.request.GET and self.request.GET['search']:
 | 
				
			||||||
 | 
					            pattern = self.request.GET['search']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # check regex
 | 
				
			||||||
 | 
					            valid_regex = is_regex(pattern)
 | 
				
			||||||
 | 
					            suffix = '__iregex' if valid_regex else '__istartswith'
 | 
				
			||||||
 | 
					            prefix = '^' if valid_regex else ''
 | 
				
			||||||
 | 
					            qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
 | 
				
			||||||
 | 
					                           | Q(**{f'organizer__name{suffix}': prefix + pattern})
 | 
				
			||||||
 | 
					                           | Q(**{f'organizer__note__alias__name{suffix}': prefix + pattern}))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            qs = qs.none()
 | 
				
			||||||
 | 
					        search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Activity, 'view'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            self.get_queryset().order_by("-date_start"),
 | 
					            self.get_queryset().order_by("-date_start"),
 | 
				
			||||||
            Activity.objects.filter(date_end__gt=timezone.now())
 | 
					            Activity.objects.filter(date_end__gt=timezone.now())
 | 
				
			||||||
                            .filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
 | 
					                            .filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
 | 
				
			||||||
                            .distinct()
 | 
					                            .distinct()
 | 
				
			||||||
                            .order_by("date_start")
 | 
					                            .order_by("date_start"),
 | 
				
			||||||
 | 
					            search_table,
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tables = context["tables"]
 | 
					        tables = context["tables"]
 | 
				
			||||||
        for name, table in zip(["table", "upcoming"], tables):
 | 
					        for name, table in zip(["all", "upcoming", "table"], tables):
 | 
				
			||||||
            context[name] = table
 | 
					            context[name] = table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
 | 
					        started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
 | 
				
			||||||
        context["started_activities"] = started_activities
 | 
					        context["started_activities"] = started_activities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        entries_count = {}
 | 
				
			||||||
 | 
					        show_entries = {}
 | 
				
			||||||
 | 
					        for activity in started_activities:
 | 
				
			||||||
 | 
					            if activity.activity_type.manage_entries:
 | 
				
			||||||
 | 
					                entries = Entry.objects.filter(activity=activity)
 | 
				
			||||||
 | 
					                entries_count[activity] = entries.count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                show_entries[activity] = True
 | 
				
			||||||
 | 
					        context["entries_count"] = entries_count
 | 
				
			||||||
 | 
					        context["show_entries"] = show_entries
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -103,12 +136,19 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
 | 
				
			|||||||
    model = Activity
 | 
					    model = Activity
 | 
				
			||||||
    context_object_name = "activity"
 | 
					    context_object_name = "activity"
 | 
				
			||||||
    extra_context = {"title": _("Activity detail")}
 | 
					    extra_context = {"title": _("Activity detail")}
 | 
				
			||||||
 | 
					    export_formats = ["csv"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tables = [
 | 
					    tables = [
 | 
				
			||||||
        lambda data: GuestTable(data, prefix="guests-"),
 | 
					        GuestTable,
 | 
				
			||||||
        lambda data: OpenerTable(data, prefix="opener-"),
 | 
					        OpenerTable,
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_tables(self):
 | 
				
			||||||
 | 
					        tables = super().get_tables()
 | 
				
			||||||
 | 
					        tables[0].prefix = "guests"
 | 
				
			||||||
 | 
					        tables[1].prefix = "opener"
 | 
				
			||||||
 | 
					        return tables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_tables_data(self):
 | 
					    def get_tables_data(self):
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            Guest.objects.filter(activity=self.object)
 | 
					            Guest.objects.filter(activity=self.object)
 | 
				
			||||||
@@ -117,6 +157,51 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
 | 
				
			|||||||
                              .filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
 | 
					                              .filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_to_response(self, context, **response_kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Gère l'export CSV manuel pour MultiTableMixin.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if "_export" in self.request.GET:
 | 
				
			||||||
 | 
					            import tablib
 | 
				
			||||||
 | 
					            table_name = self.request.GET.get("table")
 | 
				
			||||||
 | 
					            if table_name:
 | 
				
			||||||
 | 
					                tables = self.get_tables()
 | 
				
			||||||
 | 
					                data_list = self.get_tables_data()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for t, d in zip(tables, data_list):
 | 
				
			||||||
 | 
					                    if t.prefix == table_name:
 | 
				
			||||||
 | 
					                        # Préparer le CSV
 | 
				
			||||||
 | 
					                        dataset = tablib.Dataset()
 | 
				
			||||||
 | 
					                        columns = list(t.base_columns)  # noms des colonnes
 | 
				
			||||||
 | 
					                        dataset.headers = columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        for row in d:
 | 
				
			||||||
 | 
					                            values = []
 | 
				
			||||||
 | 
					                            for col in columns:
 | 
				
			||||||
 | 
					                                try:
 | 
				
			||||||
 | 
					                                    val = getattr(row, col, "")
 | 
				
			||||||
 | 
					                                    # Gestion spéciale pour la colonne 'entry'
 | 
				
			||||||
 | 
					                                    if col == "entry":
 | 
				
			||||||
 | 
					                                        if getattr(row, "has_entry", False):
 | 
				
			||||||
 | 
					                                            val = timezone.localtime(row.entry.time).strftime("%Y-%m-%d %H:%M:%S")
 | 
				
			||||||
 | 
					                                        else:
 | 
				
			||||||
 | 
					                                            val = ""
 | 
				
			||||||
 | 
					                                    values.append(str(val) if val is not None else "")
 | 
				
			||||||
 | 
					                                except Exception:  # RelatedObjectDoesNotExist ou autre
 | 
				
			||||||
 | 
					                                    values.append("")
 | 
				
			||||||
 | 
					                            dataset.append(values)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        csv_bytes = dataset.export("csv")
 | 
				
			||||||
 | 
					                        if isinstance(csv_bytes, str):
 | 
				
			||||||
 | 
					                            csv_bytes = csv_bytes.encode("utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        response = HttpResponse(csv_bytes, content_type="text/csv")
 | 
				
			||||||
 | 
					                        response["Content-Disposition"] = f'attachment; filename="{table_name}.csv"'
 | 
				
			||||||
 | 
					                        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Sinon rendu normal
 | 
				
			||||||
 | 
					        return super().render_to_response(context, **response_kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data()
 | 
					        context = super().get_context_data()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -137,6 +222,14 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
 | 
				
			|||||||
                "placeholder": ""
 | 
					                "placeholder": ""
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if self.object.activity_type.manage_entries:
 | 
				
			||||||
 | 
					            entries = Entry.objects.filter(activity=self.object)
 | 
				
			||||||
 | 
					            context["entries_count"] = {self.object: entries.count()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            context["show_entries"] = {self.object: timezone.now() > timezone.localtime(self.object.date_start)}
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            context["entries_count"] = {self.object: 0}
 | 
				
			||||||
 | 
					            context["show_entries"] = {self.object: False}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django_tables2 as tables
 | 
					import django_tables2 as tables
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Food
 | 
					from .models import Food
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,10 +11,25 @@ class FoodTable(tables.Table):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    List all foods.
 | 
					    List all foods.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    qr_code_numbers = tables.Column(empty_values=(), verbose_name=_("QR Codes"), orderable=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    date = tables.Column(empty_values=(), verbose_name=_("Arrival/creation date"), orderable=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_date(self, record):
 | 
				
			||||||
 | 
					        if record.__class__.__name__ == "BasicFood":
 | 
				
			||||||
 | 
					            return record.arrival_date.strftime("%d/%m/%Y %H:%M")
 | 
				
			||||||
 | 
					        elif record.__class__.__name__ == "TransformedFood":
 | 
				
			||||||
 | 
					            return record.creation_date.strftime("%d/%m/%Y %H:%M")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return "--"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_qr_code_numbers(self, record):
 | 
				
			||||||
 | 
					        return ", ".join(str(q.qr_code_number) for q in record.QR_code.all())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Food
 | 
					        model = Food
 | 
				
			||||||
        template_name = 'django_tables2/bootstrap4.html'
 | 
					        template_name = 'django_tables2/bootstrap4.html'
 | 
				
			||||||
        fields = ('name', 'owner', 'allergens', 'expiry_date')
 | 
					        fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'date', 'expiry_date')
 | 
				
			||||||
        row_attrs = {
 | 
					        row_attrs = {
 | 
				
			||||||
            'class': 'table-row',
 | 
					            'class': 'table-row',
 | 
				
			||||||
            'data-href': lambda record: 'detail/' + str(record.pk),
 | 
					            'data-href': lambda record: 'detail/' + str(record.pk),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,6 +34,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="card-body">
 | 
					  <div class="card-body">
 | 
				
			||||||
 | 
					    <div class="form-check">
 | 
				
			||||||
 | 
					      <label for="stock_only" class="form-check-label">
 | 
				
			||||||
 | 
					          <input id="stock_only" name="stock_only" type="checkbox" class="checkboxinput form-check-input" checked>
 | 
				
			||||||
 | 
					          {% trans "Filter with only food in stock" %}
 | 
				
			||||||
 | 
					      </label>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
      <input id="searchbar" type="text" class="form-control"
 | 
					      <input id="searchbar" type="text" class="form-control"
 | 
				
			||||||
          placeholder="{% trans "Search by attribute such as name..." %}">
 | 
					          placeholder="{% trans "Search by attribute such as name..." %}">
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -114,7 +120,26 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script type="text/javascript">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let old_pattern = null;
 | 
				
			||||||
 | 
					    let searchbar_obj = $("#searchbar");
 | 
				
			||||||
 | 
					    let stock_only_obj = $("#stock_only");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function reloadTable() {
 | 
				
			||||||
 | 
					        let pattern = searchbar_obj.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#dynamic-table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
 | 
				
			||||||
 | 
					            stock_only_obj.is(':checked') ? "" : "&stock=1") + " #dynamic-table");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    searchbar_obj.keyup(reloadTable);
 | 
				
			||||||
 | 
					    stock_only_obj.change(reloadTable);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $(document).on("click", ".table-row", function () {
 | 
				
			||||||
 | 
					        window.document.location = $(this).data("href");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  document.addEventListener('DOMContentLoaded', function() {
 | 
					  document.addEventListener('DOMContentLoaded', function() {
 | 
				
			||||||
      document.getElementById('goButton').addEventListener('click', function(event) {
 | 
					      document.getElementById('goButton').addEventListener('click', function(event) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,16 +65,24 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
 | 
				
			|||||||
            suffix = '__iregex' if valid_regex else '__istartswith'
 | 
					            suffix = '__iregex' if valid_regex else '__istartswith'
 | 
				
			||||||
            prefix = '^' if valid_regex else ''
 | 
					            prefix = '^' if valid_regex else ''
 | 
				
			||||||
            qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
 | 
					            qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
 | 
				
			||||||
                           | Q(**{f'owner__name{suffix}': prefix + pattern}))
 | 
					                           | Q(**{f'owner__name{suffix}': prefix + pattern})
 | 
				
			||||||
 | 
					                           | Q(**{f'owner__note__alias__name{suffix}': prefix + pattern}))
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            qs = qs.none()
 | 
					            qs = qs.none()
 | 
				
			||||||
 | 
					        if "stock" not in self.request.GET or not self.request.GET["stock"] == '1':
 | 
				
			||||||
 | 
					            qs = qs.filter(end_of_life='')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
 | 
					        search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
 | 
				
			||||||
        # table open
 | 
					        # table open
 | 
				
			||||||
        open_table = self.get_queryset().order_by('expiry_date').filter(
 | 
					        open_table = self.get_queryset().filter(
 | 
				
			||||||
            Q(polymorphic_ctype__model='transformedfood')
 | 
					            Q(polymorphic_ctype__model='transformedfood')
 | 
				
			||||||
            | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
 | 
					            | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
 | 
				
			||||||
                expiry_date__lt=timezone.now(), end_of_life='').filter(
 | 
					                expiry_date__lt=timezone.now(), end_of_life='').filter(
 | 
				
			||||||
                    PermissionBackend.filter_queryset(self.request, Food, 'view'))
 | 
					                    PermissionBackend.filter_queryset(self.request, Food, 'view'))
 | 
				
			||||||
 | 
					        open_table = open_table.union(self.get_queryset().filter(
 | 
				
			||||||
 | 
					            Q(end_of_life='', order__iexact='open')
 | 
				
			||||||
 | 
					        ).filter(
 | 
				
			||||||
 | 
					            PermissionBackend.filter_queryset(self.request, Food, 'view'))).order_by('expiry_date')
 | 
				
			||||||
        # table served
 | 
					        # table served
 | 
				
			||||||
        served_table = self.get_queryset().order_by('-pk').filter(
 | 
					        served_table = self.get_queryset().order_by('-pk').filter(
 | 
				
			||||||
            end_of_life='', is_ready=True).exclude(
 | 
					            end_of_life='', is_ready=True).exclude(
 | 
				
			||||||
@@ -95,6 +103,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
 | 
				
			|||||||
                owner=club, end_of_life='').filter(
 | 
					                owner=club, end_of_life='').filter(
 | 
				
			||||||
                    PermissionBackend.filter_queryset(self.request, Food, 'view')
 | 
					                    PermissionBackend.filter_queryset(self.request, Food, 'view')
 | 
				
			||||||
            ))
 | 
					            ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [search_table, open_table, served_table] + club_table
 | 
					        return [search_table, open_table, served_table] + club_table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
@@ -218,7 +227,7 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
 | 
				
			|||||||
        copy = self.request.GET.get('copy', None)
 | 
					        copy = self.request.GET.get('copy', None)
 | 
				
			||||||
        if copy is not None:
 | 
					        if copy is not None:
 | 
				
			||||||
            food = BasicFood.objects.get(pk=copy)
 | 
					            food = BasicFood.objects.get(pk=copy)
 | 
				
			||||||
            print(context['form'].fields)
 | 
					
 | 
				
			||||||
            for field in context['form'].fields:
 | 
					            for field in context['form'].fields:
 | 
				
			||||||
                if field == 'allergens':
 | 
					                if field == 'allergens':
 | 
				
			||||||
                    context['form'].fields[field].initial = getattr(food, field).all()
 | 
					                    context['form'].fields[field].initial = getattr(food, field).all()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ from django.contrib.auth.forms import AuthenticationForm
 | 
				
			|||||||
from django.contrib.auth.models import User
 | 
					from django.contrib.auth.models import User
 | 
				
			||||||
from django.db import transaction
 | 
					from django.db import transaction
 | 
				
			||||||
from django.forms import CheckboxSelectMultiple
 | 
					from django.forms import CheckboxSelectMultiple
 | 
				
			||||||
 | 
					from phonenumber_field.formfields import PhoneNumberField
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from note.models import NoteSpecial, Alias
 | 
					from note.models import NoteSpecial, Alias
 | 
				
			||||||
@@ -45,6 +46,11 @@ class ProfileForm(forms.ModelForm):
 | 
				
			|||||||
    A form for the extras field provided by the :model:`member.Profile` model.
 | 
					    A form for the extras field provided by the :model:`member.Profile` model.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    # Remove widget=forms.HiddenInput() if you want to use report frequency.
 | 
					    # Remove widget=forms.HiddenInput() if you want to use report frequency.
 | 
				
			||||||
 | 
					    phone_number = PhoneNumberField(
 | 
				
			||||||
 | 
					        widget=forms.TextInput(attrs={"type": "tel", "class": "form-control"}),
 | 
				
			||||||
 | 
					        required=False
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
 | 
					    report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
 | 
					    last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
 | 
				
			||||||
@@ -72,7 +78,12 @@ class ProfileForm(forms.ModelForm):
 | 
				
			|||||||
        if not self.instance.section or (("department" in self.changed_data
 | 
					        if not self.instance.section or (("department" in self.changed_data
 | 
				
			||||||
                                         or "promotion" in self.changed_data) and "section" not in self.changed_data):
 | 
					                                         or "promotion" in self.changed_data) and "section" not in self.changed_data):
 | 
				
			||||||
            self.instance.section = self.instance.section_generated
 | 
					            self.instance.section = self.instance.section_generated
 | 
				
			||||||
        return super().save(commit)
 | 
					        instance = super().save(commit=False)
 | 
				
			||||||
 | 
					        if instance.phone_number:
 | 
				
			||||||
 | 
					            instance.phone_number = instance.phone_number.as_e164
 | 
				
			||||||
 | 
					        if commit:
 | 
				
			||||||
 | 
					            instance.save()
 | 
				
			||||||
 | 
					        return instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Profile
 | 
					        model = Profile
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -417,7 +417,7 @@ class Membership(models.Model):
 | 
				
			|||||||
        A membership is valid if today is between the start and the end date.
 | 
					        A membership is valid if today is between the start and the end date.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.date_end is not None:
 | 
					        if self.date_end is not None:
 | 
				
			||||||
            return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
 | 
					            return self.date_start.toordinal() <= datetime.datetime.now().toordinal() <= self.date_end.toordinal()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
 | 
					            return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,6 +92,20 @@ class MembershipTable(tables.Table):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user_email = tables.Column(
 | 
				
			||||||
 | 
					        verbose_name="Email",
 | 
				
			||||||
 | 
					        accessor="user.email",
 | 
				
			||||||
 | 
					        orderable=False,
 | 
				
			||||||
 | 
					        visible=False,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user_full_name = tables.Column(
 | 
				
			||||||
 | 
					        verbose_name=_("Full name"),
 | 
				
			||||||
 | 
					        accessor="user.get_full_name",
 | 
				
			||||||
 | 
					        orderable=False,
 | 
				
			||||||
 | 
					        visible=False,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_user(self, value):
 | 
					    def render_user(self, value):
 | 
				
			||||||
        # If the user has the right, link the displayed user with the page of its detail.
 | 
					        # If the user has the right, link the displayed user with the page of its detail.
 | 
				
			||||||
        s = value.username
 | 
					        s = value.username
 | 
				
			||||||
@@ -149,6 +163,16 @@ class MembershipTable(tables.Table):
 | 
				
			|||||||
                            + "'>" + s + "</a>")
 | 
					                            + "'>" + s + "</a>")
 | 
				
			||||||
        return s
 | 
					        return s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def value_user(self, record):
 | 
				
			||||||
 | 
					        return record.user.username if record.user else ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def value_club(self, record):
 | 
				
			||||||
 | 
					        return record.club.name if record.club else ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def value_roles(self, record):
 | 
				
			||||||
 | 
					        roles = record.roles.all()
 | 
				
			||||||
 | 
					        return ", ".join(str(role) for role in roles)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        attrs = {
 | 
					        attrs = {
 | 
				
			||||||
            'class': 'table table-condensed table-striped',
 | 
					            'class': 'table table-condensed table-striped',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,8 +36,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
            {% trans "There is no membership found with this pattern." %}
 | 
					            {% trans "There is no membership found with this pattern." %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					        <div class="card-footer text-center">
 | 
				
			||||||
 | 
					            <button class="btn btn-block btn-primary mb-3" onclick="window.location.href='?_export=csv'">
 | 
				
			||||||
 | 
					                {% trans "Export to CSV" %}
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block extrajavascript %}
 | 
					{% block extrajavascript %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
        {{ title }}
 | 
					        {{ title }}
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
    <div class="card-body">
 | 
					    <div class="card-body">
 | 
				
			||||||
        <form method="post">
 | 
					        <form method="post" id="profile-form">
 | 
				
			||||||
            {% csrf_token %}
 | 
					            {% csrf_token %}
 | 
				
			||||||
            {{ form | crispy }}
 | 
					            {{ form | crispy }}
 | 
				
			||||||
            {{ profile_form | crispy }}
 | 
					            {{ profile_form | crispy }}
 | 
				
			||||||
@@ -21,3 +21,45 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block extrajavascript %}
 | 
				
			||||||
 | 
					<!-- intl-tel-input CSS/JS -->
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					(() => {
 | 
				
			||||||
 | 
					    const input = document.querySelector("input[name='phone_number']");
 | 
				
			||||||
 | 
					    const form = document.querySelector("#profile-form");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!input || !form || input.type === "hidden" || input.disabled || input.readOnly) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const iti = window.intlTelInput(input, {
 | 
				
			||||||
 | 
					        initialCountry: "auto",
 | 
				
			||||||
 | 
					        nationalMode: false,
 | 
				
			||||||
 | 
					        autoPlaceholder: "off",
 | 
				
			||||||
 | 
					        geoIpLookup: callback => {
 | 
				
			||||||
 | 
					            fetch("https://ipapi.co/json")
 | 
				
			||||||
 | 
					                .then(res => res.json())
 | 
				
			||||||
 | 
					                .then(data => callback(data.country_code))
 | 
				
			||||||
 | 
					                .catch(() => callback("fr"));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        loadUtils: () => import("https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/utils.js"),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.addEventListener("submit", function(e){
 | 
				
			||||||
 | 
					        if (!input.value.trim()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const number = iti.getNumber(intlTelInput.utils.numberFormat.E164);
 | 
				
			||||||
 | 
					        if (number) {
 | 
				
			||||||
 | 
					            input.value = number;
 | 
				
			||||||
 | 
					            form.submit();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            input.focus();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
 | 
				
			|||||||
from django.views.generic import DetailView, UpdateView, TemplateView
 | 
					from django.views.generic import DetailView, UpdateView, TemplateView
 | 
				
			||||||
from django.views.generic.edit import FormMixin
 | 
					from django.views.generic.edit import FormMixin
 | 
				
			||||||
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
 | 
					from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
 | 
				
			||||||
 | 
					from django_tables2.export.views import ExportMixin
 | 
				
			||||||
from rest_framework.authtoken.models import Token
 | 
					from rest_framework.authtoken.models import Token
 | 
				
			||||||
from api.viewsets import is_regex
 | 
					from api.viewsets import is_regex
 | 
				
			||||||
from note.models import Alias, NoteClub, NoteUser, Trust
 | 
					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})
 | 
					        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
 | 
					    model = Membership
 | 
				
			||||||
    table_class = MembershipTable
 | 
					    table_class = MembershipTable
 | 
				
			||||||
    template_name = "member/club_members.html"
 | 
					    template_name = "member/club_members.html"
 | 
				
			||||||
    extra_context = {"title": _("Members of the club")}
 | 
					    extra_context = {"title": _("Members of the club")}
 | 
				
			||||||
 | 
					    export_formats = ["csv"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self, **kwargs):
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
        qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
 | 
					        qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
 | 
				
			||||||
@@ -986,6 +988,14 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return qs.distinct()
 | 
					        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):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        club = Club.objects.filter(
 | 
					        club = Club.objects.filter(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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, ' +
 | 
					          addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
 | 
				
			||||||
              'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000)
 | 
					              '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]),
 | 
					          addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]),
 | 
				
			||||||
              'danger', 30000)
 | 
					              'danger', 30000)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,6 +67,8 @@ $(document).ready(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      last.quantity = 1
 | 
					      last.quantity = 1
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (last.note.club) {
 | 
					      if (last.note.club) {
 | 
				
			||||||
        $('#last_name').val(last.note.name)
 | 
					        $('#last_name').val(last.note.name)
 | 
				
			||||||
        $('#first_name').val(last.note.name)
 | 
					        $('#first_name').val(last.note.name)
 | 
				
			||||||
@@ -111,6 +113,7 @@ $(document).ready(function () {
 | 
				
			|||||||
    dest.removeClass('d-none')
 | 
					    dest.removeClass('d-none')
 | 
				
			||||||
    $('#dest_note_list').removeClass('d-none')
 | 
					    $('#dest_note_list').removeClass('d-none')
 | 
				
			||||||
    $('#debit_type').addClass('d-none')
 | 
					    $('#debit_type').addClass('d-none')
 | 
				
			||||||
 | 
					    $('#reason').val('')
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    $('#source_note_label').text(select_emitters_label)
 | 
					    $('#source_note_label').text(select_emitters_label)
 | 
				
			||||||
    $('#dest_note_label').text(select_receveirs_label)
 | 
					    $('#dest_note_label').text(select_receveirs_label)
 | 
				
			||||||
@@ -134,6 +137,7 @@ $(document).ready(function () {
 | 
				
			|||||||
    dest.val('')
 | 
					    dest.val('')
 | 
				
			||||||
    dest.tooltip('hide')
 | 
					    dest.tooltip('hide')
 | 
				
			||||||
    $('#debit_type').addClass('d-none')
 | 
					    $('#debit_type').addClass('d-none')
 | 
				
			||||||
 | 
					    $('#reason').val('Rechargement note')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#source_note_label').text(transfer_type_label)
 | 
					    $('#source_note_label').text(transfer_type_label)
 | 
				
			||||||
    $('#dest_note_label').text(select_receveir_label)
 | 
					    $('#dest_note_label').text(select_receveir_label)
 | 
				
			||||||
@@ -162,6 +166,7 @@ $(document).ready(function () {
 | 
				
			|||||||
    dest.addClass('d-none')
 | 
					    dest.addClass('d-none')
 | 
				
			||||||
    dest.tooltip('hide')
 | 
					    dest.tooltip('hide')
 | 
				
			||||||
    $('#debit_type').removeClass('d-none')
 | 
					    $('#debit_type').removeClass('d-none')
 | 
				
			||||||
 | 
					    $('#reason').val('')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#source_note_label').text(select_emitter_label)
 | 
					    $('#source_note_label').text(select_emitter_label)
 | 
				
			||||||
    $('#dest_note_label').text(transfer_type_label)
 | 
					    $('#dest_note_label').text(transfer_type_label)
 | 
				
			||||||
@@ -305,10 +310,10 @@ $('#btn_transfer').click(function () {
 | 
				
			|||||||
            destination: dest.note.id,
 | 
					            destination: dest.note.id,
 | 
				
			||||||
            destination_alias: dest.name
 | 
					            destination_alias: dest.name
 | 
				
			||||||
          }).done(function () {
 | 
					          }).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)
 | 
					            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)
 | 
					            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()
 | 
					        bank: $('#bank').val()
 | 
				
			||||||
      }).done(function () {
 | 
					      }).done(function () {
 | 
				
			||||||
      addMsg(gettext('Credit/debit succeed!'), 'success', 10000)
 | 
					      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()
 | 
					      reset()
 | 
				
			||||||
    }).fail(function (err) {
 | 
					    }).fail(function (err) {
 | 
				
			||||||
      const errObj = JSON.parse(err.responseText)
 | 
					      const errObj = JSON.parse(err.responseText)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4430,6 +4430,22 @@
 | 
				
			|||||||
            "description": "Modifier le type de caution de mon inscription WEI tant qu'elle n'est pas validée"
 | 
					            "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",
 | 
					        "model": "permission.permission",
 | 
				
			||||||
        "pk": 311,
 | 
					        "pk": 311,
 | 
				
			||||||
@@ -4686,6 +4702,22 @@
 | 
				
			|||||||
            "description": "Supprimer un succès"
 | 
					            "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",
 | 
					        "model": "permission.role",
 | 
				
			||||||
        "pk": 1,
 | 
					        "pk": 1,
 | 
				
			||||||
@@ -4833,7 +4865,11 @@
 | 
				
			|||||||
                221,
 | 
					                221,
 | 
				
			||||||
                247,
 | 
					                247,
 | 
				
			||||||
                258,
 | 
					                258,
 | 
				
			||||||
                259
 | 
					                259,
 | 
				
			||||||
 | 
					                260,
 | 
				
			||||||
 | 
					                263,
 | 
				
			||||||
 | 
					                265,
 | 
				
			||||||
 | 
					                330
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -4845,7 +4881,6 @@
 | 
				
			|||||||
            "name": "Pr\u00e9sident\u22c5e de club",
 | 
					            "name": "Pr\u00e9sident\u22c5e de club",
 | 
				
			||||||
            "permissions": [
 | 
					            "permissions": [
 | 
				
			||||||
                62,
 | 
					                62,
 | 
				
			||||||
                135,
 | 
					 | 
				
			||||||
                142
 | 
					                142
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -5122,7 +5157,8 @@
 | 
				
			|||||||
                289,
 | 
					                289,
 | 
				
			||||||
                290,
 | 
					                290,
 | 
				
			||||||
                291,
 | 
					                291,
 | 
				
			||||||
                293
 | 
					                293,
 | 
				
			||||||
 | 
					                298
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -5182,6 +5218,7 @@
 | 
				
			|||||||
            "permissions": [
 | 
					            "permissions": [
 | 
				
			||||||
                37,
 | 
					                37,
 | 
				
			||||||
                41,
 | 
					                41,
 | 
				
			||||||
 | 
					                42,
 | 
				
			||||||
                53,
 | 
					                53,
 | 
				
			||||||
                54,
 | 
					                54,
 | 
				
			||||||
                55,
 | 
					                55,
 | 
				
			||||||
@@ -5233,7 +5270,9 @@
 | 
				
			|||||||
                168,
 | 
					                168,
 | 
				
			||||||
                176,
 | 
					                176,
 | 
				
			||||||
                177,
 | 
					                177,
 | 
				
			||||||
                197
 | 
					                197,
 | 
				
			||||||
 | 
					                311,
 | 
				
			||||||
 | 
					                319
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -5313,7 +5352,8 @@
 | 
				
			|||||||
                289,
 | 
					                289,
 | 
				
			||||||
                290,
 | 
					                290,
 | 
				
			||||||
                291,
 | 
					                291,
 | 
				
			||||||
                293
 | 
					                293,
 | 
				
			||||||
 | 
					                298
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								apps/treasury/migrations/0011_sogecredit_valid.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/treasury/migrations/0011_sogecredit_valid.py
									
									
									
									
									
										Normal file
									
								
							@@ -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'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -308,6 +308,12 @@ class SogeCredit(models.Model):
 | 
				
			|||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    valid = models.BooleanField(
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        verbose_name=_("Valid"),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Credit from the Société générale")
 | 
					        verbose_name = _("Credit from the Société générale")
 | 
				
			||||||
        verbose_name_plural = _("Credits 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.save()
 | 
				
			||||||
            credit_transaction.refresh_from_db()
 | 
					            credit_transaction.refresh_from_db()
 | 
				
			||||||
            self.credit_transaction = credit_transaction
 | 
					            self.credit_transaction = credit_transaction
 | 
				
			||||||
        elif not self.valid:
 | 
					        elif not self.valid_legacy:
 | 
				
			||||||
            self.credit_transaction.amount = self.amount
 | 
					            self.credit_transaction.amount = self.amount
 | 
				
			||||||
            self.credit_transaction._force_save = True
 | 
					            self.credit_transaction._force_save = True
 | 
				
			||||||
            self.credit_transaction.save()
 | 
					            self.credit_transaction.save()
 | 
				
			||||||
@@ -346,12 +352,12 @@ class SogeCredit(models.Model):
 | 
				
			|||||||
        return super().save(*args, **kwargs)
 | 
					        return super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def valid(self):
 | 
					    def valid_legacy(self):
 | 
				
			||||||
        return self.credit_transaction and self.credit_transaction.valid
 | 
					        return self.credit_transaction and self.credit_transaction.valid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def amount(self):
 | 
					    def amount(self):
 | 
				
			||||||
        if self.valid:
 | 
					        if self.valid_legacy:
 | 
				
			||||||
            return self.credit_transaction.total
 | 
					            return self.credit_transaction.total
 | 
				
			||||||
        amount = 0
 | 
					        amount = 0
 | 
				
			||||||
        transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False)
 | 
					        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.
 | 
					        The Sogé credit may be created after the user already paid its memberships.
 | 
				
			||||||
        We query transactions and update the credit, if it is unvalid.
 | 
					        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
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Soge do not pay BDE and kfet memberships since 2022
 | 
					# 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.
 | 
					        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...
 | 
					        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.valid = False
 | 
				
			||||||
            self.credit_transaction.save()
 | 
					            self.credit_transaction.save()
 | 
				
			||||||
        for tr in self.transactions.all():
 | 
					        for tr in self.transactions.all():
 | 
				
			||||||
@@ -414,7 +420,7 @@ class SogeCredit(models.Model):
 | 
				
			|||||||
            tr.save()
 | 
					            tr.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def validate(self, force=False):
 | 
					    def validate(self, force=False):
 | 
				
			||||||
        if self.valid and not force:
 | 
					        if self.valid_legacy and not force:
 | 
				
			||||||
            # The credit is already done
 | 
					            # The credit is already done
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 104 KiB  | 
@@ -359,7 +359,7 @@ class TestSogeCredits(TestCase):
 | 
				
			|||||||
        ))
 | 
					        ))
 | 
				
			||||||
        self.assertRedirects(response, reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), 302, 200)
 | 
					        self.assertRedirects(response, reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), 302, 200)
 | 
				
			||||||
        soge_credit.refresh_from_db()
 | 
					        soge_credit.refresh_from_db()
 | 
				
			||||||
        self.assertTrue(soge_credit.valid)
 | 
					        self.assertTrue(soge_credit.valid_legacy)
 | 
				
			||||||
        self.user.note.refresh_from_db()
 | 
					        self.user.note.refresh_from_db()
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
 | 
					            Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ from ...models import WEIMembership, Bus
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
WORDS = {
 | 
					WORDS = {
 | 
				
			||||||
    'list': [
 | 
					    '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',
 | 
					        'Strass et paillettes', 'Spectaculaire', 'Splendide', 'Flow inégalable', 'Rap', 'Battles légendaires',
 | 
				
			||||||
        'Techno', 'Alcool', 'Kiffeur·euse', 'Rugby', 'Médiéval', 'Festif',
 | 
					        'Techno', 'Alcool', 'Kiffeur·euse', 'Rugby', 'Médiéval', 'Festif',
 | 
				
			||||||
        'Stylé', 'Chipie', 'Rétro', 'Vache', 'Farfadet', 'Fanfare',
 | 
					        'Stylé', 'Chipie', 'Rétro', 'Vache', 'Farfadet', 'Fanfare',
 | 
				
			||||||
@@ -57,7 +57,7 @@ WORDS = {
 | 
				
			|||||||
                42: "Un burgouzz de valouzz",
 | 
					                42: "Un burgouzz de valouzz",
 | 
				
			||||||
                47: "Un ocarina (pour me téléporter hors de ce bourbier)",
 | 
					                47: "Un ocarina (pour me téléporter hors de ce bourbier)",
 | 
				
			||||||
                48: "Des paillettes, un micro de karaoké et une enceinte bluetooth",
 | 
					                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",
 | 
					                44: "Une 86 et un caisson pour taper du pied",
 | 
				
			||||||
                46: "Une épée, un ballon et une tireuse",
 | 
					                46: "Une épée, un ballon et une tireuse",
 | 
				
			||||||
                43: "Des lunettes de soleil",
 | 
					                43: "Des lunettes de soleil",
 | 
				
			||||||
@@ -176,7 +176,33 @@ WORDS = {
 | 
				
			|||||||
                49: "Soirée raclette !"
 | 
					                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 = {
 | 
					IMAGES = {
 | 
				
			||||||
@@ -235,7 +261,7 @@ class WEISurveyForm2025(forms.Form):
 | 
				
			|||||||
            all_preferred_words = WORDS['list']
 | 
					            all_preferred_words = WORDS['list']
 | 
				
			||||||
            rng.shuffle(all_preferred_words)
 | 
					            rng.shuffle(all_preferred_words)
 | 
				
			||||||
            self.fields["words"].choices = [(w, w) for w in 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())
 | 
					            questions = list(WORDS['questions'].items())
 | 
				
			||||||
            idx = information.step - 1
 | 
					            idx = information.step - 1
 | 
				
			||||||
            if idx < len(questions):
 | 
					            if idx < len(questions):
 | 
				
			||||||
@@ -251,6 +277,15 @@ class WEISurveyForm2025(forms.Form):
 | 
				
			|||||||
                    widget=OptionalImageRadioSelect(images=IMAGES.get(q, {})),
 | 
					                    widget=OptionalImageRadioSelect(images=IMAGES.get(q, {})),
 | 
				
			||||||
                    required=True,
 | 
					                    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):
 | 
					    def clean_words(self):
 | 
				
			||||||
        data = self.cleaned_data['words']
 | 
					        data = self.cleaned_data['words']
 | 
				
			||||||
@@ -377,7 +412,7 @@ class WEISurvey2025(WEISurvey):
 | 
				
			|||||||
                setattr(self.information, "word" + str(i), word)
 | 
					                setattr(self.information, "word" + str(i), word)
 | 
				
			||||||
            self.information.step += 1
 | 
					            self.information.step += 1
 | 
				
			||||||
            self.save()
 | 
					            self.save()
 | 
				
			||||||
        else:
 | 
					        elif 1 <= self.information.step <= len(WORDS['questions']):
 | 
				
			||||||
            questions = list(WORDS['questions'].keys())
 | 
					            questions = list(WORDS['questions'].keys())
 | 
				
			||||||
            idx = self.information.step - 1
 | 
					            idx = self.information.step - 1
 | 
				
			||||||
            if idx < len(questions):
 | 
					            if idx < len(questions):
 | 
				
			||||||
@@ -385,6 +420,13 @@ class WEISurvey2025(WEISurvey):
 | 
				
			|||||||
                setattr(self.information, q, form.cleaned_data[q])
 | 
					                setattr(self.information, q, form.cleaned_data[q])
 | 
				
			||||||
                self.information.step += 1
 | 
					                self.information.step += 1
 | 
				
			||||||
                self.save()
 | 
					                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
 | 
					    @classmethod
 | 
				
			||||||
    def get_algorithm_class(cls):
 | 
					    def get_algorithm_class(cls):
 | 
				
			||||||
@@ -394,7 +436,7 @@ class WEISurvey2025(WEISurvey):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        The survey is complete once the bus is chosen.
 | 
					        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
 | 
					    @classmethod
 | 
				
			||||||
    @lru_cache()
 | 
					    @lru_cache()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								apps/wei/static/wei/img/logo_auvergne_rhone_alpes.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								apps/wei/static/wei/img/logo_auvergne_rhone_alpes.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 34 KiB  | 
@@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
        {{ title }}
 | 
					        {{ title }}
 | 
				
			||||||
    </h3>
 | 
					    </h3>
 | 
				
			||||||
    <div class="card-body">
 | 
					    <div class="card-body">
 | 
				
			||||||
        <form method="post">
 | 
					        <form id="registration-form" method="post">
 | 
				
			||||||
            {% csrf_token %}
 | 
					            {% csrf_token %}
 | 
				
			||||||
            {{ form|crispy }}
 | 
					            {{ form|crispy }}
 | 
				
			||||||
            {{ membership_form|crispy }}
 | 
					            {{ membership_form|crispy }}
 | 
				
			||||||
@@ -22,6 +22,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block extrajavascript %}
 | 
					{% block extrajavascript %}
 | 
				
			||||||
 | 
					<!-- intl-tel-input CSS/JS -->
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					(() => {
 | 
				
			||||||
 | 
					    const input = document.querySelector("input[name='emergency_contact_phone']");
 | 
				
			||||||
 | 
					    const form = document.querySelector("#registration-form");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!input || !form || input.type === "hidden" || input.disabled || input.readOnly) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const iti = window.intlTelInput(input, {
 | 
				
			||||||
 | 
					        initialCountry: "auto",
 | 
				
			||||||
 | 
					        nationalMode: false,
 | 
				
			||||||
 | 
					        autoPlaceholder: "off",
 | 
				
			||||||
 | 
					        geoIpLookup: callback => {
 | 
				
			||||||
 | 
					            fetch("https://ipapi.co/json")
 | 
				
			||||||
 | 
					                .then(res => res.json())
 | 
				
			||||||
 | 
					                .then(data => callback(data.country_code))
 | 
				
			||||||
 | 
					                .catch(() => callback("fr"));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        loadUtils: () => import("https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/utils.js"),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.addEventListener("submit", function(e){
 | 
				
			||||||
 | 
					        if (!input.value.trim()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const number = iti.getNumber(intlTelInput.utils.numberFormat.E164);
 | 
				
			||||||
 | 
					        if (number) {
 | 
				
			||||||
 | 
					            input.value = number;
 | 
				
			||||||
 | 
					            form.submit();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            input.focus();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if not object.membership %}
 | 
					{% if not object.membership %}
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
    $(document).ready(function () {
 | 
					    $(document).ready(function () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,9 +53,11 @@ class TestWEIAlgorithm(TestCase):
 | 
				
			|||||||
                birth_date='2000-01-01',
 | 
					                birth_date='2000-01-01',
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            information = WEISurveyInformation2025(registration)
 | 
					            information = WEISurveyInformation2025(registration)
 | 
				
			||||||
            for j in range(1, 21):
 | 
					            for j in range(1, 1 + NB_WORDS):
 | 
				
			||||||
                setattr(information, f'word{j}', random.choice(WORDS['list']))
 | 
					                setattr(information, f'word{j}', random.choice(WORDS['list']))
 | 
				
			||||||
            information.step = 20
 | 
					            for q in WORDS['questions']:
 | 
				
			||||||
 | 
					                setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys())))
 | 
				
			||||||
 | 
					            information.step = len(WORDS['questions']) + 2
 | 
				
			||||||
            information.save(registration)
 | 
					            information.save(registration)
 | 
				
			||||||
            registration.save()
 | 
					            registration.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,7 +89,7 @@ class TestWEIAlgorithm(TestCase):
 | 
				
			|||||||
                setattr(information, f'word{j}', random.choice(WORDS['list']))
 | 
					                setattr(information, f'word{j}', random.choice(WORDS['list']))
 | 
				
			||||||
            for q in WORDS['questions']:
 | 
					            for q in WORDS['questions']:
 | 
				
			||||||
                setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys())))
 | 
					                setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys())))
 | 
				
			||||||
            information.step = len(WORDS['questions']) + 1
 | 
					            information.step = len(WORDS['questions']) + 2
 | 
				
			||||||
            information.save(registration)
 | 
					            information.save(registration)
 | 
				
			||||||
            registration.save()
 | 
					            registration.save()
 | 
				
			||||||
            survey = WEISurvey2025(registration)
 | 
					            survey = WEISurvey2025(registration)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -770,7 +770,7 @@ msgstr "Créer une famille ou un défi"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: apps/family/templates/family/manage.html:96
 | 
					#: apps/family/templates/family/manage.html:96
 | 
				
			||||||
msgid "Add a family"
 | 
					msgid "Add a family"
 | 
				
			||||||
msgstr "Ajouter une famille"
 | 
					msgstr "Fonder une famille"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/family/templates/family/manage.html:101
 | 
					#: apps/family/templates/family/manage.html:101
 | 
				
			||||||
msgid "Add a challenge"
 | 
					msgid "Add a challenge"
 | 
				
			||||||
@@ -4399,7 +4399,7 @@ msgstr "Géré par le BDE"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: note_kfet/templates/base.html:231
 | 
					#: note_kfet/templates/base.html:231
 | 
				
			||||||
msgid "Hosted by Cr@ns"
 | 
					msgid "Hosted by Cr@ns"
 | 
				
			||||||
msgstr "Hébergé par le Cr@ans"
 | 
					msgstr "Hébergé par le Cr@ns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: note_kfet/templates/base.html:273
 | 
					#: note_kfet/templates/base.html:273
 | 
				
			||||||
msgid "The note is not available for now"
 | 
					msgid "The note is not available for now"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -306,8 +306,8 @@ PIC_WIDTH = 200
 | 
				
			|||||||
PIC_RATIO = 1
 | 
					PIC_RATIO = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Custom phone number format
 | 
					# Custom phone number format
 | 
				
			||||||
PHONENUMBER_DB_FORMAT = 'NATIONAL'
 | 
					PHONENUMBER_DB_FORMAT = 'E164'
 | 
				
			||||||
PHONENUMBER_DEFAULT_REGION = 'FR'
 | 
					PHONENUMBER_DEFAULT_REGION = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# We add custom information to CAS, in order to give a normalized name to other services
 | 
					# We add custom information to CAS, in order to give a normalized name to other services
 | 
				
			||||||
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
 | 
					CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    <link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
 | 
					    <link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
 | 
				
			||||||
    <link rel="stylesheet" href="{% static "css/custom.css" %}">
 | 
					    <link rel="stylesheet" href="{% static "css/custom.css" %}">
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/css/intlTelInput.css">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {# JQuery, Bootstrap and Turbolinks JavaScript #}
 | 
					    {# JQuery, Bootstrap and Turbolinks JavaScript #}
 | 
				
			||||||
    <script src="{% static "jquery/jquery.min.js" %}"></script>
 | 
					    <script src="{% static "jquery/jquery.min.js" %}"></script>
 | 
				
			||||||
    <script src="{% static "popper.js/umd/popper.min.js" %}"></script>
 | 
					    <script src="{% static "popper.js/umd/popper.min.js" %}"></script>
 | 
				
			||||||
@@ -41,6 +43,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    {# Translation in javascript files #}
 | 
					    {# Translation in javascript files #}
 | 
				
			||||||
    <script src="{% static "js/jsi18n/"|add:LANGUAGE_CODE|add:".js" %}"></script>
 | 
					    <script src="{% static "js/jsi18n/"|add:LANGUAGE_CODE|add:".js" %}"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script src="https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/intlTelInput.min.js"></script>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    {# If extra ressources are needed for a form, load here #}
 | 
					    {# If extra ressources are needed for a form, load here #}
 | 
				
			||||||
    {% if form.media %}
 | 
					    {% if form.media %}
 | 
				
			||||||
        {{ form.media }}
 | 
					        {{ form.media }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
            {% endblocktrans %}
 | 
					            {% endblocktrans %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <form method="post">
 | 
					        <form method="post" id="profile_form">
 | 
				
			||||||
            {% csrf_token %}
 | 
					            {% csrf_token %}
 | 
				
			||||||
            {{ form|crispy }}
 | 
					            {{ form|crispy }}
 | 
				
			||||||
            {{ profile_form|crispy }}
 | 
					            {{ profile_form|crispy }}
 | 
				
			||||||
@@ -31,3 +31,45 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block extrajavascript %}
 | 
				
			||||||
 | 
					<!-- intl-tel-input CSS/JS -->
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					(() => {
 | 
				
			||||||
 | 
					    const input = document.querySelector("input[name='phone_number']");
 | 
				
			||||||
 | 
					    const form = document.querySelector("#profile_form");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!input || !form || input.type === "hidden" || input.disabled || input.readOnly) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const iti = window.intlTelInput(input, {
 | 
				
			||||||
 | 
					        initialCountry: "auto",
 | 
				
			||||||
 | 
					        nationalMode: false,
 | 
				
			||||||
 | 
					        autoPlaceholder: "off",
 | 
				
			||||||
 | 
					        geoIpLookup: callback => {
 | 
				
			||||||
 | 
					            fetch("https://ipapi.co/json")
 | 
				
			||||||
 | 
					                .then(res => res.json())
 | 
				
			||||||
 | 
					                .then(data => callback(data.country_code))
 | 
				
			||||||
 | 
					                .catch(() => callback("fr"));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        loadUtils: () => import("https://cdn.jsdelivr.net/npm/intl-tel-input@25.5.2/build/js/utils.js"),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.addEventListener("submit", function(e){
 | 
				
			||||||
 | 
					        if (!input.value.trim()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const number = iti.getNumber(intlTelInput.utils.numberFormat.E164);
 | 
				
			||||||
 | 
					        if (number) {
 | 
				
			||||||
 | 
					            input.value = number;
 | 
				
			||||||
 | 
					            form.submit();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            input.focus();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@@ -12,10 +12,11 @@ django-filter~=25.1
 | 
				
			|||||||
django-mailer~=2.3.2
 | 
					django-mailer~=2.3.2
 | 
				
			||||||
django-oauth-toolkit~=3.0.1
 | 
					django-oauth-toolkit~=3.0.1
 | 
				
			||||||
django-phonenumber-field~=8.1.0
 | 
					django-phonenumber-field~=8.1.0
 | 
				
			||||||
django-polymorphic~=3.1.0
 | 
					django-polymorphic~=4.1.0
 | 
				
			||||||
djangorestframework~=3.16.0
 | 
					djangorestframework~=3.16.0
 | 
				
			||||||
django-rest-polymorphic~=0.1.10
 | 
					django-rest-polymorphic~=0.1.10
 | 
				
			||||||
django-tables2~=2.7.5
 | 
					django-tables2~=2.7.5
 | 
				
			||||||
python-memcached~=1.62
 | 
					python-memcached~=1.62
 | 
				
			||||||
phonenumbers~=9.0.8
 | 
					phonenumbers~=9.0.8
 | 
				
			||||||
 | 
					tablib~=3.8.0
 | 
				
			||||||
Pillow>=11.3.0
 | 
					Pillow>=11.3.0
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user