1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-22 16:56:48 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
4975c1ab6f Merge branch 'translations' into 'main'
Translations

See merge request bde/nk20!332
2025-07-19 18:29:18 +02:00
61999a31a5 Wei details 2025-07-19 18:04:14 +02:00
b217f7ceec Merge branch 'wei' into 'main'
Wei

See merge request bde/nk20!331
2025-07-19 17:27:20 +02:00
2755a5f7ab Minor fail 2025-07-19 17:10:25 +02:00
9ab4df94e6 Minor fixes 2025-07-19 16:55:07 +02:00
edb6abfff5 Add fee field to WEIRegistration to be able to sort on validation status 2025-07-19 16:24:25 +02:00
03c1bb41b6 First of many 2025-07-18 23:49:34 +02:00
43 changed files with 153 additions and 2118 deletions

View File

@ -19,10 +19,6 @@ if "activity" in settings.INSTALLED_APPS:
from activity.api.urls import register_activity_urls
register_activity_urls(router, 'activity')
if "family" in settings.INSTALLED_APPS:
from family.api.urls import register_family_urls
register_family_urls(router, 'family')
if "food" in settings.INSTALLED_APPS:
from food.api.urls import register_food_urls
register_food_urls(router, 'food')

View File

@ -1,46 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Family, FamilyMembership, Challenge, Achievement
class FamilySerializer(serializers.ModelSerializer):
"""
REST API Serializer for Family.
The djangorestframework plugin will analyse the model `Family` and parse all fields in the API.
"""
class Meta:
model = Family
fields = '__all__'
class FamilyMembershipSerializer(serializers.ModelSerializer):
"""
REST API Serializer for FamilyMembership.
The djangorestframework plugin will analyse the model `FamilyMembership` and parse all fields in the API.
"""
class Meta:
model = FamilyMembership
fields = '__all__'
class ChallengeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Challenge.
The djangorestframework plugin will analyse the model `Challenge` and parse all fields in the API.
"""
class Meta:
model = Challenge
fields = '__all__'
class AchievementSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Achievement.
The djangorestframework plugin will analyse the model `Achievement` and parse all fields in the API.
"""
class Meta:
model = Achievement
fields = '__all__'

View File

@ -1,20 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import FamilyViewSet, FamilyMembershipViewSet, ChallengeViewSet, AchievementViewSet, BatchAchievementsAPIView
def register_family_urls(router, path):
"""
Configure router for Family REST API
"""
router.register(path + '/family', FamilyViewSet)
router.register(path + '/familymembership', FamilyMembershipViewSet)
router.register(path + '/challenge', ChallengeViewSet)
router.register(path + '/achievement', AchievementViewSet)
urlpatterns = [
path('achievements/batch/', BatchAchievementsAPIView.as_view(), name='batch_achievements')
]

View File

@ -1,88 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .serializers import FamilySerializer, FamilyMembershipSerializer, ChallengeSerializer, AchievementSerializer
from ..models import Family, FamilyMembership, Challenge, Achievement
class FamilyViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Family` objects, serialize it to JSON with the given serializer,
then render it on /api/family/family/
"""
queryset = Family.objects.order_by('id')
serializer_class = FamilySerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class FamilyMembershipViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `FamilyMembership` objects, serialize it to JSON with the given serializer,
then render it on /api/family/familymembership/
"""
queryset = FamilyMembership.objects.order_by('id')
serializer_class = FamilyMembershipSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class ChallengeViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Challenge` objects, serialize it to JSON with the given serializer,
then render it on /api/family/challenge/
"""
queryset = Challenge.objects.order_by('id')
serializer_class = ChallengeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class AchievementViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Achievement` objects, serialize it to JSON with the given serializer,
then render it on /api/family/achievement/
"""
queryset = Achievement.objects.order_by('id')
serializer_class = AchievementSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class BatchAchievementsAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, format=None):
print("POST de la view spéciale")
family_ids = request.data.get('families', [])
challenge_ids = request.data.get('challenges', [])
families = Family.objects.filter(id__in=family_ids)
challenges = Challenge.objects.filter(id__in=challenge_ids)
for family in families:
for challenge in challenges:
a = Achievement(family=family, challenge=challenge)
a.save(update_score=False)
for family in families:
family.update_score()
Family.update_ranking()
return Response({'status': 'ok'}, status=status.HTTP_201_CREATED)

View File

@ -1,11 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FamilyConfig(AppConfig):
name = 'family'
verbose_name = _('family')

View File

@ -1,44 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.forms.widgets import NumberInput
from note_kfet.inputs import Autocomplete
from .models import Challenge, FamilyMembership, User, Family
class ChallengeForm(forms.ModelForm):
"""
To update a challenge
"""
class Meta:
model = Challenge
fields = ('name', 'description', 'points',)
widgets = {
"points": NumberInput()
}
class FamilyForm(forms.ModelForm):
class Meta:
model = Family
fields = ('name', 'description', )
class FamilyMembershipForm(forms.ModelForm):
class Meta:
model = FamilyMembership
fields = ('user', )
widgets = {
"user":
Autocomplete(
User,
attrs={
'api_url': '/api/user/',
'name_field': 'username',
'placeholder': 'Nom ...',
},
)
}

View File

@ -1,73 +0,0 @@
# Generated by Django 4.2.21 on 2025-07-06 16:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Challenge',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('description', models.CharField(max_length=255, verbose_name='description')),
('points', models.PositiveIntegerField(verbose_name='points')),
('obtained', models.PositiveIntegerField(default=0, verbose_name='obtained')),
],
options={
'verbose_name': 'challenge',
'verbose_name_plural': 'challenges',
},
),
migrations.CreateModel(
name='Family',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('description', models.CharField(max_length=255, verbose_name='description')),
('score', models.PositiveIntegerField(default=0, verbose_name='score')),
('rank', models.PositiveIntegerField(verbose_name='rank')),
],
options={
'verbose_name': 'Family',
'verbose_name_plural': 'Families',
},
),
migrations.CreateModel(
name='Achievement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('obtained_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='obtained at')),
('challenge', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challenge')),
('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.family', verbose_name='family')),
],
options={
'verbose_name': 'achievement',
'verbose_name_plural': 'achievements',
},
),
migrations.CreateModel(
name='FamilyMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.PositiveIntegerField(default=2025, verbose_name='year')),
('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='members', to='family.family', verbose_name='family')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='family_memberships', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'family membership',
'verbose_name_plural': 'family memberships',
'unique_together': {('user', 'year')},
},
),
]

View File

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

View File

@ -1,207 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models, transaction
from django.utils import timezone
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
class Family(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
unique=True,
)
description = models.CharField(
max_length=255,
verbose_name=_('description'),
)
score = models.PositiveIntegerField(
verbose_name=_('score'),
default=0,
)
rank = models.PositiveIntegerField(
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')
def __str__(self):
return self.name
def update_score(self, *args, **kwargs):
challenge_set = Challenge.objects.select_for_update().filter(achievement__family=self)
points_sum = challenge_set.aggregate(models.Sum("points"))
self.score = points_sum["points__sum"]
self.save()
self.update_ranking()
@staticmethod
def update_ranking(*args, **kwargs):
"""
Update ranking when adding or removing points
"""
family_set = Family.objects.select_for_update().all().order_by("-score")
for i in range(family_set.count()):
if i == 0 or family_set[i].score != family_set[i - 1].score:
new_rank = i + 1
family = family_set[i]
family.rank = new_rank
family._force_save = True
family.save()
def save(self, *args, **kwargs):
if self.rank is None:
last_family = Family.objects.order_by("rank").last()
if last_family is None or last_family.score > self.score:
self.rank = Family.objects.count() + 1
else:
self.rank = last_family.rank
super().save(*args, **kwargs)
class FamilyMembership(models.Model):
user = models.OneToOneField(
User,
on_delete=models.PROTECT,
related_name=_('family_memberships'),
verbose_name=_('user'),
)
family = models.ForeignKey(
Family,
on_delete=models.PROTECT,
related_name=_('members'),
verbose_name=_('family'),
)
year = models.PositiveIntegerField(
verbose_name=_('year'),
default=timezone.now().year,
)
class Meta:
unique_together = ('user', 'year',)
verbose_name = _('family membership')
verbose_name_plural = _('family memberships')
def __str__(self):
return _('Family membership of {user} to {family}').format(user=self.user.username, family=self.family.name, )
class Challenge(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
)
description = models.CharField(
max_length=255,
verbose_name=_('description'),
)
points = models.PositiveIntegerField(
verbose_name=_('points'),
)
obtained = models.PositiveIntegerField(
verbose_name=_('obtained'),
default=0,
)
@transaction.atomic
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Update families who already obtained this challenge
achievements = Achievement.objects.filter(challenge=self)
for achievement in achievements:
achievement.save()
class Meta:
verbose_name = _('challenge')
verbose_name_plural = _('challenges')
def __str__(self):
return self.name
class Achievement(models.Model):
challenge = models.ForeignKey(
Challenge,
on_delete=models.PROTECT,
)
family = models.ForeignKey(
Family,
on_delete=models.PROTECT,
verbose_name=_('family'),
)
obtained_at = models.DateTimeField(
verbose_name=_('obtained at'),
default=timezone.now,
)
class Meta:
verbose_name = _('achievement')
verbose_name_plural = _('achievements')
def __str__(self):
return _('Challenge {challenge} carried out by Family {family}').format(challenge=self.challenge.name, family=self.family.name, )
@transaction.atomic
def save(self, *args, update_score=True, **kwargs):
"""
When saving, also grants points to the family
"""
self.family = Family.objects.select_for_update().get(pk=self.family_id)
self.challenge = Challenge.objects.select_for_update().get(pk=self.challenge_id)
is_new = self.pk is None
super().save(*args, **kwargs)
if update_score:
self.family.refresh_from_db()
self.family.update_score()
# Count only when getting a new achievement
if is_new:
self.challenge.refresh_from_db()
self.challenge.obtained += 1
self.challenge._force_save = True
self.challenge.save()
@transaction.atomic
def delete(self, *args, **kwargs):
"""
When deleting, also removes points from the family
"""
# Get the family and challenge before deletion
self.family = Family.objects.select_for_update().get(pk=self.family_id)
# Delete the achievement
super().delete(*args, **kwargs)
# Remove points from the family
self.family.refresh_from_db()
self.family.update_score()
self.challenge.refresh_from_db()
self.challenge.obtained -= 1
self.challenge._force_save = True
self.challenge.save()

View File

@ -1,449 +0,0 @@
// Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks.
var LOCK = false
/**
* Refresh the history table on the consumptions page.
*/
function refreshHistory () {
$('#history').load('/family/manage/ #history')
}
$(document).ready(function () {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab('show')
} else {
$("a[data-toggle='tab']").first().tab('show')
}
// When selecting a category, change URL
$(document.body).on('click', "a[data-toggle='tab']", function () {
location.hash = this.getAttribute('href')
})
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
document.getElementById("consume_all").addEventListener('click', consumeAll)
})
notes = []
notes_display = []
buttons = []
// When the user searches an alias, we update the auto-completion
autoCompleteFamily('note', 'note_list', notes, notes_display,
'note', 'user_note', 'profile_pic', function () {
return true
})
/**
* Add a transaction from a button.
* @param fam Where the money goes
* @param amount The price of the item
* @param type The type of the transaction (content type id for RecurrentTransaction)
* @param category_id The category identifier
* @param category_name The category name
* @param template_id The identifier of the button
* @param template_name The name of the button
*/
function addChallenge (id, name, amount) {
var challenge = null
/** Ajout de 1 à chaque clic d'un bouton déjà choisi */
buttons.forEach(function (b) {
if (b.id === id) {
b.quantity += 1
challenge = b
}
})
if (challenge == null) {
challenge = {
id: id,
name: name,
quantity: 1,
amount: amount,
}
buttons.push(challenge)
}
const dc_obj = true
const list = 'consos_list'
let html = ''
buttons.forEach(function (challenge) {
html += li('conso_button_' + challenge.id, challenge.name +
'<span class="badge badge-dark badge-pill">' + challenge.quantity + '</span>')
})
document.getElementById(list).innerHTML = html
buttons.forEach((button) => {
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
if (LOCK) { return }
removeNote(button, 'conso_button', buttons, list)()
})
})
}
/**
* Reset the page as its initial state.
*/
function reset () {
console.log("reset lancée")
notes_display.length = 0
notes.length = 0
buttons.length = 0
document.getElementById('note_list').innerHTML = ''
document.getElementById('consos_list').innerHTML = ''
document.getElementById('note').value = ''
document.getElementById('note').dataset.originTitle = ''
$('#note').tooltip('hide')
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png'
document.getElementById('profile_pic_link').href = '#'
refreshHistory()
LOCK = false
}
/**
* Apply all transactions: all notes in `notes` buy each item in `buttons`
*/
function consumeAll () {
if (LOCK) { return }
LOCK = true
let error = false
if (notes_display.length === 0) {
// ... gestion erreur ...
error = true
}
if (buttons.length === 0) {
// ... gestion erreur ...
error = true
}
if (error) {
LOCK = false
return
}
// Récupérer les IDs des familles et des challenges
const family_ids = notes_display.map(fam => fam.id)
const challenge_ids = buttons.map(chal => chal.id)
$.ajax({
url: '/family/api/family/achievements/batch/',
type: 'POST',
data: JSON.stringify({
families: family_ids,
challenges: challenge_ids
}),
contentType: 'application/json',
headers: {
'X-CSRFToken': CSRF_TOKEN
},
success: function () {
reset()
addMsg("Défis validés pour les familles!", 'success', 5000)
},
error: function (e) {
reset()
addMsg("Erreur lors de la création des achievements.",'danger',5000)
}
})
}
/**
* Create a new achievement through the API.
* @param family The selected family
* @param challenge The selected challenge
*/
function grantAchievement (family, challenge) {
console.log("grant lancée",family,challenge)
$.post('/api/family/achievement/',
{
csrfmiddlewaretoken: CSRF_TOKEN,
family: family.id,
challenge: challenge.id,
})
.done(function () {
reset()
addMsg("Défi validé pour la famille!", 'success', 5000)
})
.fail(function (e) {
reset()
if (e.responseJSON) {
errMsg(e.responseJSON)
} else if (e.responseText) {
errMsg(e.responseText)
} else {
errMsg("Erreur inconnue lors de la création de l'achievement.")
}
})
}
var searchbar = document.getElementById("search-input")
var search_results = document.getElementById("search-results")
var old_pattern = null;
var firstMatch = null;
/**
* Updates the button search tab
* @param force Forces the update even if the pattern didn't change
*/
function updateSearch(force = false) {
let pattern = searchbar.value
if (pattern === "")
firstMatch = null;
if ((pattern === old_pattern || pattern === "") && !force)
return;
firstMatch = null;
const re = new RegExp(pattern, "i");
Array.from(search_results.children).forEach(function(b) {
if (re.test(b.innerText)) {
b.hidden = false;
if (firstMatch === null) {
firstMatch = b;
}
} else
b.hidden = true;
});
}
searchbar.addEventListener("input", function (e) {
debounce(updateSearch)()
});
searchbar.addEventListener("keyup", function (e) {
if (firstMatch && e.key === "Enter")
firstMatch.click()
});
function createshiny() {
const list_btn = document.querySelectorAll('.btn-outline-dark')
const shiny_class = list_btn[Math.floor(Math.random() * list_btn.length)].classList
shiny_class.replace('btn-outline-dark', 'btn-outline-dark-shiny')
}
createshiny()
/**
* Query the 20 first matched notes with a given pattern
* @param pattern The pattern that is queried
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/
function getMatchedFamilies (pattern, fun) {
$.getJSON('/api/family/family/?format=json&alias=' + pattern + '&search=family', fun)
}
/**
* Generate a <li> entry with a given id and text
*/
function li (id, text, extra_css) {
return '<li class="list-group-item py-1 px-2 d-flex justify-content-between align-items-center text-truncate ' +
(extra_css || '') + '"' + ' id="' + id + '">' + text + '</li>\n'
}
/**
* Génère un champ d'auto-complétion pour rechercher une famille par son nom (version simplifiée sans alias)
* @param field_id L'identifiant du champ texte où le nom est saisi
* @param family_list_id L'identifiant du bloc div où les familles sélectionnées sont affichées
* @param families Un tableau contenant les objets famille sélectionnés
* @param families_display Un tableau contenant les infos des familles sélectionnées : [nom, id, objet famille, quantité]
* @param family_prefix Le préfixe des <li> pour les familles sélectionnées
* @param user_family_field L'identifiant du champ qui affiche la famille survolée (optionnel)
* @param profile_pic_field L'identifiant du champ qui affiche la photo de la famille survolée (optionnel)
* @param family_click Fonction appelée lors du clic sur un nom. Si elle existe et ne retourne pas true, la famille n'est pas affichée.
*/
function autoCompleteFamily(field_id, family_list_id, families, families_display, family_prefix = 'family', user_family_field = null, profile_pic_field = null, family_click = null) {
const field = $('#' + field_id)
console.log("autoCompleteFamily commence")
// Configuration du tooltip
field.tooltip({
html: true,
placement: 'bottom',
title: 'Chargement...',
trigger: 'manual',
container: field.parent(),
fallbackPlacement: 'clockwise'
})
// Masquer le tooltip lors d'un clic ailleurs
$(document).click(function (e) {
if (!e.target.id.startsWith(family_prefix)) {
field.tooltip('hide')
}
})
let old_pattern = null
// Réinitialiser la recherche au clic
field.click(function () {
field.tooltip('hide')
field.removeClass('is-invalid')
field.val('')
old_pattern = ''
})
// Sur "Entrée", sélectionner la première famille
field.keypress(function (event) {
if (event.originalEvent.charCode === 13 && families.length > 0) {
const li_obj = field.parent().find('ul li').first()
displayFamily(families[0], families[0].name, user_family_field, profile_pic_field)
li_obj.trigger('click')
}
})
// Mise à jour des suggestions lors de la saisie
field.keyup(function (e) {
field.removeClass('is-invalid')
if (e.originalEvent.charCode === 13) { return }
const pattern = field.val()
if (pattern === old_pattern) { return }
old_pattern = pattern
families.length = 0
if (pattern === '') {
field.tooltip('hide')
families.length = 0
return
}
// Appel à l'API pour récupérer les familles correspondantes
$.getJSON('/api/family/family/?format=json&search=' + pattern,
function (results) {
if (pattern !== $('#' + field_id).val()) { return }
let matched_html = '<ul class="list-group list-group-flush">'
results.results.forEach(function (family) {
matched_html += li(family_prefix + '_' + family.id,
family.name,
'')
families.push(family)
})
matched_html += '</ul>'
field.attr('data-original-title', matched_html).tooltip('show')
results.results.forEach(function (family) {
const family_obj = $('#' + family_prefix + '_' + family.id)
family_obj.hover(function () {
displayFamily(family, family.name, user_family_field, profile_pic_field)
})
family_obj.click(function () {
var disp = null
families_display.forEach(function (d) {
if (d.id === family.id) {
d.quantity += 1
disp = d
}
})
if (disp == null) {
disp = {
name: family.name,
id: family.id,
family: family,
quantity: 1
}
families_display.push(disp)
}
if (family_click && !family_click()) { return }
const family_list = $('#' + family_list_id)
let html = ''
families_display.forEach(function (disp) {
html += li(family_prefix + '_' + disp.id,
disp.name +
'<span class="badge badge-dark badge-pill">' +
disp.quantity + '</span>',
'')
})
family_list.html(html)
field.tooltip('update')
families_display.forEach(function (disp) {
const line_obj = $('#' + family_prefix + '_' + disp.id)
line_obj.hover(function () {
displayFamily(disp.family, disp.name, user_family_field, profile_pic_field)
})
line_obj.click(removeFamily(disp, family_prefix, families_display, family_list_id, user_family_field,
profile_pic_field))
})
})
})
})
})
}
/**
* Affiche le nom et la photo d'une famille
* @param family L'objet famille à afficher
* @param user_family_field L'identifiant du champ où afficher le nom (optionnel)
* @param profile_pic_field L'identifiant du champ où afficher la photo (optionnel)
*/
function displayFamily(family, user_family_field = null, profile_pic_field = null) {
if (!family.display_image) {
family.display_image = '/static/member/img/default_picture.png'
}
if (user_family_field !== null) {
$('#' + user_family_field).removeAttr('class')
$('#' + user_family_field).text(family.name)
if (profile_pic_field != null) {
$('#' + profile_pic_field).attr('src', family.display_image)
// Si tu veux un lien vers la page famille :
$('#' + profile_pic_field + '_link').attr('href', '/family/detail/' + family.id + '/')
}
}
}
/**
* Retire une famille de la liste sélectionnée.
* @param d La famille à retirer
* @param family_prefix Le préfixe des <li>
* @param families_display Le tableau des familles sélectionnées
* @param family_list_id L'id du bloc où sont affichées les familles
* @param user_family_field Champ d'affichage (optionnel)
* @param profile_pic_field Champ photo (optionnel)
* @returns une fonction compatible avec les événements jQuery
*/
function removeFamily(d, family_prefix, families_display, family_list_id, user_family_field = null, profile_pic_field = null) {
return function () {
const new_families_display = []
let html = ''
families_display.forEach(function (disp) {
if (disp.quantity > 1 || disp.id !== d.id) {
disp.quantity -= disp.id === d.id ? 1 : 0
new_families_display.push(disp)
html += li(family_prefix + '_' + disp.id, disp.name +
'<span class="badge badge-dark badge-pill">' + disp.quantity + '</span>')
}
})
families_display.length = 0
new_families_display.forEach(function (disp) {
families_display.push(disp)
})
$('#' + family_list_id).html(html)
families_display.forEach(function (disp) {
const obj = $('#' + family_prefix + '_' + disp.id)
obj.click(removeFamily(disp, family_prefix, families_display, family_list_id, user_family_field, profile_pic_field))
obj.hover(function () {
displayFamily(disp.family, user_family_field, profile_pic_field)
})
})
}
}

View File

@ -1,92 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_tables2 import A
from .models import Family, Challenge, FamilyMembership, Achievement
class FamilyTable(tables.Table):
"""
List all families
"""
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Family
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'score', 'rank',)
order_by = ('rank',)
row_attrs = {
'class': 'table-row',
'data-href': lambda record: reverse('family:family_detail', args=[record.pk]),
'style': 'cursor:pointer',
}
class ChallengeTable(tables.Table):
"""
List all challenges
"""
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('id',)
model = Challenge
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'description', 'points',)
row_attrs = {
'class': 'table-row',
'data-href': lambda record: reverse('family:challenge_detail', args=[record.pk]),
'style': 'cursor:pointer',
}
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
class AchievementTable(tables.Table):
"""
List recent achievements.
"""
delete = tables.LinkColumn(
'family:achievement_delete',
args=[A('id')],
verbose_name=_("Delete"),
text=_("Delete"),
orderable=False,
attrs={
'th': {
'id': 'delete-membership-header'
},
'a': {
'class': 'btn btn-danger',
'data-type': 'delete-membership'
}
},
)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Achievement
fields = ('family', 'challenge', 'challenge__points', 'obtained_at', )
template_name = 'django_tables2/bootstrap4.html'
order_by = ('-obtained_at',)

View File

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-light">
<div class="card-header text-center">
<h4>{% trans "Delete achievement" %}</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
{% blocktrans %}Are you sure you want to delete this achievement? This action can't be undone.{% endblocktrans %}
</div>
</div>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<a class="btn btn-primary" href="{% url 'family:achievement_list' %}">{% trans "Return to achievements list" %}</a>
{% if not object.locked %}
<button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
{% endif %}
</form>
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n django_tables2 %}
{% block content %}
<div class="card mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Recent achievements history" %}
</p>
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:manage" %}">
{% trans "Return to management page" %}
</a>
</div>
{% render_table table %}
</div>
{% endblock %}

View File

@ -1,60 +0,0 @@
{% extends "family/base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load crispy_forms_tags i18n pretty_money %}
{% block profile_content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<form method="post" action="">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
function autocompleted(user) {
$("#id_last_name").val(user.last_name);
$("#id_first_name").val(user.first_name);
$.getJSON("/api/members/profile/" + user.id + "/", function (profile) {
let fee = profile.paid ? "{{ club.membership_fee_paid }}" : "{{ club.membership_fee_unpaid }}";
$("#id_credit_amount").val((Number(fee) / 100).toFixed(2));
});
}
soge_field = $("#id_soge");
function fillFields() {
let checked = soge_field.is(':checked');
if (!checked) {
$("input").attr('disabled', false);
$("#id_user").attr('disabled', true);
$("select").attr('disabled', false);
return;
}
let credit_type = $("#id_credit_type");
credit_type.attr('disabled', true);
credit_type.val(4);
let credit_amount = $("#id_credit_amount");
credit_amount.attr('disabled', true);
credit_amount.val('{{ total_fee }}');
let bank = $("#id_bank");
bank.attr('disabled', true);
bank.val('Société générale');
}
soge_field.change(fillFields);
</script>
{% endblock %}

View File

@ -1,52 +0,0 @@
{% 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 %}
<div class="row mt-4">
<div class="col-xl-4">
{% block profile_info %}
<div class="card bg-light" id="card-infos">
<h4 class="card-header text-center">
{{ family.name }}
</h4>
<div class="text-center">
<a href="{% url 'family:update_pic' family.pk %}">
<img src="{{ family.display_image.url }}" class="img-thumbnail mt-2">
</a>
</div>
<div class="card-body" id="profile_infos">
{% include "family/family_info.html" %}
</div>
<div class="card-footer">
{% if can_add_members %}
<a class="btn btn-sm btn-success" href="{% url 'family:family_add_member' family_pk=family.pk %}"
data-turbolinks="false"> {% trans "Add member" %}</a>
{% endif %}
{% if ".change_"|has_perm:family %}
<a class="btn btn-sm btn-secondary" href="{% url 'family:family_update' pk=family.pk %}"
data-turbolinks="false">
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
</a>
{% endif %}
{% url 'family:family_detail' family.pk as family_detail_url %}
{% if request.path_info != family_detail_url %}
<a class="btn btn-sm btn-primary" href="{{ family_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %}
<a class="btn btn-sm btn-primary" href="{% url "family:family_list" %}">
{% trans "Return to the family list" %}
</a>
</div>
</div>
{% endblock %}
</div>
<div class="col-xl-8">
{% block profile_content %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,36 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }} {{ challenge.name }}
</h3>
<div class="card-body">
<ul>
{% for field, value in fields %}
<li> {{ field }} : {{ value }}</li>
{% endfor %}
<li> {% trans "Obtained by " %} {{obtained}}
{% if obtained > 1 %}
{% trans "families" %}
{% else %}
{% trans "family" %}
{% endif %}
</li>
</ul>
<a class="btn btn-sm btn-primary" href="{% url "family:challenge_list" %}">
{% trans "Return to the challenge list" %}
</a>
{% if update %}
<a class="btn btn-sm btn-secondary" href="{% url "family:challenge_update" pk=challenge.pk %}">
{% trans "Update" %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% 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 %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,40 +0,0 @@
{% extends "base.html" %}
{% comment %}
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 %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="{% url "family:family_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Families" %}
</a>
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Challenges" %}
</a>
<a href="{% url "family:manage" %}" class="btn btn-sm btn-outline-primary">
{% trans "Manage" %}
</a>
</div>
</div>
</div>
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(".table-row").click(function () {
window.document.location = $(this).data("href");
});
</script>
{% endblock %}

View File

@ -1,16 +0,0 @@
{% extends "family/base.html" %}
{% comment %}
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 %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<i class="fa fa-users"></i> {% trans "Family members" %}
</div>
{% render_table member_list %}
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% 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 %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% load i18n pretty_money perms %}
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
<dd class="col-xl-6">{{ family.name }}</dd>
<dt class="col-xl-6">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6">{{ family.description }}</dd>
<dt class="col-xl-6">{% trans 'score'|capfirst %}</dt>
<dd class="col-xl-6">{{ family.score }}</dd>
<dt class="col-xl-6">{% trans 'rank'|capfirst %}</dt>
<dd class="col-xl-6">{{ family.rank }}</dd>
</dl>

View File

@ -1,41 +0,0 @@
{% extends "base.html" %}
{% comment %}
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 %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Families" %}
</a>
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Challenges" %}
</a>
<a href="{% url "family:manage" %}" class="btn btn-sm btn-outline-primary">
{% trans "Manage" %}
</a>
</div>
</div>
</div>
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(".table-row").click(function () {
window.document.location = $(this).data("href");
});
</script>
{% endblock %}

View File

@ -1,181 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n static django_tables2 %}
{% block containertype %}container-fluid{% endblock %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="{% url "family:family_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Families" %}
</a>
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Challenges" %}
</a>
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Manage" %}
</a>
</div>
</div>
</div>
<div class="row mb-3">
<div class='col-sm-5 col-xl-6' id="infos_div">
<div class="row justify-content-center justify-content-md-end">
{# Family details column #}
<div class="col picture-col">
<div class="card bg-light mb-4 text-center">
<a id="profile_pic_link" href="#">
<img src="{% static "member/img/default_picture.png" %}" id="profile_pic" alt="" class="card-img-top d-none d-sm-block">
</a>
<div class="card-body text-center text-break p-2">
<span id="user_note"><i class="small">{% trans "Please select a family" %}</i></span>
</div>
</div>
</div>
{# Family selection column #}
<div class="col-xl" id="user_select_div">
<div class="card bg-light border-success mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Families" %}
</p>
</div>
<div class="card-body p-0" style="min-height:125px;">
<ul class="list-group list-group-flush" id="note_list"></ul>
</div>
{# User search with autocompletion #}
<div class="card-footer">
<input class="form-control mx-auto d-block" placeholder="{% trans "Name" %}" type="text" id="note" autofocus />
</div>
</div>
</div>
{# Summary of challenges and validate button #}
<div class="col-xl-5" id="consos_list_div">
<div class="card bg-light border-info mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Challenges" %}
</p>
</div>
<div class="card-body p-0" style="min-height:125px;">
<ul class="list-group list-group-flush" id="consos_list"></ul>
</div>
<div class="card-footer text-center">
<span id="consume_all" class="btn btn-primary">
{% trans "Validate!" %}
</span>
</div>
</div>
</div>
</div>
{# Create family/challenge buttons #}
<div class="card bg-light border-success mb-4">
<h3 class="card-header font-weight-bold text-center">
{% trans "Create a family or challenge" %}
</h3>
<div class="card-body text-center">
{% if can_add_family %}
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:add_family" %}">
{% trans "Add a family" %}
</a>
{% endif %}
{% if can_add_challenge %}
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:add_challenge" %}">
{% trans "Add a challenge" %}
</a>
{% endif %}
</div>
</div>
</div>
{# Buttons column #}
<div class="col">
<div class="card bg-light border-primary text-center mb-4">
{# Tabs for list and search #}
<div class="card-header">
<ul class="nav nav-tabs nav-fill card-header-tabs">
<li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#list">
{% trans "List" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
{% trans "Search" %}
</a>
</li>
</ul>
</div>
{# Tabs content #}
<div class="card-body">
<div class="tab-content">
<div class="tab-pane" id="list">
<div class="d-inline-flex flex-wrap justify-content-center">
{% for challenge in all_challenges %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
id="challenge{{ challenge.id }}" name="button" value="{{ challenge.name }}">
{{ challenge.name }} ({{ challenge.points }} {% trans "points" %})
</button>
{% endfor %}
</div>
</div>
<div class="tab-pane" id="search">
<input class="form-control mx-auto d-block mb-3" placeholder="{% trans "Search challenge..." %}" type="search" id="search-input"/>
<div class="d-inline-flex flex-wrap justify-content-center" id="search-results">
{% for challenge in all_challenges %}
<button class="btn btn-outline-dark rounded-0 flex-fill" hidden
id="search_challenge{{ challenge.id }}" name="button" value="{{ challenge.name }}">
{{ challenge.name }} ({{ challenge.points }} {% trans "points" %})
</button>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{# transaction history #}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold"
href="{% url 'family:achievement_list' %}" >
{% trans "Recent achievements history" %}
</a>
</div>
<div id="history_list">
{% render_table table %}
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript" src="{% static "family/js/achievements.js" %}"></script>
<script type="text/javascript">
{% for challenge in all_challenges %}
document.getElementById("challenge{{ challenge.id }}").addEventListener("click", function() {
addChallenge({{ challenge.id}}, "{{ challenge.name|escapejs }}", {{ challenge.points }});
});
{% endfor %}
{% for challenge in all_challenges %}
document.getElementById("search_challenge{{ challenge.id }}").addEventListener("click", function() {
addChallenge({{ challenge.id}}, "{{ challenge.name|escapejs }}", {{ challenge.points }});
});
{% endfor %}
</script>
{% endblock %}

View File

@ -1,118 +0,0 @@
{% extends "family/base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block profile_content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<div class="text-center">
<form method="post" enctype="multipart/form-data" id="formUpload">
{% csrf_token %}
{{ form |crispy }}
{% if user.note.display_image != "pic/default.png" %}
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
{% endif %}
</form>
</div>
<!-- MODAL TO CROP THE IMAGE -->
<div class="modal fade" id="modalCrop" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body-wrapper" style="width: 500px; height: 500px; padding: 16px;">
<div class="modal-body" style="width: 100%; height: 100%; padding: 0">
<img src="" id="modal-image" style="display: block; max-width: 100%;">
</div>
</div>
<div class="modal-footer">
<div class="btn-group pull-left" role="group">
<button type="button" class="btn btn-default" id="js-zoom-in">
<span class="glyphicon glyphicon-zoom-in"></span>
</button>
<button type="button" class="btn btn-default js-zoom-out">
<span class="glyphicon glyphicon-zoom-out"></span>
</button>
</div>
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Nevermind" %}</button>
<button type="button" class="btn btn-primary js-crop-and-upload">{% trans "Crop and upload" %}</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extracss %}
<link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.css" rel="stylesheet">
{% endblock %}
{% block extrajavascript%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-cropper@1.0.1/dist/jquery-cropper.min.js"></script>
<script>
$(function () {
/* SCRIPT TO OPEN THE MODAL WITH THE PREVIEW */
$("#id_image").change(function (e) {
if (this.files && this.files[0]) {
// Check the image size
if (this.files[0].size > 2*1024*1024) {
alert("Ce fichier est trop volumineux.")
} else {
// Read the selected image file
var reader = new FileReader();
reader.onload = function (e) {
$("#modal-image").attr("src", e.target.result);
$("#modalCrop").modal("show");
}
reader.readAsDataURL(this.files[0]);
}
}
});
/* SCRIPTS TO HANDLE THE CROPPER BOX */
var $image = $("#modal-image");
var cropBoxData;
var canvasData;
$("#modalCrop").on("shown.bs.modal", function () {
$image.cropper({
viewMode: 1,
aspectRatio: 1 / 1,
minCropBoxWidth: 200,
minCropBoxHeight: 200,
ready: function () {
$image.cropper("setCanvasData", canvasData);
$image.cropper("setCropBoxData", cropBoxData);
}
});
}).on("hidden.bs.modal", function () {
cropBoxData = $image.cropper("getCropBoxData");
canvasData = $image.cropper("getCanvasData");
$image.cropper("destroy");
});
$(".js-zoom-in").click(function () {
$image.cropper("zoom", 0.1);
});
$(".js-zoom-out").click(function () {
$image.cropper("zoom", -0.1);
});
/* SCRIPT TO COLLECT THE DATA AND POST TO THE SERVER */
$(".js-crop-and-upload").click(function () {
var cropData = $image.cropper("getData");
$("#id_x").val(cropData["x"]);
$("#id_y").val(cropData["y"]);
$("#id_height").val(cropData["height"]);
$("#id_width").val(cropData["width"]);
$("#formUpload").submit();
});
});
</script>
{% endblock %}

View File

@ -1,24 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path, include
from . import views
app_name = 'family'
urlpatterns = [
path('list/', views.FamilyListView.as_view(), name="family_list"),
path('add-family/', views.FamilyCreateView.as_view(), name="add_family"),
path('detail/<int:pk>/', views.FamilyDetailView.as_view(), name="family_detail"),
path('update/<int:pk>/', views.FamilyUpdateView.as_view(), name="family_update"),
path('update_pic/<int:pk>/', views.FamilyPictureUpdateView.as_view(), name="update_pic"),
path('add_member/<int:family_pk>/', views.FamilyAddMemberView.as_view(), name="family_add_member"),
path('challenge/list/', views.ChallengeListView.as_view(), name="challenge_list"),
path('add-challenge/', views.ChallengeCreateView.as_view(), name="add_challenge"),
path('challenge/detail/<int:pk>/', views.ChallengeDetailView.as_view(), name="challenge_detail"),
path('challenge/update/<int:pk>/', views.ChallengeUpdateView.as_view(), name="challenge_update"),
path('manage/', views.FamilyManageView.as_view(), name="manage"),
path('achievements/', views.AchievementsView.as_view(), name="achievement_list"),
path('achievement/delete/<int:pk>/', views.AchievementDeleteView.as_view(), name="achievement_delete"),
path('api/family/', include('family.api.urls')),
]

View File

@ -1,310 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
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.views.generic.edit import DeleteView
from django.utils.translation import gettext_lazy as _
from django_tables2 import SingleTableView
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from django.urls import reverse_lazy
from member.views import PictureUpdateView
from .models import Family, Challenge, FamilyMembership, User, Achievement
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable
from .forms import ChallengeForm, FamilyMembershipForm, FamilyForm
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create family
"""
model = Family
extra_context = {"title": _('Create family')}
form_class = FamilyForm
def get_sample_object(self):
return Family(
name="",
description="Sample family",
score=0,
rank=0,
)
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("family:manage")
class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List existing Families
"""
model = Family
table_class = FamilyTable
extra_context = {"title": _('Families list')}
class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Display details of a family
"""
model = Family
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")
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, 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.
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):
"""
Update the information of a family.
"""
model = Family
context_object_name = "family"
form_class = FamilyForm
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):
"""
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_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
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})
class ChallengeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create challenge
"""
model = Challenge
extra_context = {"title": _('Create challenge')}
form_class = ChallengeForm
def get_sample_object(self):
return Challenge(
name="",
description="Sample challenge",
points=0,
)
def get_success_url(self):
return reverse_lazy('family:manage')
class ChallengeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List all challenges
"""
model = Challenge
table_class = ChallengeTable
extra_context = {"title": _('Challenges list')}
class ChallengeDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Display details of a challenge
"""
model = Challenge
context_object_name = "challenge"
extra_context = {"title": _('Details of:')}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fields = ["name", "description", "points",]
fields = dict([(field, getattr(self.object, field)) for field in fields])
context["fields"] = [(
Challenge._meta.get_field(field).verbose_name.capitalize(),
value) for field, value in fields.items()]
context["obtained"] = self.object.obtained
context["update"] = PermissionBackend.check_perm(self.request, "family.change_challenge")
return context
class ChallengeUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the information of a challenge
"""
model = Challenge
context_object_name = "challenge"
extra_context = {"title": _('Update challenge')}
form_class = ChallengeForm
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('family:challenge_detail', kwargs={'pk': self.object.pk})
class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Manage families and challenges
"""
model = Achievement
template_name = 'family/manage.html'
table_class = AchievementTable
extra_context = {'title': _('Manage families and challenges')}
def dispatch(self, request, *args, **kwargs):
# Check that the user is authenticated
if not request.user.is_authenticated:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
# retrieves only Transaction that user has the right to see.
return Achievement.objects.filter(
PermissionBackend.filter_queryset(self.request, Achievement, "view")
).order_by("-obtained_at").all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_challenges'] = Challenge.objects.filter(
PermissionBackend.filter_queryset(self.request, Challenge, "view")
).order_by('name')
context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.add_family")
context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.add_challenge")
return context
def get_table(self, **kwargs):
table = super().get_table(**kwargs)
table.exclude = ('delete',)
table.orderable = False
return table
class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List all achievements
"""
model = Achievement
table_class = AchievementTable
extra_context = {'title': _('Achievement list')}
def get_table(self, **kwargs):
table = super().get_table(**kwargs)
table.orderable = True
return table
class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
"""
Delete an Achievement
"""
model = Achievement
def get_success_url(self):
return reverse_lazy('family:achievement_list')

View File

@ -6,7 +6,7 @@ from django.conf import settings
from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _
from .signals import save_user_profile
from .signals import save_user_profile, update_wei_registration_fee_on_membership_creation, update_wei_registration_fee_on_club_change
class MemberConfig(AppConfig):
@ -17,7 +17,16 @@ class MemberConfig(AppConfig):
"""
Define app internal signals to interact with other apps
"""
from .models import Membership, Club
post_save.connect(
save_user_profile,
sender=settings.AUTH_USER_MODEL,
)
post_save.connect(
update_wei_registration_fee_on_membership_creation,
sender=Membership
)
post_save.connect(
update_wei_registration_fee_on_club_change,
sender=Club
)

View File

@ -13,3 +13,25 @@ def save_user_profile(instance, created, raw, **_kwargs):
instance.profile.email_confirmed = True
instance.profile.registration_valid = True
instance.profile.save()
def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs):
if created:
from wei.models import WEIRegistration
if instance.club.id == 1 or instance.club.id == 2:
registrations = WEIRegistration.objects.filter(
user=instance.user,
wei__year=instance.date_start.year,
)
for r in registrations:
r.save()
def update_wei_registration_fee_on_club_change(sender, instance, **kwargs):
from wei.models import WEIRegistration
if instance.id == 1 or instance.id == 2:
registrations = WEIRegistration.objects.filter(
wei__year=instance.membership_start.year,
)
for r in registrations:
r.save()

View File

@ -168,7 +168,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
class InvoiceDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
"""
Delete a non-locked Invoice
Delete a non-validated WEI registration
"""
model = Invoice
extra_context = {"title": _("Delete invoice")}

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .registration import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, WEIMembership1AForm, \
from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, \
WEIMembershipForm, BusForm, BusTeamForm
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
__all__ = [
'WEIForm', 'WEIRegistrationForm', 'WEIRegistration1AForm', 'WEIRegistration2AForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
'WEIForm', 'WEIRegistrationForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
]

View File

@ -44,7 +44,7 @@ class WEIRegistrationForm(forms.ModelForm):
fields = [
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'deposit_check'
'first_year', 'information_json', 'deposit_check', 'deposit_type'
]
widgets = {
"user": Autocomplete(
@ -62,27 +62,14 @@ class WEIRegistrationForm(forms.ModelForm):
"deposit_check": forms.BooleanField(
required=False,
),
}
class WEIRegistration2AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields + ['deposit_type']
widgets = WEIRegistrationForm.Meta.widgets.copy()
widgets.update({
"deposit_type": forms.RadioSelect(),
})
class WEIRegistration1AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields
}
class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects,
label=_("bus"),
label=_("Bus"),
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
+ " in particular if you are a free eletron."),
widget=CheckboxSelectMultiple(),

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-07-19 12:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0015_remove_weiclub_caution_amount_and_more'),
]
operations = [
migrations.AddField(
model_name='weiregistration',
name='fee',
field=models.PositiveIntegerField(blank=True, default=0, verbose_name='fee'),
),
migrations.AlterField(
model_name='weiclub',
name='fee_soge_credit',
field=models.PositiveIntegerField(default=2000, verbose_name='membership fee (soge credit)'),
),
]

View File

@ -285,6 +285,12 @@ class WEIRegistration(models.Model):
"encoded in JSON"),
)
fee = models.PositiveIntegerField(
default=0,
verbose_name=_('fee'),
blank=True,
)
class Meta:
unique_together = ('user', 'wei',)
verbose_name = _("WEI User")
@ -309,7 +315,25 @@ class WEIRegistration(models.Model):
self.information_json = json.dumps(information, indent=2)
@property
def fee(self):
def is_validated(self):
try:
return self.membership is not None
except AttributeError:
return False
@property
def validation_status(self):
"""
Define an order to have easier access to validatable registrations
"""
if self.fee + (self.wei.deposit_amount if self.deposit_type == 'note' else 0) > self.user.note.balance:
return 2
elif self.first_year:
return 1
else:
return 0
def calculate_fee(self):
bde = Club.objects.get(pk=1)
kfet = Club.objects.get(pk=2)
@ -336,12 +360,9 @@ class WEIRegistration(models.Model):
return fee
@property
def is_validated(self):
try:
return self.membership is not None
except AttributeError:
return False
def save(self, *args, **kwargs):
self.fee = self.calculate_fee()
super().save(*args, **kwargs)
class WEIMembership(Membership):

View File

@ -58,8 +58,8 @@ class WEIRegistrationTable(tables.Table):
validate = tables.Column(
verbose_name=_("Validate"),
orderable=False,
accessor=A('pk'),
orderable=True,
accessor='validate_status',
attrs={
'th': {
'id': 'validate-membership-header'
@ -98,12 +98,13 @@ class WEIRegistrationTable(tables.Table):
if not hasperm:
return format_html("<span class='no-perm'></span>")
url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true'
url = reverse_lazy('wei:validate_registration', args=(record.pk,))
text = _('Validate')
if record.fee > record.user.note.balance and not record.soge_credit:
status = record.validation_status
if status == 2:
btn_class = 'btn-secondary'
tooltip = _("The user does not have enough money.")
elif record.first_year:
elif status == 1:
btn_class = 'btn-info'
tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.")
else:
@ -121,6 +122,7 @@ class WEIRegistrationTable(tables.Table):
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('validate', 'user',)
model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check',

View File

@ -67,20 +67,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
{% endif %}
{% if history_list.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% if pre_registrations.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
@ -99,6 +85,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %}
{% if history_list.data %}
<div class="card bg-white mt-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -13,7 +13,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, Count
from django.db.models import Q, Count, Case, When, Value, IntegerField, F
from django.db.models.functions.text import Lower
from django import forms
from django.http import HttpResponse, Http404
@ -35,7 +35,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \
WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable
@ -133,6 +133,23 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
membership=None,
wei=club
)
# Annotate the query to be able to sort registrations on validate status
pre_registrations = pre_registrations.annotate(
deposit=Case(
When(deposit_type='note', then=F('wei__deposit_amount')),
default=Value(0),
output_field=IntegerField()
)
).annotate(
total_fee=F('fee') + F('deposit')
).annotate(
validate_status=Case(
When(total_fee__gt=F('user__note__balance'), then=Value(2)),
When(first_year=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
return [club_transactions, club_member, pre_registrations, buses, ]
@ -260,6 +277,23 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct()
# Annotate the query to be able to sort registrations on validate status
qs = qs.annotate(
deposit=Case(
When(deposit_type='note', then=F('wei__deposit_amount')),
default=Value(0),
output_field=IntegerField()
)
).annotate(
total_fee=F('fee') + F('deposit')
).annotate(
validate_status=Case(
When(total_fee__gt=F('user__note__balance'), then=Value(2)),
When(first_year=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
pattern = self.request.GET.get("search", "")
@ -510,7 +544,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistration1AForm
form_class = WEIRegistrationForm
extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self):
@ -606,7 +640,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistration2AForm
form_class = WEIRegistrationForm
extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self):
@ -739,14 +773,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
# Store the validate parameter in the view's state
self.should_validate = request.GET.get('validate', False)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
# Pass the validate parameter to the template
context["should_validate"] = self.should_validate
if self.object.is_validated:
membership_form = self.get_membership_form(instance=self.object.membership,
@ -788,7 +819,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
if not self.object.first_year and "deposit_type" in form.fields:
form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["deposit_type"].widget = forms.RadioSelect(choices=form.fields["deposit_type"].choices)
return form
@ -862,9 +892,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object)
if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
# On redirige vers la validation uniquement si c'est explicitement demandé (et stocké dans la vue)
if self.should_validate and self.request.user.has_perm("wei.add_weimembership"):
return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -963,9 +990,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form = context["form"]
if registration.soge_credit:
form.fields["credit_amount"].initial = registration.fee
form.fields["credit_amount"].initial = fee
else:
form.fields["credit_amount"].initial = max(0, registration.fee - registration.user.note.balance)
form.fields["credit_amount"].initial = max(0, fee - registration.user.note.balance)
return context
@ -1052,7 +1079,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
if registration.soge_credit:
fee = 2000
fee = registration.wei.fee_soge_credit
kfet = club.parent_club
bde = kfet.parent_club

View File

@ -3152,10 +3152,8 @@ msgid "Note transaction"
msgstr "Transaction Note"
#: apps/wei/models.py:217
#, fuzzy
#| msgid "Credit type"
msgid "deposit type"
msgstr "Type de rechargement"
msgstr "type de caution"
#: apps/wei/models.py:221 apps/wei/templates/wei/weimembership_form.html:64
msgid "birth date"
@ -4095,14 +4093,6 @@ msgstr "La note est indisponible pour le moment"
msgid "Thank you for your understanding -- The Respos Info of BDE"
msgstr "Merci de votre compréhension -- Les Respos Info du BDE"
#: note_kfet/templates/base_search.html:15
msgid "Search by attribute such as name..."
msgstr "Chercher par un attribut tel que le nom..."
#: note_kfet/templates/base_search.html:23
msgid "There is no results."
msgstr "Il n'y a pas de résultat."
#: note_kfet/templates/cas/logged.html:8
msgid ""
"<h3>Log In Successful</h3>You have successfully logged into the Central "

View File

@ -72,7 +72,6 @@ INSTALLED_APPS = [
# Note apps
'api',
'activity',
'family',
'food',
'logs',
'member',

View File

@ -79,13 +79,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %}</a>
</li>
{% endif %}
{% if user.is_authenticated %}
<li class="nav-item">
{% url 'family:family_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-users"></i> {% trans 'Families' %}</a>
</li>
{% endif %}
{% if "auth.user"|model_list_length >= 2 %}
<li class="nav-item">
{% url 'member:user_list' as url %}

View File

@ -21,9 +21,8 @@ urlpatterns = [
path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
path('food/', include('food.urls')),
path('wrapped/', include('wrapped.urls')),
path('family/', include('family.urls')),
path('food/',include('food.urls')),
path('wrapped/',include('wrapped.urls')),
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),