1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-09-29 21:03:32 +02:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Otthorn
717501bdd7 Merge branch 'qrcode' into 'main'
Draft: Qrcode

See merge request bde/nk20!196
2025-08-30 01:39:43 +02:00
Nicolas Margulies
e6f3084588 Added a first pass for automatically entering an activity with a qrcode 2023-10-11 18:01:51 +02:00
otthorn
145e55da75 remove useless comment 2022-03-22 15:06:04 +01:00
otthorn
d3ba95cdca Insecable space for more clarity 2022-03-22 15:04:41 +01:00
otthorn
8ffb0ebb56 Use DetailView 2022-03-22 14:59:01 +01:00
otthorn
5038af9e34 Final html template 2022-03-22 14:58:26 +01:00
otthorn
819b4214c9 Add QRCode View, URL and test template 2022-03-22 12:26:44 +01:00
otthorn
b8a93b0b75 Add link to QR code 2022-03-19 16:25:15 +01:00
32 changed files with 130 additions and 516 deletions

View File

@@ -37,11 +37,6 @@ 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 %}

View File

@@ -38,6 +38,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a>
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<button id="trigger" class="btn btn-secondary">Click me !</button>
<hr>
@@ -63,15 +64,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
refreshBalance();
}
function process_qrcode() {
let name = alias_obj.val();
$.get("/api/note/note?search=" + name + "&format=json").done(
function (res) {
let note = res.results[0];
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: note.id,
guest: null
}).done(function () {
addMsg(interpolate(gettext(
"Entry made for %s whose balance is %s €"),
[note.name, note.balance / 100]), "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
}
alias_obj.keyup(function(event) {
let code = event.originalEvent.keyCode
if (65 <= code <= 122 || code === 13) {
debounce(reloadTable)()
}
if (code === 0)
process_qrcode();
});
$(document).ready(init);
alias_obj2 = document.getElementById("alias");
$("#trigger").click(function (e) {
addMsg("Clicked", "success", 1000);
alias_obj.val(alias_obj.val() + "\0");
alias_obj2.dispatchEvent(new KeyboardEvent('keyup'));
})
function init() {
$(".table-row").click(function (e) {
let target = e.target.parentElement;

View File

@@ -1,4 +1,4 @@
{% extends "base_search.html" %}
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
@@ -44,8 +44,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3 class="card-header text-center">
{% trans "All activities" %}
</h3>
{% render_table all %}
{% render_table table %}
</div>
{{ block.super }}
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n perms pretty_money dict_get %}
{% load i18n perms pretty_money %}
{% url 'activity:activity_detail' activity.pk as activity_detail_url %}
<div id="activity_info" class="card bg-light shadow mb-3">
@@ -53,12 +53,6 @@ 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">

View File

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

View File

@@ -67,65 +67,32 @@ 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):
"""
Filter the user list with the given pattern.
"""
return super().get_queryset().distinct()
return super().get_queryset(**kwargs).distinct()
def get_tables_data(self):
# 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'))
# first table = all activities, second table = upcoming
return [
self.get_queryset().order_by("-date_start"),
Activity.objects.filter(date_end__gt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
.distinct()
.order_by("date_start"),
search_table,
.order_by("date_start")
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tables = context["tables"]
for name, table in zip(["all", "upcoming", "table"], tables):
for name, table in zip(["table", "upcoming"], 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
@@ -136,19 +103,12 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
model = Activity
context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
export_formats = ["csv"]
tables = [
GuestTable,
OpenerTable,
lambda data: GuestTable(data, prefix="guests-"),
lambda data: OpenerTable(data, prefix="opener-"),
]
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)
@@ -157,51 +117,6 @@ 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()
@@ -222,14 +137,6 @@ 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

View File

@@ -2,7 +2,6 @@
# 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
@@ -11,25 +10,10 @@ 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', 'qr_code_numbers', 'allergens', 'date', 'expiry_date')
fields = ('name', 'owner', 'allergens', 'expiry_date')
row_attrs = {
'class': 'table-row',
'data-href': lambda record: 'detail/' + str(record.pk),

View File

@@ -34,12 +34,6 @@ 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>
@@ -120,26 +114,7 @@ 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) {

View File

@@ -65,24 +65,16 @@ 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__note__alias__name{suffix}': prefix + pattern}))
| Q(**{f'owner__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().filter(
open_table = self.get_queryset().order_by('expiry_date').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(
@@ -103,7 +95,6 @@ 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):
@@ -227,7 +218,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()

View File

@@ -10,7 +10,6 @@ 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
@@ -46,11 +45,6 @@ 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"))
@@ -78,12 +72,7 @@ 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
instance = super().save(commit=False)
if instance.phone_number:
instance.phone_number = instance.phone_number.as_e164
if commit:
instance.save()
return instance
return super().save(commit)
class Meta:
model = Profile

View File

@@ -1,8 +1,6 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
def save_user_profile(instance, created, raw, **_kwargs):
"""
@@ -18,7 +16,7 @@ def save_user_profile(instance, created, raw, **_kwargs):
def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs):
if not hasattr(instance, "_no_signal") and 'wei' in settings.INSTALLED_APPS and created:
if not hasattr(instance, "_no_signal") and created:
from wei.models import WEIRegistration
if instance.club.id == 1 or instance.club.id == 2:
registrations = WEIRegistration.objects.filter(
@@ -31,8 +29,8 @@ def update_wei_registration_fee_on_membership_creation(sender, instance, created
def update_wei_registration_fee_on_club_change(sender, instance, **kwargs):
if not hasattr(instance, "_no_signal") and 'wei' in settings.INSTALLED_APPS and (instance.id == 1 or instance.id == 2):
from wei.models import WEIRegistration
from wei.models import WEIRegistration
if not hasattr(instance, "_no_signal") and (instance.id == 1 or instance.id == 2):
registrations = WEIRegistration.objects.filter(
wei__year=instance.membership_start.year,
)

View File

@@ -92,20 +92,6 @@ 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
@@ -163,16 +149,6 @@ 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',

View File

@@ -36,13 +36,7 @@ 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 %}

View File

@@ -7,7 +7,6 @@
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.username }}</dd>
{% if family_app_installed %}
<dt class="col-xl-6">{% trans 'family'|capfirst %}</dt>
<dd class="col-xl-6">
{% if families %}
@@ -18,7 +17,6 @@
<span class="text-muted">Aucune</span>
{% endif %}
</dd>
{% endif %}
{% if user_object.pk == user.pk %}
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
@@ -73,7 +71,10 @@
{% if user_object.pk == user.pk %}
<div class="text-center">
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
<i class="fa fa-cogs"></i>{% trans 'API token' %}
<i class="fa fa-cogs"></i>&nbsp;{% trans 'API token' %}
</a>
<a class="small badge badge-secondary" href="{% url 'member:qr_code' user_object.pk %}">
<i class="fa fa-qrcode"></i>&nbsp;{% trans 'QR Code' %}
</a>
</div>
{% endif %}

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }}
</h3>
<div class="card-body">
<form method="post" id="profile-form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
{{ profile_form | crispy }}
@@ -21,45 +21,3 @@ 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 %}

View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{% trans "QR Code for" %} {{ user_object.username }} ({{ user_object.first_name }} {{user_object.last_name }})
</h3>
<div class="text-center" id="qrcode">
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
var qrc = new QRCode(document.getElementById("qrcode"), {
text: "{{ user_object.pk }}\0",
width: 1024,
height: 1024
});
</script>
{% endblock %}
{% block extracss %}
<style>
img {
width: 100%
}
</style>
{% endblock %}

View File

@@ -25,4 +25,5 @@ urlpatterns = [
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
path('user/<int:pk>/qr_code/', views.QRCodeView.as_view(), name='qr_code'),
]

View File

@@ -17,7 +17,6 @@ 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
@@ -208,10 +207,9 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
if 'family' in settings.INSTALLED_APPS:
context["family_app_installed"] = True
families = Family.objects.filter(memberships__user=user).distinct()
context["families"] = families
families = Family.objects.filter(memberships__user=user).distinct()
context["families"] = families
return context
@@ -408,6 +406,14 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
return context
class QRCodeView(LoginRequiredMixin, DetailView):
"""
Affiche le QR Code
"""
model = User
context_object_name = "user_object"
template_name = "member/qr_code.html"
extra_context = {"title": _("QR Code")}
# ******************************* #
# CLUB #
@@ -951,12 +957,11 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubMembersListView(ExportMixin, ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
class ClubMembersListView(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"])
@@ -988,14 +993,6 @@ class ClubMembersListView(ExportMixin, ProtectQuerysetMixin, LoginRequiredMixin,
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(

View File

@@ -67,8 +67,6 @@ $(document).ready(function () {
last.quantity = 1
if (last.note.club) {
$('#last_name').val(last.note.name)
$('#first_name').val(last.note.name)
@@ -113,7 +111,6 @@ $(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)
@@ -137,7 +134,6 @@ $(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)
@@ -166,7 +162,6 @@ $(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)

View File

@@ -4430,22 +4430,6 @@
"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,
@@ -4702,22 +4686,6 @@
"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,
@@ -4865,11 +4833,7 @@
221,
247,
258,
259,
260,
263,
265,
330
259
]
}
},
@@ -4881,6 +4845,7 @@
"name": "Pr\u00e9sident\u22c5e de club",
"permissions": [
62,
135,
142
]
}
@@ -5157,8 +5122,7 @@
289,
290,
291,
293,
298
293
]
}
},
@@ -5218,7 +5182,6 @@
"permissions": [
37,
41,
42,
53,
54,
55,
@@ -5270,9 +5233,7 @@
168,
176,
177,
197,
311,
319
197
]
}
},
@@ -5352,8 +5313,7 @@
289,
290,
291,
293,
298
293
]
}
},

View File

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

View File

@@ -308,12 +308,6 @@ 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")
@@ -344,7 +338,7 @@ class SogeCredit(models.Model):
credit_transaction.save()
credit_transaction.refresh_from_db()
self.credit_transaction = credit_transaction
elif not self.valid_legacy:
elif not self.valid:
self.credit_transaction.amount = self.amount
self.credit_transaction._force_save = True
self.credit_transaction.save()
@@ -352,12 +346,12 @@ class SogeCredit(models.Model):
return super().save(*args, **kwargs)
@property
def valid_legacy(self):
def valid(self):
return self.credit_transaction and self.credit_transaction.valid
@property
def amount(self):
if self.valid_legacy:
if self.valid:
return self.credit_transaction.total
amount = 0
transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False)
@@ -371,7 +365,7 @@ class SogeCredit(models.Model):
The Sogé credit may be created after the user already paid its memberships.
We query transactions and update the credit, if it is unvalid.
"""
if self.valid_legacy or not self.pk:
if self.valid or not self.pk:
return
# Soge do not pay BDE and kfet memberships since 2022
@@ -411,7 +405,7 @@ class SogeCredit(models.Model):
Invalidating a Société générale delete the transaction of the bank if it was already created.
Treasurers must know what they do, With Great Power Comes Great Responsibility...
"""
if self.valid_legacy:
if self.valid:
self.credit_transaction.valid = False
self.credit_transaction.save()
for tr in self.transactions.all():
@@ -420,7 +414,7 @@ class SogeCredit(models.Model):
tr.save()
def validate(self, force=False):
if self.valid_legacy and not force:
if self.valid and not force:
# The credit is already done
return

View File

@@ -17,7 +17,7 @@ from ...models import WEIMembership, Bus
WORDS = {
'list': [
'Fiesta', 'Graillance', 'Move it move it', 'Calme', 'Nerd et geek', 'Jeux de rôles et danse rock',
'Fiesta', 'Graillance', 'Move it move it', 'Calme', 'Nert 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: "Un kebab",
45: "",
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,33 +176,7 @@ 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 = {
@@ -261,7 +235,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]
elif information.step <= len(WORDS['questions']):
else:
questions = list(WORDS['questions'].items())
idx = information.step - 1
if idx < len(questions):
@@ -277,15 +251,6 @@ 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']
@@ -412,7 +377,7 @@ class WEISurvey2025(WEISurvey):
setattr(self.information, "word" + str(i), word)
self.information.step += 1
self.save()
elif 1 <= self.information.step <= len(WORDS['questions']):
else:
questions = list(WORDS['questions'].keys())
idx = self.information.step - 1
if idx < len(questions):
@@ -420,13 +385,6 @@ 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):
@@ -436,7 +394,7 @@ class WEISurvey2025(WEISurvey):
"""
The survey is complete once the bus is chosen.
"""
return self.information.step > len(WORDS['questions']) + 1
return self.information.step > len(WORDS['questions'])
@classmethod
@lru_cache()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }}
</h3>
<div class="card-body">
<form id="registration-form" method="post">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
{{ membership_form|crispy }}
@@ -22,46 +22,6 @@ 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 () {

View File

@@ -53,11 +53,9 @@ class TestWEIAlgorithm(TestCase):
birth_date='2000-01-01',
)
information = WEISurveyInformation2025(registration)
for j in range(1, 1 + NB_WORDS):
for j in range(1, 21):
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']) + 2
information.step = 20
information.save(registration)
registration.save()
@@ -89,7 +87,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']) + 2
information.step = len(WORDS['questions']) + 1
information.save(registration)
registration.save()
survey = WEISurvey2025(registration)

View File

@@ -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 "Fonder une famille"
msgstr "Ajouter une famille"
#: apps/family/templates/family/manage.html:101
msgid "Add a challenge"

View File

@@ -306,8 +306,8 @@ PIC_WIDTH = 200
PIC_RATIO = 1
# Custom phone number format
PHONENUMBER_DB_FORMAT = 'E164'
PHONENUMBER_DEFAULT_REGION = None
PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR'
# We add custom information to CAS, in order to give a normalized name to other services
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'

View File

@@ -30,8 +30,6 @@ 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>
@@ -43,8 +41,6 @@ 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 }}

View File

@@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %}
</div>
<form method="post" id="profile_form">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
@@ -31,45 +31,3 @@ 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 %}

View File

@@ -18,5 +18,4 @@ 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