mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-30 23:39:54 +01:00 
			
		
		
		
	Compare commits
	
		
			31 Commits
		
	
	
		
			family
			...
			app_downlo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 04001202f2 | ||
|  | af36d1427a | ||
|  | 75a59e0a7a | ||
|  | af39bf7068 | ||
|  | 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"> | ||||
|         {% render_table guests %} | ||||
|     </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> | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -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 | ||||
|     <h3 class="card-header text-center"> | ||||
|         {% trans "All activities" %} | ||||
|     </h3> | ||||
|     {% render_table table %} | ||||
|     {% render_table all %} | ||||
| </div> | ||||
|  | ||||
| {{ block.super }} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -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 %} | ||||
|  | ||||
| <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> | ||||
|             <dd class="col-xl-6">{{ activity.open|yesno }}</dd> | ||||
|         </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 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 = [ | ||||
|         lambda data: ActivityTable(data, prefix="all-"), | ||||
|         lambda data: ActivityTable(data, prefix="upcoming-"), | ||||
|         lambda data: ActivityTable(data, prefix="search-"), | ||||
|     ] | ||||
|     extra_context = {"title": _("Activities")} | ||||
|  | ||||
|     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): | ||||
|         # 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 [ | ||||
|             self.get_queryset().order_by("-date_start"), | ||||
|             Activity.objects.filter(date_end__gt=timezone.now()) | ||||
|                             .filter(PermissionBackend.filter_queryset(self.request, Activity, "view")) | ||||
|                             .distinct() | ||||
|                             .order_by("date_start") | ||||
|                             .order_by("date_start"), | ||||
|             search_table, | ||||
|         ] | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|  | ||||
|         tables = context["tables"] | ||||
|         for name, table in zip(["table", "upcoming"], tables): | ||||
|         for name, table in zip(["all", "upcoming", "table"], tables): | ||||
|             context[name] = table | ||||
|  | ||||
|         started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all() | ||||
|         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 | ||||
|  | ||||
|  | ||||
| @@ -103,12 +136,19 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix | ||||
|     model = Activity | ||||
|     context_object_name = "activity" | ||||
|     extra_context = {"title": _("Activity detail")} | ||||
|     export_formats = ["csv"] | ||||
|  | ||||
|     tables = [ | ||||
|         lambda data: GuestTable(data, prefix="guests-"), | ||||
|         lambda data: OpenerTable(data, prefix="opener-"), | ||||
|         GuestTable, | ||||
|         OpenerTable, | ||||
|     ] | ||||
|  | ||||
|     def get_tables(self): | ||||
|         tables = super().get_tables() | ||||
|         tables[0].prefix = "guests" | ||||
|         tables[1].prefix = "opener" | ||||
|         return tables | ||||
|  | ||||
|     def get_tables_data(self): | ||||
|         return [ | ||||
|             Guest.objects.filter(activity=self.object) | ||||
| @@ -117,6 +157,51 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix | ||||
|                               .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): | ||||
|         context = super().get_context_data() | ||||
|  | ||||
| @@ -137,6 +222,14 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix | ||||
|                 "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 | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import django_tables2 as tables | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from .models import Food | ||||
|  | ||||
| @@ -10,10 +11,25 @@ class FoodTable(tables.Table): | ||||
|     """ | ||||
|     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: | ||||
|         model = Food | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('name', 'owner', 'allergens', 'expiry_date') | ||||
|         fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'date', 'expiry_date') | ||||
|         row_attrs = { | ||||
|             'class': 'table-row', | ||||
|             'data-href': lambda record: 'detail/' + str(record.pk), | ||||
|   | ||||
| @@ -34,6 +34,12 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     </div> | ||||
|   </div> | ||||
|   <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" | ||||
|           placeholder="{% trans "Search by attribute such as name..." %}"> | ||||
|   </div> | ||||
| @@ -114,7 +120,26 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|   {% endif %} | ||||
| </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> | ||||
|   document.addEventListener('DOMContentLoaded', function() { | ||||
|       document.getElementById('goButton').addEventListener('click', function(event) { | ||||
|   | ||||
| @@ -65,16 +65,24 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li | ||||
|             suffix = '__iregex' if valid_regex else '__istartswith' | ||||
|             prefix = '^' if valid_regex else '' | ||||
|             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: | ||||
|             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')) | ||||
|         # 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='basicfood', basicfood__date_type='DLC')).filter( | ||||
|                 expiry_date__lt=timezone.now(), end_of_life='').filter( | ||||
|                     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 | ||||
|         served_table = self.get_queryset().order_by('-pk').filter( | ||||
|             end_of_life='', is_ready=True).exclude( | ||||
| @@ -95,6 +103,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li | ||||
|                 owner=club, end_of_life='').filter( | ||||
|                     PermissionBackend.filter_queryset(self.request, Food, 'view') | ||||
|             )) | ||||
|  | ||||
|         return [search_table, open_table, served_table] + club_table | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
| @@ -218,7 +227,7 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         copy = self.request.GET.get('copy', None) | ||||
|         if copy is not None: | ||||
|             food = BasicFood.objects.get(pk=copy) | ||||
|             print(context['form'].fields) | ||||
|  | ||||
|             for field in context['form'].fields: | ||||
|                 if field == 'allergens': | ||||
|                     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.db import transaction | ||||
| from django.forms import CheckboxSelectMultiple | ||||
| from phonenumber_field.formfields import PhoneNumberField | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| 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. | ||||
|     """ | ||||
|     # 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")) | ||||
|  | ||||
|     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 | ||||
|                                          or "promotion" in self.changed_data) and "section" not in self.changed_data): | ||||
|             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: | ||||
|         model = Profile | ||||
|   | ||||
| @@ -417,7 +417,7 @@ class Membership(models.Model): | ||||
|         A membership is valid if today is between the start and the end date. | ||||
|         """ | ||||
|         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: | ||||
|             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): | ||||
|         # If the user has the right, link the displayed user with the page of its detail. | ||||
|         s = value.username | ||||
| @@ -149,6 +163,16 @@ class MembershipTable(tables.Table): | ||||
|                             + "'>" + s + "</a>") | ||||
|         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: | ||||
|         attrs = { | ||||
|             'class': 'table table-condensed table-striped', | ||||
|   | ||||
| @@ -36,7 +36,13 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|             {% trans "There is no membership found with this pattern." %} | ||||
|         </div> | ||||
|         {% 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> | ||||
| {% endblock %} | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         {{ title }} | ||||
|     </h3> | ||||
|     <div class="card-body"> | ||||
|         <form method="post"> | ||||
|         <form method="post" id="profile-form"> | ||||
|             {% csrf_token %} | ||||
|             {{ form | crispy }} | ||||
|             {{ profile_form | crispy }} | ||||
| @@ -21,3 +21,45 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     </div> | ||||
| </div> | ||||
| {% 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.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( | ||||
|   | ||||
| @@ -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) | ||||
|         } | ||||
|   | ||||
| @@ -67,6 +67,8 @@ $(document).ready(function () { | ||||
|  | ||||
|       last.quantity = 1 | ||||
|        | ||||
|        | ||||
|  | ||||
|       if (last.note.club) { | ||||
|         $('#last_name').val(last.note.name) | ||||
|         $('#first_name').val(last.note.name) | ||||
| @@ -111,6 +113,7 @@ $(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) | ||||
|   | ||||
| @@ -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 | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										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, | ||||
|     ) | ||||
|  | ||||
|     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") | ||||
| @@ -332,7 +338,7 @@ class SogeCredit(models.Model): | ||||
|                 last_name=self.user.last_name, | ||||
|                 first_name=self.user.first_name, | ||||
|                 bank="Société générale", | ||||
|                 valid=False, | ||||
|                 valid=True, | ||||
|             ) | ||||
|             credit_transaction._force_save = True | ||||
|             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) | ||||
| @@ -397,7 +403,7 @@ class SogeCredit(models.Model): | ||||
|                         self.transactions.add(m.transaction) | ||||
|  | ||||
|         for tr in self.transactions.all(): | ||||
|             tr.valid = False | ||||
|             tr.valid = True | ||||
|             tr.save() | ||||
|  | ||||
|     def invalidate(self): | ||||
| @@ -422,6 +428,7 @@ class SogeCredit(models.Model): | ||||
|         self.invalidate() | ||||
|         # Refresh credit amount | ||||
|         self.save() | ||||
|         self.valid = True | ||||
|         self.credit_transaction.valid = True | ||||
|         self.credit_transaction._force_save = True | ||||
|         self.credit_transaction.save() | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 104 KiB | 
| @@ -56,6 +56,7 @@ class InvoiceTable(tables.Table): | ||||
|         model = Invoice | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('id', 'name', 'object', 'acquitted', 'invoice',) | ||||
|         order_by = ('-id',) | ||||
|  | ||||
|  | ||||
| class RemittanceTable(tables.Table): | ||||
|   | ||||
| @@ -417,7 +417,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi | ||||
|                 ) | ||||
|  | ||||
|         if "valid" not in self.request.GET or not self.request.GET["valid"]: | ||||
|             qs = qs.filter(credit_transaction__valid=False) | ||||
|             qs = qs.filter(valid=False) | ||||
|  | ||||
|         return qs | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
							
								
								
									
										
											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 }} | ||||
|     </h3> | ||||
|     <div class="card-body"> | ||||
|         <form method="post"> | ||||
|         <form id="registration-form" method="post"> | ||||
|             {% csrf_token %} | ||||
|             {{ form|crispy }} | ||||
|             {{ membership_form|crispy }} | ||||
| @@ -22,6 +22,46 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% endblock %} | ||||
|  | ||||
| {% 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 %} | ||||
| <script> | ||||
|     $(document).ready(function () { | ||||
|   | ||||
| @@ -53,9 +53,11 @@ class TestWEIAlgorithm(TestCase): | ||||
|                 birth_date='2000-01-01', | ||||
|             ) | ||||
|             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'])) | ||||
|             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) | ||||
|             registration.save() | ||||
|  | ||||
| @@ -87,7 +89,7 @@ class TestWEIAlgorithm(TestCase): | ||||
|                 setattr(information, f'word{j}', random.choice(WORDS['list'])) | ||||
|             for q in WORDS['questions']: | ||||
|                 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) | ||||
|             registration.save() | ||||
|             survey = WEISurvey2025(registration) | ||||
|   | ||||
| @@ -680,7 +680,7 @@ class TestWEIRegistration(TestCase): | ||||
|             self.assertTrue(soge_credit.exists()) | ||||
|             soge_credit = soge_credit.get() | ||||
|             self.assertTrue(membership.transaction in soge_credit.transactions.all()) | ||||
|             self.assertFalse(membership.transaction.valid) | ||||
|             self.assertTrue(membership.transaction.valid) | ||||
|  | ||||
|         # Check that if the WEI is started, we can't update a wei | ||||
|         self.wei.date_start = date(2000, 1, 1) | ||||
|   | ||||
| @@ -4366,6 +4366,14 @@ msgstr "" | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "Passwort oder Username vergessen?" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:44 | ||||
| msgid "Download on the AppStore" | ||||
| msgstr "Im AppStore herunterladen" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:48 | ||||
| msgid "Get it on Google Play" | ||||
| msgstr "Bei Google Play herunterladen" | ||||
|  | ||||
| #: note_kfet/templates/registration/password_change_done.html:13 | ||||
| msgid "Your password was changed." | ||||
| msgstr "Ihr Passwort wurde geändert." | ||||
|   | ||||
| @@ -4281,6 +4281,14 @@ msgstr "" | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "¿ Contraseña o nombre de usuario olvidado ?" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:44 | ||||
| msgid "Download on the AppStore" | ||||
| msgstr "Descargar en la AppStore" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:48 | ||||
| msgid "Get it on Google Play" | ||||
| msgstr "Descargar en Google Play" | ||||
|  | ||||
| #: note_kfet/templates/registration/password_change_done.html:13 | ||||
| msgid "Your password was changed." | ||||
| msgstr "Su contraseña fue cambiada con éxito." | ||||
|   | ||||
| @@ -770,7 +770,7 @@ msgstr "Créer une famille ou un défi" | ||||
|  | ||||
| #: apps/family/templates/family/manage.html:96 | ||||
| msgid "Add a family" | ||||
| msgstr "Ajouter une famille" | ||||
| msgstr "Fonder une famille" | ||||
|  | ||||
| #: apps/family/templates/family/manage.html:101 | ||||
| msgid "Add a challenge" | ||||
| @@ -4584,6 +4584,14 @@ msgstr "" | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "Mot de passe ou pseudo oublié ?" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:44 | ||||
| msgid "Download on the AppStore" | ||||
| msgstr "Télécharger sur l'AppStore" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:48 | ||||
| msgid "Get it on Google Play" | ||||
| msgstr "Télécharger sur Google Play" | ||||
|  | ||||
| #: note_kfet/templates/registration/password_change_done.html:13 | ||||
| msgid "Your password was changed." | ||||
| msgstr "Votre mot de passe a bien été changé." | ||||
|   | ||||
| @@ -306,8 +306,8 @@ PIC_WIDTH = 200 | ||||
| PIC_RATIO = 1 | ||||
|  | ||||
| # Custom phone number format | ||||
| PHONENUMBER_DB_FORMAT = 'NATIONAL' | ||||
| PHONENUMBER_DEFAULT_REGION = 'FR' | ||||
| PHONENUMBER_DB_FORMAT = 'E164' | ||||
| PHONENUMBER_DEFAULT_REGION = None | ||||
|  | ||||
| # We add custom information to CAS, in order to give a normalized name to other services | ||||
| CAS_AUTH_CLASS = 'member.auth.CustomAuthUser' | ||||
|   | ||||
							
								
								
									
										50
									
								
								note_kfet/static/img/appstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								note_kfet/static/img/appstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="126.50751" height="40" viewBox="0 0 126.50751 40"> | ||||
|   <title>Download_on_the_App_Store_Badge_FR_RGB_blk_100517</title> | ||||
|   <g> | ||||
|     <g> | ||||
|       <path d="M116.97821,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H116.97821c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50641,13.50641,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.50709,13.50709,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76753,6.76753,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/> | ||||
|       <path d="M8.44482,39.125c-.30467,0-.602-.0039-.90428-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37138,12.37138,0,0,1,.16552-1.87207,5.75577,5.75577,0,0,1,.54347-1.6621A5.37365,5.37365,0,0,1,2.61182,2.61768,5.56562,5.56562,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58579,12.58579,0,0,1,7.543.88721L8.44532.875h109.612l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59375,5.59375,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/> | ||||
|     </g> | ||||
|     <g> | ||||
|       <g id="_Group_" data-name="<Group>"> | ||||
|         <g id="_Group_2" data-name="<Group>"> | ||||
|           <g id="_Group_3" data-name="<Group>"> | ||||
|             <path id="_Path_" data-name="<Path>" d="M24.7718,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914A10.962,10.962,0,0,0,27.691,24.69985,4.78205,4.78205,0,0,1,24.7718,20.30068Z" style="fill: #fff"/> | ||||
|             <path id="_Path_2" data-name="<Path>" d="M22.04017,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.04017,12.21089Z" style="fill: #fff"/> | ||||
|           </g> | ||||
|         </g> | ||||
|       </g> | ||||
|       <g id="_Group_4" data-name="<Group>"> | ||||
|         <g> | ||||
|           <path d="M35.65528,14.70166V9.57813h-1.877V8.73486h4.67676v.84326H36.582v5.12354Z" style="fill: #fff"/> | ||||
|           <path d="M42.76466,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H39.63868v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117H41.9131a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,39.63868,12.03467ZM40.2754,9.4458l1.03809-1.42236h1.042L41.19337,9.4458Z" style="fill: #fff"/> | ||||
|           <path d="M44.05274,8.44092h.88867v6.26074h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M50.208,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H47.082v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,47.082,12.03467Zm.63672-2.58887,1.03809-1.42236h1.042L48.63673,9.4458Z" style="fill: #fff"/> | ||||
|           <path d="M54.40333,11.67041a1.00546,1.00546,0,0,0-1.06348-.76465c-.74414,0-1.19922.57031-1.19922,1.52979,0,.97607.459,1.55908,1.19922,1.55908a.97873.97873,0,0,0,1.06348-.74023h.86426a1.762,1.762,0,0,1-1.92285,1.53418,2.06791,2.06791,0,0,1-2.11328-2.353,2.05305,2.05305,0,0,1,2.1084-2.32373,1.77731,1.77731,0,0,1,1.92773,1.55859Z" style="fill: #fff"/> | ||||
|           <path d="M56.44728,8.44092h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723h-.88965v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M61.43946,13.42822c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031V11.625c0-.47559-.31445-.74414-.92188-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76514.562,1.76514,1.51318v3.07666h-.855v-.63281H64.293a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,61.43946,13.42822Zm2.89453-.38477V12.667l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,64.334,13.04346Z" style="fill: #fff"/> | ||||
|           <path d="M66.60987,10.19873h.85547v.69043h.06641a1.22092,1.22092,0,0,1,1.21582-.76514,1.86836,1.86836,0,0,1,.39648.03711v.877a2.43442,2.43442,0,0,0-.49609-.05371A1.05507,1.05507,0,0,0,67.49855,12.043v2.65869h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M69.96144,15.15234h.90918c.0752.32666.45117.5376,1.05078.5376.74023,0,1.17871-.35156,1.17871-.94678v-.86426H73.0337a1.51433,1.51433,0,0,1-1.38965.75635c-1.14941,0-1.86035-.88867-1.86035-2.23682,0-1.373.71875-2.27441,1.86914-2.27441a1.56045,1.56045,0,0,1,1.41406.79395h.07031v-.71924h.85156v4.54c0,1.02979-.80664,1.68311-2.08008,1.68311C70.7837,16.42188,70.05616,15.91748,69.96144,15.15234Zm3.15527-2.7583c0-.897-.46387-1.47168-1.2207-1.47168-.76465,0-1.19434.57471-1.19434,1.47168,0,.89746.42969,1.47217,1.19434,1.47217C72.65773,13.86621,73.11671,13.2959,73.11671,12.394Z" style="fill: #fff"/> | ||||
|           <path d="M79.21241,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H76.08644v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,76.08644,12.03467Z" style="fill: #fff"/> | ||||
|           <path d="M80.45948,10.19873H81.315v.69043h.06641a1.22092,1.22092,0,0,1,1.21582-.76514,1.86836,1.86836,0,0,1,.39648.03711v.877a2.43442,2.43442,0,0,0-.49609-.05371A1.05507,1.05507,0,0,0,81.34815,12.043v2.65869h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M86.19581,12.44824c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.44092h.88867v6.26074h-.85156v-.71143H89.479a1.56284,1.56284,0,0,1-1.41406.78564C86.91944,14.77588,86.19581,13.87451,86.19581,12.44824Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C87.56886,10.92236,87.11378,11.501,87.11378,12.44824Z" style="fill: #fff"/> | ||||
|           <path d="M91.60206,13.42822c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031V11.625c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,91.60206,13.42822Zm2.89453-.38477V12.667L93.397,12.7373c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,94.49659,13.04346Z" style="fill: #fff"/> | ||||
|           <path d="M96.773,10.19873h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00977c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428H96.773Z" style="fill: #fff"/> | ||||
|           <path d="M103.61769,10.11182c1.0127,0,1.6748.47119,1.76172,1.26514h-.85254c-.082-.33057-.40527-.5415-.90918-.5415-.49609,0-.873.23535-.873.58691,0,.269.22754.43848.71582.55029l.748.17334c.85645.19873,1.25781.56689,1.25781,1.22852,0,.84766-.79,1.41406-1.86523,1.41406-1.07129,0-1.76953-.48389-1.84863-1.28174h.88965a.91365.91365,0,0,0,.97949.562c.55371,0,.94727-.248.94727-.60791,0-.26855-.21094-.44238-.66211-.5498l-.78516-.18213c-.85645-.20264-1.25293-.58691-1.25293-1.25684C101.86866,10.67383,102.60011,10.11182,103.61769,10.11182Z" style="fill: #fff"/> | ||||
|         </g> | ||||
|       </g> | ||||
|     </g> | ||||
|     <g> | ||||
|       <path d="M35.19825,18.06689h1.85938V30.48535H35.19825Z" style="fill: #fff"/> | ||||
|       <path d="M39.29786,22.61084l1.01563-4.54395h1.80664l-1.23047,4.54395Z" style="fill: #fff"/> | ||||
|       <path d="M49.14649,27.12891H44.4131l-1.13672,3.35645H41.27149l4.4834-12.41846h2.083l4.4834,12.41846H50.28224Zm-4.24316-1.54883h3.752l-1.84961-5.44775h-.05176Z" style="fill: #fff"/> | ||||
|       <path d="M62.00294,25.959c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.43115h1.79883V22.937h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C60.48829,21.33643,62.00294,23.15283,62.00294,25.959Zm-1.91016,0c0-1.8335-.94727-3.03857-2.39258-3.03857-1.41992,0-2.375,1.23047-2.375,3.03857,0,1.82422.95508,3.0459,2.375,3.0459C59.14552,29.00488,60.09278,27.80859,60.09278,25.959Z" style="fill: #fff"/> | ||||
|       <path d="M71.9673,25.959c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438H63.43946V21.43115H65.2378V22.937H65.272a3.21162,3.21162,0,0,1,2.88281-1.60059C70.45265,21.33643,71.9673,23.15283,71.9673,25.959Zm-1.91016,0c0-1.8335-.94727-3.03857-2.39258-3.03857-1.41992,0-2.375,1.23047-2.375,3.03857,0,1.82422.95508,3.0459,2.375,3.0459C69.10987,29.00488,70.05714,27.80859,70.05714,25.959Z" style="fill: #fff"/> | ||||
|       <path d="M78.55323,27.02539c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.38818C78.03663,24.271,76.978,23.20459,76.978,21.47412c0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426H84.104c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.62646,3.60645,3.44287,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/> | ||||
|       <path d="M90.19,19.28857v2.14258h1.72168v1.47168H90.19v4.9917c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.90283H87.00636V21.43115h1.31641V19.28857Z" style="fill: #fff"/> | ||||
|       <path d="M92.90773,25.959c0-2.84912,1.67773-4.63916,4.29395-4.63916,2.625,0,4.29492,1.79,4.29492,4.63916,0,2.85645-1.66113,4.63867-4.29492,4.63867C94.56886,30.59766,92.90773,28.81543,92.90773,25.959Zm6.69531,0c0-1.95459-.89551-3.10791-2.40137-3.10791s-2.40039,1.16211-2.40039,3.10791c0,1.96191.89453,3.10645,2.40039,3.10645S99.603,27.9209,99.603,25.959Z" style="fill: #fff"/> | ||||
|       <path d="M103.02882,21.43115h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934V23.144a2.59794,2.59794,0,0,0-.835-.1123,1.8728,1.8728,0,0,0-1.93652,2.0835v5.37012h-1.8584Z" style="fill: #fff"/> | ||||
|       <path d="M116.22608,27.82617c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.84033,1.64355-4.68213,4.19043-4.68213,2.50488,0,4.08008,1.7207,4.08008,4.46631v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344ZM109.94386,25.124h4.52637a2.17744,2.17744,0,0,0-2.2207-2.29834A2.29214,2.29214,0,0,0,109.94386,25.124Z" style="fill: #fff"/> | ||||
|     </g> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										49
									
								
								note_kfet/static/img/playstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								note_kfet/static/img/playstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg id="artwork" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 135 40"> | ||||
|   <!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141)  --> | ||||
|   <defs> | ||||
|     <style> | ||||
|       .st0 { | ||||
|         fill: #4285f4; | ||||
|       } | ||||
|  | ||||
|       .st1 { | ||||
|         fill: #a6a6a6; | ||||
|       } | ||||
|  | ||||
|       .st2 { | ||||
|         fill: #34a853; | ||||
|       } | ||||
|  | ||||
|       .st3 { | ||||
|         fill: #fbbc04; | ||||
|       } | ||||
|  | ||||
|       .st4, .st5 { | ||||
|         fill: #fff; | ||||
|       } | ||||
|  | ||||
|       .st6 { | ||||
|         fill: #ea4335; | ||||
|       } | ||||
|  | ||||
|       .st5 { | ||||
|         font-family: GoogleSans-Medium, 'Google Sans'; | ||||
|         font-size: 8.7px; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|     </style> | ||||
|   </defs> | ||||
|   <g> | ||||
|     <rect width="135" height="40" rx="5" ry="5"/> | ||||
|     <path class="st1" d="M130,.8c2.316,0,4.2,1.884,4.2,4.2v30c0,2.316-1.884,4.2-4.2,4.2H5c-2.316,0-4.2-1.884-4.2-4.2V5C.8,2.684,2.684.8,5,.8h125M130,0H5C2.25,0,0,2.25,0,5v30c0,2.75,2.25,5,5,5h125c2.75,0,5-2.25,5-5V5C135,2.25,132.75,0,130,0h0Z"/> | ||||
|     <path class="st4" d="M68.136,21.752c-2.352,0-4.269,1.788-4.269,4.253,0,2.449,1.917,4.253,4.269,4.253s4.269-1.804,4.269-4.253c0-2.465-1.917-4.253-4.269-4.253ZM68.136,28.583c-1.289,0-2.4-1.063-2.4-2.578,0-1.531,1.112-2.578,2.4-2.578s2.4,1.047,2.4,2.578c0,1.514-1.112,2.578-2.4,2.578ZM58.822,21.752c-2.352,0-4.269,1.788-4.269,4.253,0,2.449,1.917,4.253,4.269,4.253s4.269-1.804,4.269-4.253c0-2.465-1.917-4.253-4.269-4.253ZM58.822,28.583c-1.289,0-2.4-1.063-2.4-2.578,0-1.531,1.112-2.578,2.4-2.578s2.4,1.047,2.4,2.578c0,1.514-1.112,2.578-2.4,2.578ZM47.744,23.057v1.804h4.318c-.129,1.015-.467,1.756-.983,2.272-.628.628-1.611,1.321-3.335,1.321-2.658,0-4.736-2.143-4.736-4.801s2.078-4.801,4.736-4.801c1.434,0,2.481.564,3.254,1.289l1.273-1.273c-1.079-1.031-2.513-1.82-4.527-1.82-3.641,0-6.702,2.964-6.702,6.605s3.061,6.605,6.702,6.605c1.965,0,3.448-.644,4.608-1.853,1.192-1.192,1.563-2.868,1.563-4.221,0-.419-.032-.805-.097-1.128h-6.074ZM93.052,24.458c-.354-.95-1.434-2.707-3.641-2.707-2.191,0-4.011,1.724-4.011,4.253,0,2.384,1.804,4.253,4.221,4.253,1.949,0,3.077-1.192,3.544-1.885l-1.45-.967c-.483.709-1.144,1.176-2.094,1.176s-1.627-.435-2.062-1.289l5.687-2.352-.193-.483ZM87.252,25.876c-.048-1.643,1.273-2.481,2.223-2.481.741,0,1.369.37,1.579.902l-3.802,1.579ZM82.628,30h1.869v-12.502h-1.869v12.502ZM79.567,22.702h-.064c-.419-.499-1.224-.951-2.239-.951-2.127,0-4.076,1.869-4.076,4.269,0,2.384,1.949,4.237,4.076,4.237,1.015,0,1.82-.451,2.239-.967h.064v.612c0,1.627-.87,2.497-2.272,2.497-1.144,0-1.853-.822-2.143-1.514l-1.627.677c.467,1.128,1.708,2.513,3.77,2.513,2.191,0,4.044-1.289,4.044-4.43v-7.636h-1.772v.693ZM77.425,28.583c-1.289,0-2.368-1.079-2.368-2.562,0-1.498,1.079-2.594,2.368-2.594,1.273,0,2.272,1.096,2.272,2.594,0,1.482-.999,2.562-2.272,2.562ZM101.806,17.499h-4.471v12.501h1.866v-4.736h2.605c2.068,0,4.101-1.497,4.101-3.883s-2.033-3.882-4.101-3.882ZM101.854,23.524h-2.654v-4.285h2.654c1.395,0,2.187,1.155,2.187,2.143,0,.969-.792,2.143-2.187,2.143ZM113.386,21.729c-1.351,0-2.75.595-3.329,1.914l1.657.692c.354-.692,1.013-.917,1.705-.917.965,0,1.946.579,1.962,1.608v.129c-.338-.193-1.061-.483-1.946-.483-1.785,0-3.603.981-3.603,2.815,0,1.673,1.463,2.75,3.104,2.75,1.254,0,1.946-.563,2.38-1.222h.064v.965h1.801v-4.793c0-2.22-1.657-3.458-3.796-3.458ZM113.16,28.58c-.611,0-1.464-.305-1.464-1.061,0-.965,1.061-1.335,1.978-1.335.82,0,1.206.177,1.705.418-.145,1.158-1.142,1.978-2.219,1.978ZM123.743,22.002l-2.139,5.42h-.064l-2.219-5.42h-2.01l3.329,7.575-1.898,4.214h1.946l5.131-11.789h-2.075ZM106.936,30h1.866v-12.501h-1.866v12.501Z"/> | ||||
|     <g> | ||||
|       <path class="st6" d="M20.717,19.424l-10.647,11.3s.001.005.002.007c.327,1.227,1.447,2.13,2.777,2.13.531,0,1.031-.144,1.459-.396l.034-.02,11.984-6.915-5.609-6.106Z"/> | ||||
|       <path class="st3" d="M31.488,17.5l-.01-.007-5.174-3-5.829,5.187,5.849,5.848,5.146-2.969c.902-.487,1.515-1.438,1.515-2.535,0-1.09-.604-2.036-1.498-2.525Z"/> | ||||
|       <path class="st0" d="M10.07,9.277c-.064.236-.098.484-.098.74v19.968c0,.256.033.504.098.739l11.013-11.011-11.013-10.436Z"/> | ||||
|       <path class="st2" d="M20.796,20.001l5.51-5.509-11.97-6.94c-.435-.261-.943-.411-1.486-.411-1.33,0-2.452.905-2.779,2.134,0,0,0,.002,0,.003l10.726,10.724Z"/> | ||||
|     </g> | ||||
|   </g> | ||||
|   <text class="st5" transform="translate(41.08 13.134)"><tspan x="0" y="0">DISPONIBLE SUR</tspan></text> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 4.2 KiB | 
| @@ -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 "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 #} | ||||
|     <script src="{% static "jquery/jquery.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 #} | ||||
|     <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 form.media %} | ||||
|         {{ form.media }} | ||||
|   | ||||
| @@ -39,6 +39,17 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|             <a href="{% url 'password_reset' %}" | ||||
|                 class="badge badge-light">{% trans 'Forgotten your password or username?' %}</a> | ||||
|         </form> | ||||
|  | ||||
|         <div class="text-center mt-4"> | ||||
|             <a href="https://apps.apple.com/fr/app/la-note-kfet/id6754661723" class="d-inline-block mx-1" aria-label="{% trans 'Download on the AppStore' %}" style="cursor: pointer;"> | ||||
|                 <img src="/static/img/appstore_badge_fr.svg" | ||||
|                      alt="{% trans 'Download on the AppStore' %}" style="height: 50px;"> | ||||
|             </a> | ||||
|             <a href="https://play.google.com/store/apps/details?id=org.crans.bde.note&hl=fr" class="d-inline-block mx-1" aria-label="{% trans 'Get it on Google Play' %}" style="cursor: pointer;"> | ||||
|                 <img src="/static/img/playstore_badge_fr.svg" | ||||
|                      alt="{% trans 'Get it on Google Play' %}" style="height: 50px;"> | ||||
|             </a> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
| @@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|             {% endblocktrans %} | ||||
|         </div> | ||||
|  | ||||
|         <form method="post"> | ||||
|         <form method="post" id="profile_form"> | ||||
|             {% csrf_token %} | ||||
|             {{ form|crispy }} | ||||
|             {{ profile_form|crispy }} | ||||
| @@ -31,3 +31,45 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     </div> | ||||
| </div> | ||||
| {% 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-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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user