1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

..

8 Commits

Author SHA1 Message Date
04b41a7d0d Merge branch 'qrcode' into 'main'
Draft: Qrcode

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

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.20 on 2025-05-08 19:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('activity', '0006_guest_school'),
]
operations = [
migrations.AlterField(
model_name='guest',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='activity.activity'),
),
]

View File

@ -234,7 +234,7 @@ class Guest(models.Model):
""" """
activity = models.ForeignKey( activity = models.ForeignKey(
Activity, Activity,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='+', related_name='+',
) )

View File

@ -95,23 +95,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON);
}); });
}); });
$("#delete_activity").click(function () {
if (!confirm("{% trans 'Are you sure you want to delete this activity?' %}")) {
return;
}
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "DELETE",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
}
}).done(function () {
addMsg("{% trans 'Activity deleted' %}", "success");
window.location.href = "/activity/"; // Redirige vers la liste des activités
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
</script> </script>
{% endblock %} {% endblock %}

View File

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

View File

@ -70,10 +70,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if ".change_"|has_perm:activity %} {% if ".change_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a>
{% endif %} {% endif %}
{% if not activity.valid and ".delete_"|has_perm:activity %} {% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-danger btn-sm my-1" id="delete_activity"> {% trans "delete"|capfirst %} </a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started and activity.valid %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -15,5 +15,4 @@ urlpatterns = [
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'), path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
path('new/', views.ActivityCreateView.as_view(), name='activity_create'), path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'), path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'),
path('<int:pk>/delete', views.ActivityDeleteView.as_view(), name='delete_activity'),
] ]

View File

@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import F, Q from django.db.models import F, Q
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -153,34 +153,6 @@ class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityDeleteView(View):
"""
Deletes an Activity
"""
def delete(self, request, pk):
try:
activity = Activity.objects.get(pk=pk)
activity.delete()
return JsonResponse({"message": "Activity deleted"})
except Activity.DoesNotExist:
return JsonResponse({"error": "Activity not found"}, status=404)
def dispatch(self, *args, **kwargs):
"""
Don't display the delete button if the user has no right to delete.
"""
if not self.request.user.is_authenticated:
return self.handle_no_permission()
activity = Activity.objects.get(pk=self.kwargs["pk"])
if not PermissionBackend.check_perm(self.request, "activity.delete_activity", activity):
raise PermissionDenied(_("You are not allowed to delete this activity."))
if activity.valid:
raise PermissionDenied(_("This activity is valid."))
return super().dispatch(*args, **kwargs)
class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
""" """
Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm` Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`

View File

@ -3,7 +3,7 @@
from rest_framework import serializers from rest_framework import serializers
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode from ..models import Allergen, BasicFood, TransformedFood, QRCode
class AllergenSerializer(serializers.ModelSerializer): class AllergenSerializer(serializers.ModelSerializer):
@ -16,16 +16,6 @@ class AllergenSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class FoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Food.
The djangorestframework plugin will analyse the model `Food` and parse all fields in the API.
"""
class Meta:
model = Food
fields = '__all__'
class BasicFoodSerializer(serializers.ModelSerializer): class BasicFoodSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for BasicFood. REST API Serializer for BasicFood.

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet from .views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
def register_food_urls(router, path): def register_food_urls(router, path):
@ -9,7 +9,6 @@ def register_food_urls(router, path):
Configure router for Food REST API. Configure router for Food REST API.
""" """
router.register(path + '/allergen', AllergenViewSet) router.register(path + '/allergen', AllergenViewSet)
router.register(path + '/food', FoodViewSet)
router.register(path + '/basicfood', BasicFoodViewSet) router.register(path + '/basicfood', BasicFoodViewSet)
router.register(path + '/transformedfood', TransformedFoodViewSet) router.register(path + '/transformedfood', TransformedFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet) router.register(path + '/qrcode', QRCodeViewSet)

View File

@ -5,8 +5,8 @@ from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer from .serializers import AllergenSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode from ..models import Allergen, BasicFood, TransformedFood, QRCode
class AllergenViewSet(ReadProtectedModelViewSet): class AllergenViewSet(ReadProtectedModelViewSet):
@ -22,19 +22,6 @@ class AllergenViewSet(ReadProtectedModelViewSet):
search_fields = ['$name', ] search_fields = ['$name', ]
class FoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Food` objects, serialize it to JSON with the given serializer,
then render it on /api/food/food/
"""
queryset = Food.objects.order_by('id')
serializer_class = FoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class BasicFoodViewSet(ReadProtectedModelViewSet): class BasicFoodViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.

View File

@ -12,7 +12,7 @@ from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .models import Food, BasicFood, TransformedFood, QRCode from .models import BasicFood, TransformedFood, QRCode
class QRCodeForms(forms.ModelForm): class QRCodeForms(forms.ModelForm):
@ -22,6 +22,7 @@ class QRCodeForms(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter( self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
is_ready=False,
end_of_life__isnull=True, end_of_life__isnull=True,
polymorphic_ctype__model='transformedfood', polymorphic_ctype__model='transformedfood',
).filter(PermissionBackend.filter_queryset( ).filter(PermissionBackend.filter_queryset(
@ -150,38 +151,3 @@ class AddIngredientForms(forms.ModelForm):
class Meta: class Meta:
model = TransformedFood model = TransformedFood
fields = ('ingredients',) fields = ('ingredients',)
class ManageIngredientsForm(forms.Form):
"""
Form to manage ingredient
"""
fully_used = forms.BooleanField()
fully_used.initial = True
fully_used.required = True
fully_used.label = _('Fully used')
name = forms.CharField()
name.widget = Autocomplete(
model=Food,
resetable=True,
attrs={"api_url": "/api/food/food",
"class": "autocomplete"},
)
name.label = _('Name')
qrcode = forms.IntegerField()
qrcode.widget = Autocomplete(
model=QRCode,
resetable=True,
attrs={"api_url": "/api/food/qrcode/",
"name_field": "qr_code_number",
"class": "autocomplete"},
)
qrcode.label = _('QR code number')
ManageIngredientsFormSet = forms.formset_factory(
ManageIngredientsForm,
extra=1,
)

View File

@ -39,11 +39,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}"> <a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans "Add to a meal" %} {% trans "Add to a meal" %}
</a> </a>
{% endif %}
{% if manage_ingredients %}
<a class="btn btn-sm btn-secondary" href="{% url "food:manage_ingredients" pk=food.pk %}">
{% trans "Manage ingredients" %}
</a>
{% endif %} {% endif %}
<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}"> <a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
{% trans "Return to the food list" %} {% trans "Return to the food list" %}

View File

@ -1,116 +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"></div>
<form method="post" action="">
{% csrf_token %}
<table class="table table-condensed table-striped">
{# Fill initial data #}
{% for display, form in formset %}
{% if forloop.first %}
<thead>
<tr>
<th>{{ form.name.label }}</th>
<th>{{ form.qrcode.label }}</th>
<th>{{ form.fully_used.label }}</th>
</tr>
</thead>
<tbody id="form_body">
{% endif %}
{% if display %}
<tr class="row-formset ingredients">
{% else %}
<tr class="row-formset ingredients" style="display: none">
{% endif %}
<td>{{ form.name }}</td>
<td>{{ form.qrcode }}</td>
<td>{{ form.fully_used }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{# Display buttons to add and remove ingredients #}
<div class="card-body">
<div class="btn-group btn-block" role="group">
<button type="button" id="add_more" class="btn btn-success">{% trans "Add ingredient" %}</button>
<button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove ingredient" %}</button>
</div>
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</div>
</form>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
/* script that handles add and remove lines */
const foods = {{ ingredients | safe }};
function set_ingredient_id () {
let ingredients = document.getElementsByClassName('ingredients');
for (var i = 0; i < ingredients.length; i++) {
ingredients[i].id = 'ingredients-' + parseInt(i);
};
}
set_ingredient_id();
function prepopulate () {
for (var i = 0; i < {{ ingredients_count }}; i++) {
let prefix = 'id_form-' + parseInt(i) + '-';
document.getElementById(prefix + 'name_pk').value = parseInt(foods[i]['food_pk']);
document.getElementById(prefix + 'name').value = foods[i]['food_name'];
document.getElementById(prefix + 'qrcode_pk').value = parseInt(foods[i]['qr_pk']);
if (foods[i]['qr_number'] === '') {
document.getElementById(prefix + 'qrcode').value = '';
}
else {
document.getElementById(prefix + 'qrcode').value = parseInt(foods[i]['qr_number']);
};
document.getElementById(prefix + 'fully_used').checked = Boolean(foods[i]['fully_used']);
};
}
prepopulate();
function delete_form_data (form_id) {
let prefix = "id_form-" + parseInt(form_id) + "-";
document.getElementById(prefix + "name_pk").value = "";
document.getElementById(prefix + "name").value = "";
document.getElementById(prefix + "qrcode_pk").value = "";
document.getElementById(prefix + "qrcode").value = "";
document.getElementById(prefix + "fully_used").checked = true;
}
var form_count = {{ ingredients_count }} + 1;
$('#add_more').click(function () {
let ingredient_form = document.getElementById('ingredients-' + parseInt(form_count));
if (ingredient_form === null) {
addMsg(gettext("You can't add more ingredient"), "danger", 5000);
return;};
ingredient_form.style = "display: true";
form_count += 1;
});
$('#remove_one').click(function () {
let ingredient_form = document.getElementById('ingredients-' + parseInt(form_count - 1));
if (ingredient_form === null) {
return;};
ingredient_form.style = "display: none";
delete_form_data(form_count - 1);
form_count -= 1;
});
addMsg(gettext("Add ingredient with their name or their qrcode, if two different priority is given to qrcode"), "warning");
</script>
{% endblock %}

View File

@ -1,87 +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 }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<table class="table table-condensed table-striped">
{# Fill initial data #}
{% for ingredient_form in formset %}
{% if forloop.first %}
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "QR-code number" %}</th>
<th>{% trans "Fully used" %}<th>
</tr>
</thead>
<tbody id="form_body">
{% endif %}
<tr class="row-formset">
{{ ingredient_form | crispy }}
<td>{{ ingredient_form.name }}</td>
<td>{{ ingredient_form.qrcode }}</td>
<td>{{ ingredient_form.fully_used }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{# Display buttons to add and remove products #}
<div class="card-body">
<div class="btn-group btn-block" role="group">
<button type="button" id="add_more" class="btn btn-success">{% trans "Add ingredient" %}</button>
<button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove ingredient" %}</button>
</div>
<button type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button>
</div>
</form>
</div>
</div>
{# Hidden div that store an empty product form, to be copied into new forms #}
<div id="empty_form" style="display: none;">
<table class='no_error'>
<tbody id="for_real">
<tr class="row-formset">
<td>{{ formset.empty_form.name }}</td>
<td>{{ formset.empty_form.qrcode }}</td>
<td>{{ formset.empty_form.fully_used }}</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
/* script that handles add and remove lines */
IDS = {};
$("#id_form-TOTAL_FORMS").val($(".row-formset").length - 1);
$('#add_more').click(function () {
let form_idx = $('#id_form-TOTAL_FORMS').val();
$('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx));
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
$('#id_form-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]);
});
$('#remove_one').click(function () {
let form_idx = $('#id_form-TOTAL_FORMS').val();
if (form_idx > 0) {
IDS[parseInt(form_idx) - 1] = $('#id_form-' + (parseInt(form_idx) - 1) + '-id').val();
$('#form_body tr:last-child').remove();
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) - 1);
}
});
</script>
{% endblock %}

View File

@ -13,7 +13,6 @@ urlpatterns = [
path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'), path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'),
path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'), path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'),
path('update/<int:pk>', views.FoodUpdateView.as_view(), name='food_update'), path('update/<int:pk>', views.FoodUpdateView.as_view(), name='food_update'),
path('update/ingredients/<int:pk>', views.ManageIngredientsView.as_view(), name='manage_ingredients'),
path('detail/<int:pk>', views.FoodDetailView.as_view(), name='food_view'), path('detail/<int:pk>', views.FoodDetailView.as_view(), name='food_view'),
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'), path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'),

View File

@ -7,7 +7,7 @@ from api.viewsets import is_regex
from django_tables2.views import MultiTableMixin from django_tables2.views import MultiTableMixin
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect
from django.views.generic import DetailView, UpdateView, CreateView from django.views.generic import DetailView, UpdateView, CreateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -18,9 +18,7 @@ from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin
from .models import Food, BasicFood, TransformedFood, QRCode from .models import Food, BasicFood, TransformedFood, QRCode
from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ from .forms import AddIngredientForms, BasicFoodForms, TransformedFoodForms, BasicFoodUpdateForms, TransformedFoodUpdateForms, QRCodeForms
ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \
BasicFoodUpdateForms, TransformedFoodUpdateForms
from .tables import FoodTable from .tables import FoodTable
from .utils import pretty_duration from .utils import pretty_duration
@ -71,11 +69,11 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
open_table = self.get_queryset().order_by('expiry_date').filter( open_table = self.get_queryset().order_by('expiry_date').filter(
Q(polymorphic_ctype__model='transformedfood') Q(polymorphic_ctype__model='transformedfood')
| Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter( | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
expiry_date__lt=timezone.now(), end_of_life='').filter( expiry_date__lt=timezone.now()).filter(
PermissionBackend.filter_queryset(self.request, Food, 'view')) PermissionBackend.filter_queryset(self.request, Food, 'view'))
# table served # table served
served_table = self.get_queryset().order_by('-pk').filter( served_table = self.get_queryset().order_by('-pk').filter(
end_of_life='', is_ready=True).exclude( end_of_life='', is_ready=True).filter(
Q(polymorphic_ctype__model='basicfood', Q(polymorphic_ctype__model='basicfood',
basicfood__date_type='DLC', basicfood__date_type='DLC',
expiry_date__lte=timezone.now(),) expiry_date__lte=timezone.now(),)
@ -108,7 +106,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
return context return context
class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): class QRCodeCreateView(ProtectQuerysetMixin, CreateView):
""" """
A view to add qrcode A view to add qrcode
""" """
@ -168,8 +166,7 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
template_name = "food/food_update.html" template_name = "food/food_update.html"
def get_sample_object(self): def get_sample_object(self):
# We choose a club which may work or BDE else return BasicFood(
food = BasicFood(
name="", name="",
owner_id=1, owner_id=1,
expiry_date=timezone.now(), expiry_date=timezone.now(),
@ -178,14 +175,6 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
date_type='DLC', date_type='DLC',
) )
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
return food
return food
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0: if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0:
@ -236,22 +225,13 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
template_name = "food/food_update.html" template_name = "food/food_update.html"
def get_sample_object(self): def get_sample_object(self):
# We choose a club which may work or BDE else return TransformedFood(
food = TransformedFood(
name="", name="",
owner_id=1, owner_id=1,
expiry_date=timezone.now(), expiry_date=timezone.now(),
is_ready=True, is_ready=True,
) )
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
return food
return food
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.expiry_date = timezone.now() + timedelta(days=3) form.instance.expiry_date = timezone.now() + timedelta(days=3)
@ -263,80 +243,7 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
MAX_FORMS = 100 class AddIngredientView(ProtectQuerysetMixin, UpdateView):
class ManageIngredientsView(LoginRequiredMixin, UpdateView):
"""
A view to manage ingredient for a transformed food
"""
model = TransformedFood
fields = ['ingredients']
extra_context = {"title": _("Manage ingredients of:")}
template_name = 'food/manage_ingredients.html'
@transaction.atomic
def form_valid(self, form):
old_ingredients = list(self.object.ingredients.all()).copy()
old_allergens = list(self.object.allergens.all()).copy()
self.object.ingredients.clear()
for i in range(self.object.ingredients.all().count() + 1 + MAX_FORMS):
prefix = 'form-' + str(i) + '-'
if form.data[prefix + 'qrcode'] not in ['0', '']:
ingredient = QRCode.objects.get(pk=form.data[prefix + 'qrcode']).food_container
self.object.ingredients.add(ingredient)
if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
ingredient.end_of_life = _('Fully used in {meal}'.format(
meal=self.object.name))
ingredient.save()
elif form.data[prefix + 'name'] != '':
ingredient = Food.objects.get(pk=form.data[prefix + 'name'])
self.object.ingredients.add(ingredient)
if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
ingredient.end_of_life = _('Fully used in {meal}'.format(
meal=self.object.name))
ingredient.save()
# We recalculate new expiry date and allergens
self.object.expiry_date = self.object.creation_date + self.object.shelf_life
self.object.allergens.clear()
for ingredient in self.object.ingredients.iterator():
if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['title'] += ' ' + self.object.name
formset = ManageIngredientsFormSet()
ingredients = self.object.ingredients.all()
formset.extra += ingredients.count() + MAX_FORMS
context['form'] = ManageIngredientsForm()
context['ingredients_count'] = ingredients.count()
display = [True] * (1 + ingredients.count()) + [False] * (formset.extra - ingredients.count() - 1)
context['formset'] = zip(display, formset)
context['ingredients'] = []
for ingredient in ingredients:
qr = QRCode.objects.filter(food_container=ingredient)
context['ingredients'].append({
'food_pk': ingredient.pk,
'food_name': ingredient.name,
'qr_pk': '' if qr.count() == 0 else qr[0].pk,
'qr_number': '' if qr.count() == 0 else qr[0].qr_code_number,
'fully_used': 'true' if ingredient.end_of_life else '',
})
return context
def get_success_url(self, **kwargs):
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
class AddIngredientView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
""" """
A view to add ingredient to a meal A view to add ingredient to a meal
""" """
@ -459,8 +366,6 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
if Food.objects.filter(pk=kwargs['pk']).count() != 1:
return Http404
model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model
if 'stop_redirect' in kwargs and kwargs['stop_redirect']: if 'stop_redirect' in kwargs and kwargs['stop_redirect']:
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
@ -499,7 +404,6 @@ class TransformedFoodDetailView(FoodDetailView):
pretty_duration(self.object.shelf_life) pretty_duration(self.object.shelf_life)
)) ))
context["foods"] = self.object.ingredients.all() context["foods"] = self.object.ingredients.all()
context["manage_ingredients"] = True
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):

View File

@ -1,46 +0,0 @@
from django.db import migrations
def create_bda(apps, schema_editor):
"""
The club BDA is now pre-injected.
"""
Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub")
Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
Club.objects.get_or_create(
id=10,
name="BDA",
email="bda.ensparissaclay@gmail.com",
require_memberships=True,
membership_fee_paid=750,
membership_fee_unpaid=750,
membership_duration=396,
membership_start="2024-08-01",
membership_end="2025-09-30",
)
NoteClub.objects.get_or_create(
id=1937,
club_id=10,
polymorphic_ctype_id=polymorphic_ctype_id,
)
Alias.objects.get_or_create(
id=1937,
note_id=1937,
name="BDA",
normalized_name="bda",
)
class Migration(migrations.Migration):
dependencies = [
('member', '0013_auto_20240801_1436'),
]
operations = [
migrations.RunPython(create_bda),
]

View File

@ -60,7 +60,10 @@
{% if user_object.pk == user.pk %} {% if user_object.pk == user.pk %}
<div class="text-center"> <div class="text-center">
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}"> <a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
<i class="fa fa-cogs"></i>{% trans 'API token' %} <i class="fa fa-cogs"></i>&nbsp;{% trans 'API token' %}
</a>
<a class="small badge badge-secondary" href="{% url 'member:qr_code' user_object.pk %}">
<i class="fa fa-qrcode"></i>&nbsp;{% trans 'QR Code' %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

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

View File

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

View File

@ -402,6 +402,14 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
context['token'] = Token.objects.get_or_create(user=self.request.user)[0] context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
return context return context
class QRCodeView(LoginRequiredMixin, DetailView):
"""
Affiche le QR Code
"""
model = User
context_object_name = "user_object"
template_name = "member/qr_code.html"
extra_context = {"title": _("QR Code")}
# ******************************* # # ******************************* #
# CLUB # # CLUB #

View File

@ -1695,7 +1695,7 @@
"wei", "wei",
"weimembership" "weimembership"
], ],
"query": "{\"club\": [\"club\"]}", "query": "[\"AND\", {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}, [\"OR\", {\"registration__soge_credit\": true}, {\"user__note__balance__gte\": {\"F\": [\"F\", \"fee\"]}}]]",
"type": "add", "type": "add",
"mask": 2, "mask": 2,
"field": "", "field": "",
@ -3998,358 +3998,6 @@
"description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -50 €" "description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -50 €"
} }
}, },
{
"model": "permission.permission",
"pk": 271,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier n'importe quel bus du wei"
}
},
{
"model": "permission.permission",
"pk": 272,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tous les bus du wei"
}
},
{
"model": "permission.permission",
"pk": 273,
"fields": {
"model": [
"wei",
"busteam"
],
"query": "{\"bus__wei\": [\"club\"], \"bus__wei__membership_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir toutes les équipes WEI"
}
},
{
"model": "permission.permission",
"pk": 274,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les informations de clubs des bus"
}
},
{
"model": "permission.permission",
"pk": 275,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus__wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier les clubs des bus"
}
},
{
"model": "permission.permission",
"pk": 276,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un⋅e membre à un club de bus"
}
},
{
"model": "permission.permission",
"pk": 277,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les adhérents d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 278,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus__wei\": [\"club\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier l'adhésion d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 279,
"fields": {
"model": [
"note",
"note"
],
"query": "{\"noteclub__club__bus__wei\": [\"club\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir la note d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 280,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}]",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les transactions d'un club de bus"
}
},
{
"model": "permission.permission",
"pk": 281,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Créer une transaction d'un club de bus tant que la source reste au dessus de -20 €"
}
},
{
"model": "permission.permission",
"pk": 282,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club\": [\"club\"]}, {\"destination__noteclub__club\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Créer une transaction d'un WEI tant que la source reste au dessus de -20 €"
}
},
{
"model": "permission.permission",
"pk": 283,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club__name\": \"Kfet\", \"memberships__roles__name\": \"Adh\u00e9rent\u22c5e Kfet\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir n'importe quel⋅le utilisateur⋅rice qui est adhérent⋅e Kfet"
}
},
{
"model": "permission.permission",
"pk": 284,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les informations de club de son bus"
}
},
{
"model": "permission.permission",
"pk": 285,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier le club de son bus"
}
},
{
"model": "permission.permission",
"pk": 286,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un⋅e membre au club de son bus"
}
},
{
"model": "permission.permission",
"pk": 287,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les adhérents du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 288,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier l'adhésion au club de son bus"
}
},
{
"model": "permission.permission",
"pk": 289,
"fields": {
"model": [
"note",
"note"
],
"query": "{\"noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir la note du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 290,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"destination__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}]",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir les transactions du club de son bus"
}
},
{
"model": "permission.permission",
"pk": 291,
"fields": {
"model": [
"wei",
"bus"
],
"query": "{\"pk\": [\"membership\", \"weimembership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir mon bus"
}
},
{
"model": "permission.permission",
"pk": 292,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__pk__lte\": 2}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un membre au BDE ou à la Kfet"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -4443,8 +4091,8 @@
158, 158,
159, 159,
160, 160,
212, 212,
222 222
] ]
} }
}, },
@ -4485,14 +4133,14 @@
50, 50,
141, 141,
169, 169,
217, 217,
218, 218,
219, 219,
220, 220,
221, 221,
247, 247,
258, 258,
259 259
] ]
} }
}, },
@ -4504,8 +4152,8 @@
"name": "Pr\u00e9sident\u22c5e de club", "name": "Pr\u00e9sident\u22c5e de club",
"permissions": [ "permissions": [
62, 62,
135, 142,
142 135
] ]
} }
}, },
@ -4710,8 +4358,6 @@
"name": "GC WEI", "name": "GC WEI",
"permissions": [ "permissions": [
22, 22,
49,
62,
70, 70,
72, 72,
76, 76,
@ -4736,23 +4382,7 @@
112, 112,
113, 113,
128, 128,
130, 130
142,
269,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
292
] ]
} }
}, },
@ -4771,14 +4401,7 @@
119, 119,
120, 120,
121, 121,
122, 122
284,
285,
286,
287,
289,
290,
291
] ]
} }
}, },
@ -4915,8 +4538,8 @@
"name": "GC anti-VSS", "name": "GC anti-VSS",
"permissions": [ "permissions": [
42, 42,
135, 135,
150, 150,
163, 163,
164 164
] ]
@ -4932,147 +4555,13 @@
137, 137,
211, 211,
212, 212,
213, 213,
214, 214,
215, 215,
216 216
] ]
} }
}, },
{
"model": "permission.role",
"pk": 23,
"fields": {
"for_club": 2,
"name": "Darbonne",
"permissions": [
30,
31,
32
]
}
},
{
"model": "permission.role",
"pk": 24,
"fields": {
"for_club": null,
"name": "Staffeur⋅euse (S&L,Respo Tech,...)",
"permissions": []
}
},
{
"model": "permission.role",
"pk": 25,
"fields": {
"for_club": null,
"name": "Référent⋅e Bus",
"permissions": [
22,
84,
115,
117,
118,
119,
120,
121,
122,
284,
285,
286,
287,
289,
290,
291
]
}
},
{
"model": "permission.role",
"pk": 28,
"fields": {
"for_club": 10,
"name": "Trésorièr⸱e BDA",
"permissions": [
55,
56,
57,
58,
135,
143,
176,
177,
178,
243,
260,
261,
262,
263,
264,
265,
266,
267,
268,
269
]
}
},
{
"model": "permission.role",
"pk": 30,
"fields": {
"for_club": 10,
"name": "Respo sorties",
"permissions": [
49,
62,
141,
241,
242,
243
]
}
},
{
"model": "permission.role",
"pk": 31,
"fields": {
"for_club": 1,
"name": "Respo comm",
"permissions": [
135,
244
]
}
},
{
"model": "permission.role",
"pk": 32,
"fields": {
"for_club": 10,
"name": "Respo comm Art",
"permissions": [
135,
245
]
}
},
{
"model": "permission.role",
"pk": 33,
"fields": {
"for_club": 10,
"name": "Respo Jam",
"permissions": [
247,
250,
251,
252,
253,
254
]
}
},
{ {
"model": "wei.weirole", "model": "wei.weirole",
"pk": 12, "pk": 12,
@ -5107,15 +4596,5 @@
"model": "wei.weirole", "model": "wei.weirole",
"pk": 18, "pk": 18,
"fields": {} "fields": {}
},
{
"model": "wei.weirole",
"pk": 24,
"fields": {}
},
{
"model": "wei.weirole",
"pk": 25,
"fields": {}
} }
] ]

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from activity.models import Activity from activity.models import Activity
from member.models import Club, Membership from member.models import Club, Membership
from note.models import NoteUser, NoteClub from note.models import NoteUser
from wei.models import WEIClub, Bus, WEIRegistration from wei.models import WEIClub, Bus, WEIRegistration
@ -122,13 +122,10 @@ class TestPermissionDenied(TestCase):
def test_validate_weiregistration(self): def test_validate_weiregistration(self):
wei = WEIClub.objects.create( wei = WEIClub.objects.create(
name="WEI Test",
membership_start=date.today(), membership_start=date.today(),
date_start=date.today() + timedelta(days=1), date_start=date.today() + timedelta(days=1),
date_end=date.today() + timedelta(days=1), date_end=date.today() + timedelta(days=1),
parent_club=Club.objects.get(name="Kfet"),
) )
NoteClub.objects.create(club=wei)
registration = WEIRegistration.objects.create(wei=wei, user=self.user, birth_date="2000-01-01") registration = WEIRegistration.objects.create(wei=wei, user=self.user, birth_date="2000-01-01")
response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=registration.pk))) response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=registration.pk)))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)

View File

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

View File

@ -24,7 +24,6 @@ class WEIForm(forms.ModelForm):
"membership_end": DatePickerInput(), "membership_end": DatePickerInput(),
"date_start": DatePickerInput(), "date_start": DatePickerInput(),
"date_end": DatePickerInput(), "date_end": DatePickerInput(),
"caution_amount": AmountInput(),
} }
@ -40,11 +39,7 @@ class WEIRegistrationForm(forms.ModelForm):
class Meta: class Meta:
model = WEIRegistration model = WEIRegistration
fields = [ exclude = ('wei', 'clothing_cut')
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'caution_check'
]
widgets = { widgets = {
"user": Autocomplete( "user": Autocomplete(
User, User,
@ -54,30 +49,11 @@ class WEIRegistrationForm(forms.ModelForm):
'placeholder': 'Nom ...', 'placeholder': 'Nom ...',
}, },
), ),
"birth_date": DatePickerInput(options={ "birth_date": DatePickerInput(options={'minDate': '1900-01-01',
'minDate': '1900-01-01', 'maxDate': '2100-01-01'}),
'maxDate': '2100-01-01'
}),
"caution_check": forms.BooleanField(
required=False,
),
} }
class WEIRegistration2AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields + ['caution_type']
widgets = WEIRegistrationForm.Meta.widgets.copy()
widgets.update({
"caution_type": forms.RadioSelect(),
})
class WEIRegistration1AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields
class WEIChooseBusForm(forms.Form): class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField( bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects, queryset=Bus.objects,
@ -96,7 +72,7 @@ class WEIChooseBusForm(forms.Form):
) )
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")), queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"), label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."), help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(), initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
@ -105,8 +81,13 @@ class WEIChooseBusForm(forms.Form):
class WEIMembershipForm(forms.ModelForm): class WEIMembershipForm(forms.ModelForm):
caution_check = forms.BooleanField(
required=False,
label=_("Caution check given"),
)
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="GC WEI")), queryset=WEIRole.objects,
label=_("WEI Roles"), label=_("WEI Roles"),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
) )
@ -213,4 +194,3 @@ class BusTeamForm(forms.ModelForm):
), ),
"color": ColorWidget(), "color": ColorWidget(),
} }
# "color": ColorWidget(),

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.21 on 2025-05-25 12:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0010_remove_weiregistration_specific_diet'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2025, unique=True, verbose_name='year'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 4.2.21 on 2025-05-29 16:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('member', '0014_create_bda'),
('wei', '0011_alter_weiclub_year'),
]
operations = [
migrations.AddField(
model_name='bus',
name='club',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bus', to='member.club', verbose_name='club'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.21 on 2025-06-01 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0012_bus_club'),
]
operations = [
migrations.AddField(
model_name='weiclub',
name='caution_amount',
field=models.PositiveIntegerField(default=0, verbose_name='caution amount'),
),
migrations.AddField(
model_name='weiregistration',
name='caution_type',
field=models.CharField(choices=[('check', 'Check'), ('note', 'Note transaction')], default='check', max_length=16, verbose_name='caution type'),
),
]

View File

@ -33,11 +33,6 @@ class WEIClub(Club):
verbose_name=_("date end"), verbose_name=_("date end"),
) )
caution_amount = models.PositiveIntegerField(
verbose_name=_("caution amount"),
default=0,
)
class Meta: class Meta:
verbose_name = _("WEI") verbose_name = _("WEI")
verbose_name_plural = _("WEI") verbose_name_plural = _("WEI")
@ -77,15 +72,6 @@ class Bus(models.Model):
default=50, default=50,
) )
club = models.OneToOneField(
Club,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="bus",
verbose_name=_("club"),
)
description = models.TextField( description = models.TextField(
blank=True, blank=True,
default="", default="",
@ -202,16 +188,6 @@ class WEIRegistration(models.Model):
verbose_name=_("Caution check given") verbose_name=_("Caution check given")
) )
caution_type = models.CharField(
max_length=16,
choices=(
('check', _("Check")),
('note', _("Note transaction")),
),
default='check',
verbose_name=_("caution type"),
)
birth_date = models.DateField( birth_date = models.DateField(
verbose_name=_("birth date"), verbose_name=_("birth date"),
) )

View File

@ -98,7 +98,7 @@ class WEIRegistrationTable(tables.Table):
if not hasperm: if not hasperm:
return format_html("<span class='no-perm'></span>") 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') text = _('Validate')
if record.fee > record.user.note.balance and not record.soge_credit: if record.fee > record.user.note.balance and not record.soge_credit:
btn_class = 'btn-secondary' btn_class = 'btn-secondary'

View File

@ -40,20 +40,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd> <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
{% else %} {% else %}
{% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %}
<dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }} <dd class="col-xl-6">{{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }} <dd class="col-xl-6">{{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if club.caution_amount > 0 %}
<dt class="col-xl-6">{% trans 'Caution amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.caution_amount|pretty_money }}</dd>
{% endif %}
{% if "note.view_note"|has_perm:club.note %} {% if "note.view_note"|has_perm:club.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>

View File

@ -16,10 +16,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
{% if object.club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_detail' pk=object.club.pk %}"
data-turbolinks="false">{% trans "View club" %}</a>
{% endif %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a> data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}"

View File

@ -18,8 +18,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-footer text-center"> <div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a> data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:manage_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "View" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a> data-turbolinks="false">{% trans "Add team" %}</a>
</div> </div>

View File

@ -13,17 +13,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.media }}
{{ form|crispy }} {{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form> </form>
</div> </div>
</div> </div>
<script>
document.addEventListener("DOMContentLoaded", function () {
if (window.jscolor && jscolor.install) {
jscolor.install();
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -95,11 +95,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% if can_validate_1a %} {% if can_validate_1a %}
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a> <a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}

View File

@ -143,35 +143,25 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% else %} {% else %}
<div class="alert {% if registration.user.note.balance < fee %}alert-danger{% else %}alert-success{% endif %}"> {% if registration.user.note.balance < fee %}
<h5>{% trans "Required payments:" %}</h5> <div class="alert alert-danger">
<ul> {% with pretty_fee=fee|pretty_money %}
<li>{% blocktrans trimmed with amount=fee|pretty_money %} {% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
Membership fees: {{ amount }} The note don't have enough money ({{ balance }}, {{ pretty_fee }} required).
{% endblocktrans %}</li> The registration may fail if you don't credit the note now.
{% if registration.caution_type == 'note' %} {% endblocktrans %}
<li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %} {% endwith %}
Deposit (by Note transaction): {{ amount }} </div>
{% endblocktrans %}</li> {% else %}
<li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %} <div class="alert alert-success">
Total needed: {{ total }} {% blocktrans trimmed with pretty_fee=fee|pretty_money %}
{% endblocktrans %}</strong></li> The note has enough money ({{ pretty_fee }} required), the registration is possible.
{% else %} {% endblocktrans %}
<li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %} </div>
Deposit (by check): {{ amount }} {% endif %}
{% endblocktrans %}</li>
<li><strong>{% blocktrans trimmed with total=fee|pretty_money %}
Total needed: {{ total }}
{% endblocktrans %}</strong></li>
{% endif %}
</ul>
<p>{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
Current balance: {{ balance }}
{% endblocktrans %}</p>
</div>
{% endif %} {% endif %}
{% if not registration.caution_check and not registration.first_year and registration.caution_type == 'check' %} {% if not registration.caution_check and not registration.first_year %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %} {% trans "The user didn't give her/his caution check." %}
</div> </div>

View File

@ -126,7 +126,6 @@ class TestWEIRegistration(TestCase):
year=self.year + 1, year=self.year + 1,
date_start=str(self.year + 1) + "-09-01", date_start=str(self.year + 1) + "-09-01",
date_end=str(self.year + 1) + "-09-03", date_end=str(self.year + 1) + "-09-03",
caution_amount=12000,
)) ))
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1) qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
@ -161,7 +160,6 @@ class TestWEIRegistration(TestCase):
membership_end="2000-09-30", membership_end="2000-09-30",
date_start="2000-09-01", date_start="2000-09-01",
date_end="2000-09-03", date_end="2000-09-03",
caution_amount=12000,
)) ))
qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id) qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id)
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
@ -320,7 +318,6 @@ class TestWEIRegistration(TestCase):
bus=[], bus=[],
team=[], team=[],
roles=[], roles=[],
caution_type='check'
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["membership_form"].is_valid()) self.assertFalse(response.context["membership_form"].is_valid())
@ -337,8 +334,7 @@ class TestWEIRegistration(TestCase):
emergency_contact_phone='+33123456789', emergency_contact_phone='+33123456789',
bus=[self.bus.id], bus=[self.bus.id],
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")).all()], roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
caution_type='check'
)) ))
qs = WEIRegistration.objects.filter(user_id=user.id) qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
@ -358,7 +354,6 @@ class TestWEIRegistration(TestCase):
bus=[self.bus.id], bus=[self.bus.id],
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()], roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
caution_type='check'
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors)) self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors))
@ -511,12 +506,11 @@ class TestWEIRegistration(TestCase):
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()], roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json, information_json=self.registration.information_json,
caution_type='check'
) )
) )
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M") qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
# Check the page when the registration is already validated # Check the page when the registration is already validated
membership = WEIMembership( membership = WEIMembership(
@ -566,12 +560,11 @@ class TestWEIRegistration(TestCase):
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()], roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json, information_json=self.registration.information_json,
caution_type='check'
) )
) )
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L") qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
# Test invalid form # Test invalid form
response = self.client.post( response = self.client.post(
@ -590,7 +583,6 @@ class TestWEIRegistration(TestCase):
team=[], team=[],
roles=[], roles=[],
information_json=self.registration.information_json, information_json=self.registration.information_json,
caution_type='check'
) )
) )
self.assertFalse(response.context["membership_form"].is_valid()) self.assertFalse(response.context["membership_form"].is_valid())
@ -632,7 +624,7 @@ class TestWEIRegistration(TestCase):
second_bus = Bus.objects.create(wei=self.wei, name="Second bus") second_bus = Bus.objects.create(wei=self.wei, name="Second bus")
second_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42) second_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42)
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id], roles=[WEIRole.objects.get(name="GC WEI").id],
bus=self.bus.pk, bus=self.bus.pk,
team=second_team.pk, team=second_team.pk,
credit_type=4, # Bank transfer credit_type=4, # Bank transfer
@ -640,14 +632,13 @@ class TestWEIRegistration(TestCase):
last_name="admin", last_name="admin",
first_name="admin", first_name="admin",
bank="Société générale", bank="Société générale",
caution_check=True,
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["form"].is_valid()) self.assertFalse(response.context["form"].is_valid())
self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors)) self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors))
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id], roles=[WEIRole.objects.get(name="GC WEI").id],
bus=self.bus.pk, bus=self.bus.pk,
team=self.team.pk, team=self.team.pk,
credit_type=4, # Bank transfer credit_type=4, # Bank transfer
@ -655,10 +646,8 @@ class TestWEIRegistration(TestCase):
last_name="admin", last_name="admin",
first_name="admin", first_name="admin",
bank="Société générale", bank="Société générale",
caution_check=True,
)) ))
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200) self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
# Check if the membership is successfully created # Check if the membership is successfully created
membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei) membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei)
self.assertTrue(membership.exists()) self.assertTrue(membership.exists())

View File

@ -4,18 +4,16 @@
import os import os
import shutil import shutil
import subprocess import subprocess
from datetime import date from datetime import date, timedelta
from tempfile import mkdtemp from tempfile import mkdtemp
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Q, Count from django.db.models import Q, Count
from django.db.models.functions.text import Lower from django.db.models.functions.text import Lower
from django import forms
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -35,7 +33,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms.registration import WEIChooseBusForm from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole 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 WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \ from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable WEIRegistration1ATable, WEIMembershipTable
@ -443,10 +441,6 @@ class BusTeamCreateView(ProtectQuerysetMixin, ProtectedCreateView):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
def get_template_names(self):
names = super().get_template_names()
return names
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
""" """
@ -479,10 +473,6 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
def get_template_names(self):
names = super().get_template_names()
return names
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
@ -510,7 +500,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI Register a new user to the WEI
""" """
model = WEIRegistration model = WEIRegistration
form_class = WEIRegistration1AForm form_class = WEIRegistrationForm
extra_context = {"title": _("Register first year student to the WEI")} extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self): def get_sample_object(self):
@ -556,17 +546,9 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
form.fields["user"].initial = self.request.user form.fields["user"].initial = self.request.user
del form.fields["first_year"]
# Cacher les champs pendant l'inscription initiale del form.fields["caution_check"]
if "first_year" in form.fields: del form.fields["information_json"]
del form.fields["first_year"]
if "caution_check" in form.fields:
del form.fields["caution_check"]
if "information_json" in form.fields:
del form.fields["information_json"]
if "caution_type" in form.fields:
del form.fields["caution_type"]
return form return form
@transaction.atomic @transaction.atomic
@ -604,7 +586,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI Register an old user to the WEI
""" """
model = WEIRegistration model = WEIRegistration
form_class = WEIRegistration2AForm form_class = WEIRegistrationForm
extra_context = {"title": _("Register old student to the WEI")} extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self): def get_sample_object(self):
@ -662,19 +644,9 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["soge_credit"].disabled = True form.fields["soge_credit"].disabled = True
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
# Cacher les champs pendant l'inscription initiale del form.fields["caution_check"]
if "first_year" in form.fields: del form.fields["first_year"]
del form.fields["first_year"] del form.fields["information_json"]
if "caution_check" in form.fields:
del form.fields["caution_check"]
if "information_json" in form.fields:
del form.fields["information_json"]
# S'assurer que le champ caution_type est obligatoire
if "caution_type" in form.fields:
form.fields["caution_type"].required = True
form.fields["caution_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["caution_type"].widget = forms.RadioSelect(choices=form.fields["caution_type"].choices)
return form return form
@ -701,9 +673,6 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information form.instance.information = information
# Sauvegarder le type de caution
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save() form.instance.save()
if 'treasury' in settings.INSTALLED_APPS: if 'treasury' in settings.INSTALLED_APPS:
@ -733,15 +702,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# We can't update a registration once the WEI is started and before the membership start date # We can't update a registration once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start: if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) 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) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["club"] = self.object.wei context["club"] = self.object.wei
# Pass the validate parameter to the template
context["should_validate"] = self.should_validate
if self.object.is_validated: if self.object.is_validated:
membership_form = self.get_membership_form(instance=self.object.membership, membership_form = self.get_membership_form(instance=self.object.membership,
@ -775,16 +740,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# The auto-json-format may cause issues with the default field remove # The auto-json-format may cause issues with the default field remove
if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object): if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object):
del form.fields["information_json"] del form.fields["information_json"]
# Masquer le champ caution_check pour tout le monde dans le formulaire de modification
if "caution_check" in form.fields:
del form.fields["caution_check"]
# S'assurer que le champ caution_type est obligatoire pour les 2A+
if not self.object.first_year and "caution_type" in form.fields:
form.fields["caution_type"].required = True
form.fields["caution_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["caution_type"].widget = forms.RadioSelect(choices=form.fields["caution_type"].choices)
return form return form
def get_membership_form(self, data=None, instance=None): def get_membership_form(self, data=None, instance=None):
@ -804,30 +759,10 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
def form_valid(self, form): def form_valid(self, form):
# If the membership is already validated, then we update the bus and the team (and the roles) # If the membership is already validated, then we update the bus and the team (and the roles)
if form.instance.is_validated: if form.instance.is_validated:
try: membership_form = self.get_membership_form(self.request.POST, form.instance.membership)
membership = form.instance.membership if not membership_form.is_valid():
if membership is None:
raise ValueError(_("No membership found for this registration"))
membership_form = self.get_membership_form(self.request.POST, instance=membership)
if not membership_form.is_valid():
return self.form_invalid(form)
# Vérifier que l'utilisateur a la permission de modifier le membership
# On vérifie d'abord si l'utilisateur a la permission générale de modification
if not self.request.user.has_perm("wei.change_weimembership"):
raise PermissionDenied(_("You don't have the permission to update memberships"))
# On vérifie ensuite les permissions spécifiques pour chaque champ modifié
for field_name in membership_form.changed_data:
perm = f"wei.change_weimembership_{field_name}"
if not self.request.user.has_perm(perm):
raise PermissionDenied(_("You don't have the permission to update the field %(field)s") % {'field': field_name})
membership_form.save()
except (WEIMembership.DoesNotExist, ValueError, PermissionDenied) as e:
form.add_error(None, str(e))
return self.form_invalid(form) return self.form_invalid(form)
membership_form.save()
# If it is not validated and if this is an old member, then we update the choices # If it is not validated and if this is an old member, then we update the choices
elif not form.instance.first_year and PermissionBackend.check_perm( elif not form.instance.first_year and PermissionBackend.check_perm(
self.request, "wei.change_weiregistration_information_json", self.object): self.request, "wei.change_weiregistration_information_json", self.object):
@ -842,10 +777,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information form.instance.information = information
# Sauvegarder le type de caution pour les 2A+
if "caution_type" in form.cleaned_data:
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save() form.instance.save()
return super().form_valid(form) return super().form_valid(form)
@ -856,8 +787,14 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object) survey = CurrentSurvey(self.object)
if not survey.is_complete(): if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) 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 PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership(
if self.should_validate and self.request.user.has_perm("wei.add_weimembership"): club=self.object.wei,
user=self.object.user,
date_start=date.today(),
date_end=date.today(),
fee=0,
registration=self.object,
)):
return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk}) return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -899,23 +836,18 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
extra_context = {"title": _("Validate WEI registration")} extra_context = {"title": _("Validate WEI registration")}
def get_sample_object(self): def get_sample_object(self):
"""
Return a sample object for permission checking
"""
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
return WEIMembership( return WEIMembership(
user=registration.user,
club=registration.wei, club=registration.wei,
date_start=registration.wei.date_start, user=registration.user,
fee=registration.wei.membership_fee_paid if registration.user.profile.paid else registration.wei.membership_fee_unpaid, date_start=date.today(),
# Add any fields needed for proper permission checking date_end=date.today() + timedelta(days=1),
fee=0,
registration=registration, registration=registration,
) )
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
wei = registration.wei
today = date.today() today = date.today()
# We can't validate anyone once the WEI is started and before the membership start date # We can't validate anyone once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start: if today >= wei.date_start or today < wei.membership_start:
@ -946,14 +878,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
date_start__gte=bde.membership_start, date_start__gte=bde.membership_start,
).exists() ).exists()
fee = registration.fee context["fee"] = registration.fee
context["fee"] = fee
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.caution_type == 'note':
total_needed += registration.wei.caution_amount
context["total_needed"] = total_needed
form = context["form"] form = context["form"]
if registration.soge_credit: if registration.soge_credit:
@ -965,7 +890,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form_class(self): def get_form_class(self):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
if registration.first_year and 'selected_bus_pk' not in registration.information: if registration.first_year and 'sleected_bus_pk' not in registration.information:
return WEIMembership1AForm return WEIMembership1AForm
return WEIMembershipForm return WEIMembershipForm
@ -975,24 +900,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["last_name"].initial = registration.user.last_name form.fields["last_name"].initial = registration.user.last_name
form.fields["first_name"].initial = registration.user.first_name form.fields["first_name"].initial = registration.user.first_name
# Ajouter le champ caution_check uniquement pour les non-première année et le rendre obligatoire if "caution_check" in form.fields:
if not registration.first_year: form.fields["caution_check"].initial = registration.caution_check
if registration.caution_type == 'check':
form.fields["caution_check"] = forms.BooleanField(
required=True,
initial=registration.caution_check,
label=_("Caution check given"),
help_text=_("Please make sure the check is given before validating the registration")
)
else:
form.fields["caution_check"] = forms.BooleanField(
required=True,
initial=False,
label=_("Create deposit transaction"),
help_text=_("A transaction of %(amount).2f€ will be created from the user's Note account") % {
'amount': registration.wei.caution_amount / 100
}
)
if registration.soge_credit: if registration.soge_credit:
form.fields["credit_type"].disabled = True form.fields["credit_type"].disabled = True
@ -1076,20 +985,10 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
if credit_type is None or registration.soge_credit: if credit_type is None or registration.soge_credit:
credit_amount = 0 credit_amount = 0
# Calculer le montant total nécessaire (frais + caution si transaction) if not registration.soge_credit and user.note.balance + credit_amount < fee:
total_needed = fee # Users must have money before registering to the WEI.
if registration.caution_type == 'note':
total_needed += club.caution_amount
# Vérifier que l'utilisateur a assez d'argent pour tout payer
if not registration.soge_credit and user.note.balance + credit_amount < total_needed:
form.add_error('credit_type', form.add_error('credit_type',
_("This user doesn't have enough money to join this club and pay the deposit. " _("This user don't have enough money to join this club, and can't have a negative balance."))
"Current balance: %(balance)d€, credit: %(credit)d€, needed: %(needed)d") % {
'balance': user.note.balance,
'credit': credit_amount,
'needed': total_needed}
)
return super().form_invalid(form) return super().form_invalid(form)
if credit_amount: if credit_amount:
@ -1129,18 +1028,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db() membership.refresh_from_db()
membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI")) membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI"))
# Créer la transaction de caution si nécessaire
if registration.caution_type == 'note':
from note.models import Transaction
Transaction.objects.create(
source=user.note,
destination=club.note,
quantity=1,
amount=club.caution_amount,
reason=_("Caution %(name)s") % {'name': club.name},
valid=True,
)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -1360,7 +1247,6 @@ class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
def get_queryset(self, filter_permissions=True, **kwargs): def get_queryset(self, filter_permissions=True, **kwargs):
qs = super().get_queryset(filter_permissions, **kwargs) qs = super().get_queryset(filter_permissions, **kwargs)
qs = qs.filter(first_year=True, membership__isnull=False) qs = qs.filter(first_year=True, membership__isnull=False)
qs = qs.filter(wei=self.club)
qs = qs.order_by('-membership__bus') qs = qs.order_by('-membership__bus')
return qs return qs
@ -1403,22 +1289,8 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
if not wei.exists(): if not wei.exists():
raise Http404 raise Http404
wei = wei.get() wei = wei.get()
qs = WEIRegistration.objects.filter(wei=wei, membership__isnull=False, membership__bus__isnull=True)
# On cherche d'abord les 1A qui ont une inscription validée (membership) mais pas de bus qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works...
qs = WEIRegistration.objects.filter( if qs.exists():
wei=wei, return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, ))
first_year=True, return reverse_lazy('wei:wei_1A_list', args=(wei.pk, ))
membership__isnull=False,
membership__bus__isnull=True
)
# Parmi eux, on prend ceux qui ont répondu au questionnaire (ont un bus préféré)
qs = qs.filter(information_json__contains='selected_bus_pk')
if not qs.exists():
# Si on ne trouve personne, on affiche un message et on retourne à la liste
messages.info(self.request, _("No first year student without a bus found. Either all of them have a bus, or none has filled the survey yet."))
return reverse_lazy('wei:wei_1A_list', args=(wei.pk,))
# On redirige vers la page d'attribution pour le premier étudiant trouvé
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk,))

View File

@ -19,9 +19,8 @@ Le modèle regroupe :
* Propriétaire (doit-être un Club) * Propriétaire (doit-être un Club)
* Allergènes (ManyToManyField) * Allergènes (ManyToManyField)
* date d'expiration * date d'expiration
* fin de vie * a été mangé (booléen)
* est prêt (booléen) * est prêt (booléen)
* consigne (pour les GCKs)
BasicFood BasicFood
~~~~~~~~~ ~~~~~~~~~
@ -41,7 +40,7 @@ Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuven
Le modèle regroupe : Le modèle regroupe :
* Durée de conservation (par défaut 3 jours) * Durée de consommation (par défaut 3 jours)
* Ingrédients (ManyToManyField vers Food) * Ingrédients (ManyToManyField vers Food)
* Date de création * Date de création
* Champs de Food * Champs de Food

View File

@ -12,7 +12,6 @@ Applications de la Note Kfet 2020
../api/index ../api/index
registration registration
logs logs
food
treasury treasury
wei wei
wrapped wrapped
@ -67,8 +66,6 @@ Applications facultatives
Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client. Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client.
* `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_ * `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_
Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc... Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc...
* `Food <food>`_ :
Gestion de la nourriture dans Kfet pour les clubs.
* `Treasury <treasury>`_ : * `Treasury <treasury>`_ :
Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques... Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques...
* `WEI <wei>`_ : * `WEI <wei>`_ :

View File

@ -183,7 +183,6 @@ Contributeur⋅rices
* korenst1 * korenst1
* nicomarg * nicomarg
* PAC * PAC
* Quark
* ÿnérant * ÿnérant

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,16 +63,8 @@ class ColorWidget(Widget):
def format_value(self, value): def format_value(self, value):
if value is None: if value is None:
value = 0xFFFFFF value = 0xFFFFFF
if isinstance(value, str): return "#{:06X}".format(value)
return value # Assume it's already a hex string like "#FFAA33"
try:
return "#{:06X}".format(value)
except Exception:
return "#FFFFFF"
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
val = super().value_from_datadict(data, files, name) val = super().value_from_datadict(data, files, name)
if val: return int(val[1:], 16)
return int(val[1:], 16)
return None

View File

@ -1,5 +0,0 @@
<input type="text"
name="{{ widget.name }}"
value="{{ widget.value }}"
class="jscolor"
{% include "django/forms/widgets/attrs.html" %}>