diff --git a/apps/family/forms.py b/apps/family/forms.py
index 78c6d25f..8a36d289 100644
--- a/apps/family/forms.py
+++ b/apps/family/forms.py
@@ -5,7 +5,7 @@ from django import forms
from django.forms.widgets import NumberInput
from note_kfet.inputs import Autocomplete
-from .models import Challenge, FamilyMembership, User
+from .models import Challenge, FamilyMembership, User, Family
class ChallengeUpdateForm(forms.ModelForm):
@@ -36,3 +36,9 @@ class FamilyMembershipForm(forms.ModelForm):
},
)
}
+
+
+class FamilyUpdateForm(forms.ModelForm):
+ class Meta:
+ model = Family
+ fields = ('description', )
\ No newline at end of file
diff --git a/apps/family/migrations/0002_family_display_image.py b/apps/family/migrations/0002_family_display_image.py
new file mode 100644
index 00000000..d2cf118b
--- /dev/null
+++ b/apps/family/migrations/0002_family_display_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.23 on 2025-07-17 15:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('family', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='family',
+ name='display_image',
+ field=models.ImageField(default='pic/default.png', max_length=255, upload_to='pic/', verbose_name='display image'),
+ ),
+ ]
diff --git a/apps/family/models.py b/apps/family/models.py
index 46e8af47..1d2d0d34 100644
--- a/apps/family/models.py
+++ b/apps/family/models.py
@@ -28,6 +28,15 @@ class Family(models.Model):
verbose_name=_('rank'),
)
+ display_image = models.ImageField(
+ verbose_name=_('display image'),
+ max_length=255,
+ blank=False,
+ null=False,
+ upload_to='pic/',
+ default='pic/default.png'
+ )
+
class Meta:
verbose_name = _('Family')
verbose_name_plural = _('Families')
diff --git a/apps/family/templates/family/base.html b/apps/family/templates/family/base.html
index 56789907..444dffed 100644
--- a/apps/family/templates/family/base.html
+++ b/apps/family/templates/family/base.html
@@ -13,29 +13,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% 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 %}
+ {% include "family/family_info.html" %}
{% endblock %}
diff --git a/apps/family/templates/family/family_detail.html b/apps/family/templates/family/family_detail.html
index 1f5f8e56..a1db566f 100644
--- a/apps/family/templates/family/family_detail.html
+++ b/apps/family/templates/family/family_detail.html
@@ -3,4 +3,14 @@
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
+{% load render_table from django_tables2 %}
+{% load i18n perms %}
+{% block profile_content %}
+
+
+ {% render_table member_list %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/apps/family/templates/family/family_info.html b/apps/family/templates/family/family_info.html
new file mode 100644
index 00000000..359fe6ef
--- /dev/null
+++ b/apps/family/templates/family/family_info.html
@@ -0,0 +1,15 @@
+{% load i18n pretty_money perms %}
+
+
+ - {% trans 'name'|capfirst %}
+ - {{ family.name }}
+
+ - {% trans 'description'|capfirst %}
+ - {{ family.description }}
+
+ - {% trans 'score'|capfirst %}
+ - {{ family.score }}
+
+ - {% trans 'rank'|capfirst %}
+ - {{ family.rank }}
+
diff --git a/apps/family/templates/family/family_update.html b/apps/family/templates/family/family_update.html
new file mode 100644
index 00000000..27c7bed2
--- /dev/null
+++ b/apps/family/templates/family/family_update.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+{% comment %}
+Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load i18n crispy_forms_tags %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/apps/family/templates/family/picture_update.html b/apps/family/templates/family/picture_update.html
new file mode 100644
index 00000000..e5c6749c
--- /dev/null
+++ b/apps/family/templates/family/picture_update.html
@@ -0,0 +1,118 @@
+{% extends "family/base.html" %}
+{% comment %}
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load i18n crispy_forms_tags %}
+
+{% block profile_content %}
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extracss %}
+
+{% endblock %}
+
+{% block extrajavascript%}
+
+
+
+{% endblock %}
diff --git a/apps/family/urls.py b/apps/family/urls.py
index 9a17a481..e86bc0b5 100644
--- a/apps/family/urls.py
+++ b/apps/family/urls.py
@@ -3,14 +3,15 @@
from django.urls import path
-from .views import FamilyListView, FamilyDetailView, FamilyUpdateView, FamilyAddMemberView, ChallengeListView, ChallengeDetailView, ChallengeUpdateView
+from .views import FamilyListView, FamilyDetailView, FamilyUpdateView, FamilyPictureUpdateView, 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('update_pic//', FamilyPictureUpdateView.as_view(), name="update_pic"),
+ 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 6f6e3d48..75a82e0c 100644
--- a/apps/family/views.py
+++ b/apps/family/views.py
@@ -3,7 +3,9 @@
from datetime import date
+from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
+from django.db import transaction
from django.views.generic import DetailView, UpdateView
from django.utils.translation import gettext_lazy as _
from django_tables2 import SingleTableView
@@ -13,7 +15,9 @@ from django.urls import reverse_lazy
from .models import Family, Challenge, FamilyMembership, User
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable
-from .forms import ChallengeUpdateForm, FamilyMembershipForm
+from .forms import ChallengeUpdateForm, FamilyMembershipForm, FamilyUpdateForm
+from member.forms import ImageForm
+from member.views import PictureUpdateView
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
@@ -62,9 +66,12 @@ class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
family=family,
year=date.today().year,
).filter(PermissionBackend.filter_queryset(self.request, FamilyMembership, "view"))\
- .order_by("user__username").distinct("user__username")
+ .order_by("user__username")
+ family_member = family_member.distinct("user__username")\
+ if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else family_member
- membership_table = FamilyMembershipTable(data=family_member)
+ membership_table = FamilyMembershipTable(data=family_member, prefix="membership-")
+ membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
context['member_list'] = membership_table
# Check if the user has the right to create a membership, to display the button.
@@ -85,8 +92,43 @@ class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
model = Family
context_object_name = "family"
+ form_class = FamilyUpdateForm
+ template_name = 'family/family_update.html'
extra_context = {"title": _('Update family')}
+ def get_success_url(self):
+ return reverse_lazy('family:family_detail', kwargs={'pk': self.object.pk})
+
+
+class FamilyPictureUpdateView(PictureUpdateView):
+ """
+ Update profile picture of the family
+ """
+ model = Family
+ extra_context = {"title": _("Update family picture")}
+ template_name = 'family/picture_update.html'
+
+ def get_success_url(self):
+ """Redirect to family page after upload"""
+ return reverse_lazy('family:family_detail', kwargs={'pk': self.object.id})
+
+ @transaction.atomic
+ def form_valid(self, form):
+ """
+ Save the image
+ """
+ image = form.cleaned_data['image']
+
+ if image is None:
+ image = "pic/default.png"
+ else:
+ # Rename as PNG or GIF
+ extension = image.name.split(".")[-1]
+ if extension == "gif":
+ image.name = "{}_pic.gif".format(self.object.pk)
+ else:
+ image.name = "{}_pic.png".format(self.object.pk)
+
class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
"""
@@ -108,6 +150,29 @@ class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
year=date.today().year,
)
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ form = context['form']
+
+ family = Family.objects.filter(PermissionBackend.filter_queryset(self.request, Family, "view"))\
+ .get(pk=self.kwargs['family_pk'])
+
+ context['family'] = family
+
+ return context
+
+ @transaction.atomic
+ def form_valid(self, form):
+ """
+ Create family membership, check that everythinf is good
+ """
+ family = Family.objects.filter(PermissionBackend.filter_queryset(self.request, Family, "view")) \
+ .get(pk=self.kwargs["family_pk"])
+
+ form.instance.family = family
+
+ return super().form_valid(form)
+
def get_success_url(self):
return reverse_lazy('family:family_detail', kwargs={'pk': self.object.family.id})