diff --git a/apps/family/forms.py b/apps/family/forms.py
index 63b47f48..78c6d25f 100644
--- a/apps/family/forms.py
+++ b/apps/family/forms.py
@@ -3,7 +3,6 @@
from django import forms
from django.forms.widgets import NumberInput
-from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete
from .models import Challenge, FamilyMembership, User
@@ -19,3 +18,21 @@ class ChallengeUpdateForm(forms.ModelForm):
widgets = {
"points": NumberInput()
}
+
+
+class FamilyMembershipForm(forms.ModelForm):
+ class Meta:
+ model = FamilyMembership
+ fields = ('user', )
+
+ widgets = {
+ "user":
+ Autocomplete(
+ User,
+ attrs={
+ 'api_url': '/api/user/',
+ 'name_field': 'username',
+ 'placeholder': 'Nom ...',
+ },
+ )
+ }
diff --git a/apps/family/tables.py b/apps/family/tables.py
index de00b815..4172b975 100644
--- a/apps/family/tables.py
+++ b/apps/family/tables.py
@@ -4,7 +4,7 @@
import django_tables2 as tables
from django_tables2 import A
-from .models import Family, Challenge
+from .models import Family, Challenge, FamilyMembership
class FamilyTable(tables.Table):
@@ -43,3 +43,17 @@ class ChallengeTable(tables.Table):
model = Challenge
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'description', 'points',)
+
+
+class FamilyMembershipTable(tables.Table):
+ """
+ List all family memberships.
+ """
+ class Meta:
+ attrs = {
+ 'class': 'table table-condensed table-striped',
+ 'style': 'table-layout: fixed;'
+ }
+ template_name = 'django_tables2/bootstrap4.html'
+ fields = ('user',)
+ model = FamilyMembership
diff --git a/apps/family/templates/family/add_member.html b/apps/family/templates/family/add_member.html
new file mode 100644
index 00000000..6f77283d
--- /dev/null
+++ b/apps/family/templates/family/add_member.html
@@ -0,0 +1,60 @@
+{% extends "family/base.html" %}
+{% comment %}
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load crispy_forms_tags i18n pretty_money %}
+
+{% block profile_content %}
+
+{% endblock %}
+
+{% block extrajavascript %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/apps/family/templates/family/base.html b/apps/family/templates/family/base.html
new file mode 100644
index 00000000..56789907
--- /dev/null
+++ b/apps/family/templates/family/base.html
@@ -0,0 +1,72 @@
+{% extends "base.html" %}
+{% comment %}
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load i18n perms %}
+
+{# Use a fluid-width container #}
+{% block containertype %}container-fluid{% endblock %}
+
+{% block content %}
+
+
+ {% block profile_info %}
+
+
+
+ {% if user_object %}
+
+
+
+ {% elif club %}
+
+
+
+ {% endif %}
+
+
+ {% if user_object %}
+ {% include "member/includes/profile_info.html" %}
+ {% elif club %}
+ {% include "member/includes/club_info.html" %}
+ {% endif %}
+
+
+
+ {% endblock %}
+
+
+ {% block profile_content %}{% endblock %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/apps/family/templates/family/family_detail.html b/apps/family/templates/family/family_detail.html
index b8f9d918..1f5f8e56 100644
--- a/apps/family/templates/family/family_detail.html
+++ b/apps/family/templates/family/family_detail.html
@@ -1,5 +1,6 @@
-{% extends "base.html" %}
+{% extends "family/base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
+
diff --git a/apps/family/urls.py b/apps/family/urls.py
index 622d8b54..9a17a481 100644
--- a/apps/family/urls.py
+++ b/apps/family/urls.py
@@ -3,12 +3,14 @@
from django.urls import path
-from .views import FamilyListView, FamilyDetailView, ChallengeListView, ChallengeDetailView, ChallengeUpdateView
+from .views import FamilyListView, FamilyDetailView, FamilyUpdateView, FamilyAddMemberView, ChallengeListView, ChallengeDetailView, ChallengeUpdateView
app_name = 'family'
urlpatterns = [
path('list/', FamilyListView.as_view(), name="family_list"),
path('detail//', FamilyDetailView.as_view(), name="family_detail"),
+ path('update//', FamilyUpdateView.as_view(), name="family_update"),
+ path('/add_member', FamilyAddMemberView.as_view(), name="family_add_member"),
path('challenge/list/', ChallengeListView.as_view(), name="challenge_list"),
path('challenge/detail//', ChallengeDetailView.as_view(), name="challenge_detail"),
path('challenge/update//', ChallengeUpdateView.as_view(), name="challenge_update"),
diff --git a/apps/family/views.py b/apps/family/views.py
index 4b710681..6f6e3d48 100644
--- a/apps/family/views.py
+++ b/apps/family/views.py
@@ -13,7 +13,7 @@ from django.urls import reverse_lazy
from .models import Family, Challenge, FamilyMembership, User
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable
-from .forms import ChallengeUpdateForm
+from .forms import ChallengeUpdateForm, FamilyMembershipForm
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
@@ -49,6 +49,35 @@ class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = "family"
extra_context = {"title": _('Family detail')}
+ def get_context_data(self, **kwargs):
+ """
+ Add members list
+ """
+ context = super().get_context_data(**kwargs)
+
+ family = self.object
+
+ # member list
+ family_member = FamilyMembership.objects.filter(
+ family=family,
+ year=date.today().year,
+ ).filter(PermissionBackend.filter_queryset(self.request, FamilyMembership, "view"))\
+ .order_by("user__username").distinct("user__username")
+
+ membership_table = FamilyMembershipTable(data=family_member)
+ context['member_list'] = membership_table
+
+ # Check if the user has the right to create a membership, to display the button.
+ empty_membership = FamilyMembership(
+ family=family,
+ user=User.objects.first(),
+ year=date.today().year,
+ )
+ context["can_add_members"] = PermissionBackend()\
+ .has_perm(self.request.user, "family.add_membership", empty_membership)
+
+ return context
+
class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
@@ -59,6 +88,30 @@ class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
extra_context = {"title": _('Update family')}
+class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
+ """
+ Add a membership to a family
+ """
+ model = FamilyMembership
+ form_class = FamilyMembershipForm
+ template_name = 'family/add_member.html'
+ extra_context = {"title": _("Add a new member to the family")}
+
+ def get_sample_object(self):
+ if "family_pk" in self.kwargs:
+ family = Family.objects.get(pk=self.kwargs["family_pk"])
+ else:
+ family = FamilyMembership.objects.get(pk=self.kwargs["pk"]).family
+ return FamilyMembership(
+ user=self.request.user,
+ family=family,
+ year=date.today().year,
+ )
+
+ def get_success_url(self):
+ return reverse_lazy('family:family_detail', kwargs={'pk': self.object.family.id})
+
+
class ChallengeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create challenge
@@ -72,7 +125,7 @@ class ChallengeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
description="Sample challenge",
points=0,
)
-
+
def get_success_url(self):
return reverse_lazy('family:challenge_list')
@@ -103,7 +156,7 @@ class ChallengeDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["fields"] = [(
Challenge._meta.get_field(field).verbose_name.capitalize(),
value) for field, value in fields.items()]
- context["obtained"] = getattr(self.object, "obtained")
+ context["obtained"] = self.object.obtained
context["update"] = PermissionBackend.check_perm(self.request, "family.change_challenge")
return context
@@ -121,4 +174,4 @@ class ChallengeUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
- return reverse_lazy('family:challenge_detail', kwargs={'pk': self.object.pk})
\ No newline at end of file
+ return reverse_lazy('family:challenge_detail', kwargs={'pk': self.object.pk})