diff --git a/apps/activity/templates/activity/activity_detail.html b/apps/activity/templates/activity/activity_detail.html index 1a8d01ee..bb0fc57a 100644 --- a/apps/activity/templates/activity/activity_detail.html +++ b/apps/activity/templates/activity/activity_detail.html @@ -37,6 +37,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% render_table guests %}
+ {% endif %} {% endblock %} diff --git a/apps/activity/views.py b/apps/activity/views.py index 9ef0d4f2..7829a2ee 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -136,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) @@ -150,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() diff --git a/apps/member/tables.py b/apps/member/tables.py index 475c6eb2..afded61f 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -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 + "") 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', diff --git a/apps/member/templates/member/club_members.html b/apps/member/templates/member/club_members.html index bbeb875e..c1a1d2db 100644 --- a/apps/member/templates/member/club_members.html +++ b/apps/member/templates/member/club_members.html @@ -36,7 +36,13 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "There is no membership found with this pattern." %} {% endif %} + + {% endblock %} diff --git a/apps/member/views.py b/apps/member/views.py index bf6245f5..72ad446e 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, UpdateView, TemplateView from django.views.generic.edit import FormMixin from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView +from django_tables2.export.views import ExportMixin from rest_framework.authtoken.models import Token from api.viewsets import is_regex from note.models import Alias, NoteClub, NoteUser, Trust @@ -950,11 +951,12 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id}) -class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): +class ClubMembersListView(ExportMixin, ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = Membership table_class = MembershipTable template_name = "member/club_members.html" extra_context = {"title": _("Members of the club")} + export_formats = ["csv"] def get_queryset(self, **kwargs): qs = super().get_queryset().filter(club_id=self.kwargs["pk"]) @@ -986,6 +988,14 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV return qs.distinct() + def get_export_filename(self, export_format): + return "members.csv" + + def get_export_content_type(self, export_format): + if export_format == "csv": + return "text/csv" + return super().get_export_content_type(export_format) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = Club.objects.filter( diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 7946e66d..0acd11c4 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -4702,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, @@ -4852,7 +4868,8 @@ 259, 260, 263, - 265 + 265, + 330 ] } }, @@ -5201,6 +5218,7 @@ "permissions": [ 37, 41, + 42, 53, 54, 55, diff --git a/requirements.txt b/requirements.txt index c40bf0bb..20c53dfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,5 @@ 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