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
24 changed files with 122 additions and 487 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
""" """
@ -245,72 +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 = 10 class AddIngredientView(ProtectQuerysetMixin, UpdateView):
class ManageIngredientsView(ProtectQuerysetMixin, 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()
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
""" """
@ -433,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)
@ -473,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

@ -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

@ -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

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-30 11:44+0200\n" "POT-Creation-Date: 2025-04-24 18:22+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n" "PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi <bleizi@crans.org>\n" "Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -383,9 +383,7 @@ msgstr "Entrée effectuée !"
#: apps/activity/templates/activity/activity_form.html:16 #: apps/activity/templates/activity/activity_form.html:16
#: apps/food/templates/food/food_update.html:17 #: apps/food/templates/food/food_update.html:17
#: apps/food/templates/food/manage_ingredients.html:48
#: apps/food/templates/food/qrcode.html:18 #: apps/food/templates/food/qrcode.html:18
#: apps/food/templates/food/transformedfood_update.html:45
#: apps/member/templates/member/add_members.html:46 #: apps/member/templates/member/add_members.html:46
#: apps/member/templates/member/club_form.html:16 #: apps/member/templates/member/club_form.html:16
#: apps/note/templates/note/transactiontemplate_form.html:18 #: apps/note/templates/note/transactiontemplate_form.html:18
@ -497,40 +495,26 @@ msgstr "API"
msgid "food" msgid "food"
msgstr "bouffe" msgstr "bouffe"
#: apps/food/forms.py:49 #: apps/food/forms.py:50
msgid "Pasta METRO 5kg" msgid "Pasta METRO 5kg"
msgstr "Pâtes METRO 5kg" msgstr "Pâtes METRO 5kg"
#: apps/food/forms.py:53 apps/food/forms.py:81 #: apps/food/forms.py:54 apps/food/forms.py:82
msgid "Specific order given to GCKs" msgid "Specific order given to GCKs"
msgstr "" msgstr ""
#: apps/food/forms.py:77 #: apps/food/forms.py:78
msgid "Lasagna" msgid "Lasagna"
msgstr "Lasagnes" msgstr "Lasagnes"
#: apps/food/forms.py:116 #: apps/food/forms.py:117
msgid "Shelf life (in hours)" msgid "Shelf life (in hours)"
msgstr "Durée de vie (en heure)" msgstr "Durée de vie (en heure)"
#: apps/food/forms.py:138 apps/food/forms.py:162 #: apps/food/forms.py:139
#: apps/food/templates/food/transformedfood_update.html:25
msgid "Fully used" msgid "Fully used"
msgstr "Entièrement utilisé" msgstr "Entièrement utilisé"
#: apps/food/forms.py:171 apps/food/templates/food/qrcode.html:29
#: apps/food/templates/food/transformedfood_update.html:23
#: apps/note/templates/note/transaction_form.html:132
#: apps/treasury/models.py:61
msgid "Name"
msgstr "Nom"
#: apps/food/forms.py:181
#, fuzzy
#| msgid "QR-code number"
msgid "QR code number"
msgstr "numéro de QR-code"
#: apps/food/models.py:23 #: apps/food/models.py:23
msgid "Allergen" msgid "Allergen"
msgstr "Allergène" msgstr "Allergène"
@ -563,7 +547,7 @@ msgstr "est prêt"
msgid "order" msgid "order"
msgstr "consigne" msgstr "consigne"
#: apps/food/models.py:107 apps/food/views.py:34 #: apps/food/models.py:107 apps/food/views.py:32
#: note_kfet/templates/base.html:72 #: note_kfet/templates/base.html:72
msgid "Food" msgid "Food"
msgstr "Bouffe" msgstr "Bouffe"
@ -621,7 +605,6 @@ msgid "QR-codes"
msgstr "QR-codes" msgstr "QR-codes"
#: apps/food/models.py:286 #: apps/food/models.py:286
#: apps/food/templates/food/transformedfood_update.html:24
msgid "QR-code number" msgid "QR-code number"
msgstr "numéro de QR-code" msgstr "numéro de QR-code"
@ -641,11 +624,7 @@ msgstr "Modifier"
msgid "Add to a meal" msgid "Add to a meal"
msgstr "Ajouter à un plat" msgstr "Ajouter à un plat"
#: apps/food/templates/food/food_detail.html:45 #: apps/food/templates/food/food_detail.html:44
msgid "Manage ingredients"
msgstr "Gérer les ingrédients"
#: apps/food/templates/food/food_detail.html:49
msgid "Return to the food list" msgid "Return to the food list"
msgstr "Retour à la liste de nourriture" msgstr "Retour à la liste de nourriture"
@ -681,16 +660,6 @@ msgstr "Bouffe du club"
msgid "Yours club has not food yet." msgid "Yours club has not food yet."
msgstr "Ton club n'a pas de bouffe pour l'instant" msgstr "Ton club n'a pas de bouffe pour l'instant"
#: apps/food/templates/food/manage_ingredients.html:45
#: apps/food/templates/food/transformedfood_update.html:42
msgid "Add ingredient"
msgstr "Ajouter un ingrédient"
#: apps/food/templates/food/manage_ingredients.html:46
#: apps/food/templates/food/transformedfood_update.html:43
msgid "Remove ingredient"
msgstr "Enlever un ingrédient"
#: apps/food/templates/food/qrcode.html:22 #: apps/food/templates/food/qrcode.html:22
msgid "Copy constructor" msgid "Copy constructor"
msgstr "Constructeur de copie" msgstr "Constructeur de copie"
@ -699,6 +668,12 @@ msgstr "Constructeur de copie"
msgid "New food" msgid "New food"
msgstr "Nouvel aliment" msgstr "Nouvel aliment"
#: apps/food/templates/food/qrcode.html:29
#: apps/note/templates/note/transaction_form.html:132
#: apps/treasury/models.py:61
msgid "Name"
msgstr "Nom"
#: apps/food/templates/food/qrcode.html:32 #: apps/food/templates/food/qrcode.html:32
msgid "Owner" msgid "Owner"
msgstr "Propriétaire" msgstr "Propriétaire"
@ -751,49 +726,40 @@ msgstr "semaines"
msgid "and" msgid "and"
msgstr "et" msgstr "et"
#: apps/food/views.py:118 #: apps/food/views.py:116
msgid "Add a new QRCode" msgid "Add a new QRCode"
msgstr "Ajouter un nouveau QR-code" msgstr "Ajouter un nouveau QR-code"
#: apps/food/views.py:167 #: apps/food/views.py:165
msgid "Add an aliment" msgid "Add an aliment"
msgstr "Ajouter un nouvel aliment" msgstr "Ajouter un nouvel aliment"
#: apps/food/views.py:226 #: apps/food/views.py:224
msgid "Add a meal" msgid "Add a meal"
msgstr "Ajouter un plat" msgstr "Ajouter un plat"
#: apps/food/views.py:262 #: apps/food/views.py:251
msgid "Manage ingredients of:"
msgstr "Gestion des ingrédienrs de :"
#: apps/food/views.py:276 apps/food/views.py:284
#, python-brace-format
msgid "Fully used in {meal}"
msgstr "Aliment entièrement utilisé dans : {meal}"
#: apps/food/views.py:323
msgid "Add the ingredient:" msgid "Add the ingredient:"
msgstr "Ajouter l'ingrédient" msgstr "Ajouter l'ingrédient"
#: apps/food/views.py:349 #: apps/food/views.py:275
#, python-brace-format #, python-brace-format
msgid "Food fully used in : {meal.name}" msgid "Food fully used in : {meal.name}"
msgstr "Aliment entièrement utilisé dans : {meal.name}" msgstr "Aliment entièrement utilisé dans : {meal.name}"
#: apps/food/views.py:368 #: apps/food/views.py:294
msgid "Update an aliment" msgid "Update an aliment"
msgstr "Modifier un aliment" msgstr "Modifier un aliment"
#: apps/food/views.py:416 #: apps/food/views.py:342
msgid "Details of:" msgid "Details of:"
msgstr "Détails de :" msgstr "Détails de :"
#: apps/food/views.py:426 apps/treasury/tables.py:149 #: apps/food/views.py:352 apps/treasury/tables.py:149
msgid "Yes" msgid "Yes"
msgstr "Oui" msgstr "Oui"
#: apps/food/views.py:428 apps/member/models.py:99 apps/treasury/tables.py:149 #: apps/food/views.py:354 apps/member/models.py:99 apps/treasury/tables.py:149
msgid "No" msgid "No"
msgstr "Non" msgstr "Non"