1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-11-17 20:07:52 +01:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Ehouarn
6cffe94bae 'Add all identical food' on ManageIngredients 2025-10-31 23:49:14 +01:00
Ehouarn
78372807f8 Autocomplete food on ManageIngredients now show owners 2025-10-31 23:26:17 +01:00
Ehouarn
b9bf01f2e3 Quark's tests now run, but they're still weak 2025-10-31 19:20:05 +01:00
Ehouarn
624f94823c Tests 2025-10-31 17:48:24 +01:00
Ehouarn
30a598c0b7 Fix dish form and add kitchen view 2025-10-31 17:47:11 +01:00
17 changed files with 543 additions and 135 deletions

View File

@@ -21,9 +21,13 @@ class FoodSerializer(serializers.ModelSerializer):
REST API Serializer for Food. REST API Serializer for Food.
The djangorestframework plugin will analyse the model `Food` and parse all fields in the API. The djangorestframework plugin will analyse the model `Food` and parse all fields in the API.
""" """
# This fields is used for autocompleting food in ManageIngredientsView
# TODO Find a better way to do it
owner_name = serializers.CharField(source='owner.name', read_only=True)
class Meta: class Meta:
model = Food model = Food
fields = '__all__' fields = ['name', 'owner', 'allergens', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'owner_name']
class BasicFoodSerializer(serializers.ModelSerializer): class BasicFoodSerializer(serializers.ModelSerializer):

View File

@@ -3,7 +3,6 @@
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.utils import timezone
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer, \ from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer, \
@@ -114,12 +113,6 @@ class OrderViewSet(ReadProtectedModelViewSet):
filterset_fields = ['user', 'activity', 'dish', 'supplements', 'number', ] filterset_fields = ['user', 'activity', 'dish', 'supplements', 'number', ]
search_fields = ['$user', '$activity', '$dish', '$supplements', '$number', ] search_fields = ['$user', '$activity', '$dish', '$supplements', '$number', ]
def perform_update(self, serializer):
instance = serializer.save()
if instance.served and not instance.served_at:
instance.served_at = timezone.now()
instance.save()
class FoodTransactionViewSet(ReadProtectedModelViewSet): class FoodTransactionViewSet(ReadProtectedModelViewSet):
""" """

View File

@@ -167,7 +167,7 @@ class ManageIngredientsForm(forms.Form):
model=Food, model=Food,
resetable=True, resetable=True,
attrs={"api_url": "/api/food/food", attrs={"api_url": "/api/food/food",
"class": "autocomplete"}, "class": "autocomplete manageingredients-autocomplete"},
) )
name.label = _('Name') name.label = _('Name')
@@ -181,6 +181,11 @@ class ManageIngredientsForm(forms.Form):
) )
qrcode.label = _('QR code number') qrcode.label = _('QR code number')
add_all_same_name = forms.BooleanField(
required=False,
label=_("Add all identical food")
)
ManageIngredientsFormSet = forms.formset_factory( ManageIngredientsFormSet = forms.formset_factory(
ManageIngredientsForm, ManageIngredientsForm,
@@ -219,7 +224,7 @@ SupplementFormSet = forms.inlineformset_factory(
Dish, Dish,
Supplement, Supplement,
form=SupplementForm, form=SupplementForm,
extra=0, extra=1,
) )

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.6 on 2025-10-31 17:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('food', '0003_dish_order_foodtransaction_supplement_and_more'),
]
operations = [
migrations.AlterField(
model_name='foodtransaction',
name='order',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='transaction', to='food.order', verbose_name='order'),
),
]

View File

@@ -256,7 +256,7 @@ class TransformedFood(Food):
self.allergens.set(self.allergens.union(child.allergens.all())) self.allergens.set(self.allergens.union(child.allergens.all()))
if not (child.polymorphic_ctype.model == 'basicfood' and child.date_type == 'DDM'): if not (child.polymorphic_ctype.model == 'basicfood' and child.date_type == 'DDM'):
self.expiry_date = min(self.expiry_date, child.expiry_date) self.expiry_date = min(self.expiry_date, child.expiry_date)
return super().save(force_insert, force_update, using, update_fields) return super().save(force_insert=False, force_update=force_update, using=using, update_fields=update_fields)
class Meta: class Meta:
verbose_name = _('Transformed food') verbose_name = _('Transformed food')
@@ -445,36 +445,29 @@ class Order(models.Model):
self.number = 1 self.number = 1
else: else:
self.number = last_order.number + 1 self.number = last_order.number + 1
super().save(*args, **kwargs)
elif self.served:
if FoodTransaction.objects.filter(order=self).exists():
transaction = FoodTransaction.objects.get(order=self)
transaction.valid = True
transaction.save()
else:
transaction = FoodTransaction( transaction = FoodTransaction(
order=self,
source=self.user.note, source=self.user.note,
destination=self.activity.organizer.note, destination=self.activity.organizer.note,
amount=self.amount, amount=self.amount,
quantity=1, quantity=1,
valid=True,
order=self,
) )
transaction.save() transaction.save()
else: else:
if FoodTransaction.objects.filter(order=self).exists(): old_object = Order.objects.get(pk=self.pk)
transaction = FoodTransaction.objects.get(order=self) if not old_object.served and self.served:
transaction.valid = False self.served_at = timezone.now()
transaction.save() self.transaction.save()
super().save(*args, **kwargs)
return super().save(*args, **kwargs)
class FoodTransaction(Transaction): class FoodTransaction(Transaction):
""" """
Special type of :model:`note.Transaction` associated to a :model:`food.Order`. Special type of :model:`note.Transaction` associated to a :model:`food.Order`.
""" """
order = models.ForeignKey( order = models.OneToOneField(
Order, Order,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='transaction', related_name='transaction',
@@ -484,3 +477,7 @@ class FoodTransaction(Transaction):
class Meta: class Meta:
verbose_name = _("food transaction") verbose_name = _("food transaction")
verbose_name_plural = _("food transactions") verbose_name_plural = _("food transactions")
def save(self, *args, **kwargs):
self.valid = self.order.served
super().save(*args, **kwargs)

View File

@@ -21,7 +21,6 @@ function delete_button (button_id, table_id) {
* @param table_id: Id of the table to reload * @param table_id: Id of the table to reload
*/ */
function serve_button(button_id, table_id, current_state) { function serve_button(button_id, table_id, current_state) {
console.log("update")
const new_state = !current_state; const new_state = !current_state;
$.ajax({ $.ajax({
url: '/api/food/order/' + button_id + '/', url: '/api/food/order/' + button_id + '/',

View File

@@ -106,14 +106,10 @@ class OrderTable(tables.Table):
get_current_request(), "food.change_order_saved", get_current_request(), "food.change_order_saved",
record) else '')}}, verbose_name=_("Serve"), ) record) else '')}}, verbose_name=_("Serve"), )
request = tables.Column(
orderable=False
)
class Meta: class Meta:
model = Order model = Order
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('ordered_at', 'user', 'dish', 'supplements', 'request', 'serve', 'delete') fields = ('number', 'ordered_at', 'user', 'dish', 'supplements', 'request', 'serve', 'delete')
order_by = ('ordered_at', ) order_by = ('ordered_at', )
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',

View File

@@ -31,6 +31,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans "Update" %} {% trans "Update" %}
</a> </a>
{% endif %} {% endif %}
<a class="btn btn-sm btn-primary" href="{% url "food:dish_list" activity_pk=dish.activity.pk %}">
{% trans "Return to dish list" %}
</a>
{% if delete %} {% if delete %}
<a class="btn btn-sm btn-danger" href="{% url "food:dish_delete" activity_pk=dish.activity.pk pk=dish.pk %}"> <a class="btn btn-sm btn-danger" href="{% url "food:dish_delete" activity_pk=dish.activity.pk pk=dish.pk %}">
{% trans "Delete" %} {% trans "Delete" %}

View File

@@ -16,7 +16,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% crispy form %} {% crispy form %}
</div> </div>
<h3 class="card-header text-center"> <h3 class="card-header text-center">
{% trans "Ajouter des suppléments (optionnel)" %} {% trans "Add supplements (optional)" %}
</h3> </h3>
{{ formset.management_form }} {{ formset.management_form }}
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">

View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<!-- Colonne de plats -->
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem;">
{% for food, quantity in orders.items %}
<div class="card bg-white mb-3" style="flex: 1 1 calc(33.333% - 1rem); border: 1px solid #ccc; padding: 1rem; border-radius: 0.5rem; box-sizing: border-box;">
<h3 class="card-header text-center">
<strong>{{ food }}</strong><br>
</h3>
<h1 class="card-body text-center">
{{ quantity }}</h1>
</div>
{% endfor %}
</div>
<!-- Colonne de la table -->
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{% trans "Special orders" %}
</h3>
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There are no special orders." %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -22,6 +22,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<th>{{ form.name.label }}</th> <th>{{ form.name.label }}</th>
<th>{{ form.qrcode.label }}</th> <th>{{ form.qrcode.label }}</th>
<th>{{ form.fully_used.label }}</th> <th>{{ form.fully_used.label }}</th>
<th>{{ form.add_all_same_name.label }}</th>
</tr> </tr>
</thead> </thead>
<tbody id="form_body"> <tbody id="form_body">
@@ -34,6 +35,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<td>{{ form.name }}</td> <td>{{ form.name }}</td>
<td>{{ form.qrcode }}</td> <td>{{ form.qrcode }}</td>
<td>{{ form.fully_used }}</td> <td>{{ form.fully_used }}</td>
<td>{{ form.add_all_same_name }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

View File

@@ -6,9 +6,12 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from activity.models import Activity, ActivityType
from member.models import Club
from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet, \
from ..models import Allergen, BasicFood, TransformedFood, QRCode DishViewSet, SupplementViewSet, OrderViewSet, FoodTransactionViewSet
from ..models import Allergen, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction
class TestFood(TestCase): class TestFood(TestCase):
@@ -64,14 +67,14 @@ class TestFood(TestCase):
""" """
Display QRCode creation Display QRCode creation
""" """
response = self.client.get(reverse('food:qrcode_create')) response = self.client.get(reverse('food:qrcode_create', kwargs={"slug": 2}))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_basicfood_create(self): def test_basicfood_create(self):
""" """
Display BasicFood creation Display BasicFood creation
""" """
response = self.client.get(reverse('food:basicfood_create')) response = self.client.get(reverse('food:basicfood_create', kwargs={"slug": 2}))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_transformedfood_create(self): def test_transformedfood_create(self):
@@ -81,45 +84,265 @@ class TestFood(TestCase):
response = self.client.get(reverse('food:transformedfood_create')) response = self.client.get(reverse('food:transformedfood_create'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_food_create(self): def test_food_update(self):
""" """
Display Food update Display Food update
""" """
response = self.client.get(reverse('food:food_update')) response = self.client.get(reverse('food:food_update', args=(self.basicfood.pk,)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_food_view(self): def test_food_view(self):
""" """
Display Food detail Display Food detail
""" """
response = self.client.get(reverse('food:food_view')) response = self.client.get(reverse('food:food_view', args=(self.basicfood.pk,)))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_basicfood_view(self): def test_basicfood_view(self):
""" """
Display BasicFood detail Display BasicFood detail
""" """
response = self.client.get(reverse('food:basicfood_view')) response = self.client.get(reverse('food:basicfood_view', args=(self.basicfood.pk,)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_transformedfood_view(self): def test_transformedfood_view(self):
""" """
Display TransformedFood detail Display TransformedFood detail
""" """
response = self.client.get(reverse('food:transformedfood_view')) response = self.client.get(reverse('food:transformedfood_view', args=(self.transformedfood.pk,)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_add_ingredient(self): def test_add_ingredient(self):
""" """
Display add ingredient view Display add ingredient view
""" """
response = self.client.get(reverse('food:add_ingredient')) response = self.client.get(reverse('food:add_ingredient', args=(self.transformedfood.pk,)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class TestFoodOrder(TestCase):
"""
Test Food Order
"""
fixtures = ('initial',)
def setUp(self):
self.user = User.objects.create_superuser(
username='admintoto',
password='toto1234',
email='toto@example.com'
)
self.client.force_login(self.user)
sess = self.client.session
sess['permission_mask'] = 42
sess.save()
self.basicfood = BasicFood.objects.create(
id=1,
name='basicfood',
owner=Club.objects.get(name="BDE"),
expiry_date=timezone.now(),
is_ready=True,
date_type='DLC',
)
self.transformedfood = TransformedFood.objects.create(
id=2,
name='transformedfood',
owner=Club.objects.get(name="BDE"),
expiry_date=timezone.now(),
is_ready=True,
)
self.second_transformedfood = TransformedFood.objects.create(
id=3,
name='second transformedfood',
owner=Club.objects.get(name="BDE"),
expiry_date=timezone.now(),
is_ready=True,
)
self.third_transformedfood = TransformedFood.objects.create(
id=4,
name='third transformedfood',
owner=Club.objects.get(name="BDE"),
expiry_date=timezone.now(),
is_ready=True,
)
self.activity = Activity.objects.create(
activity_type=ActivityType.objects.get(name="Perm bouffe"),
organizer=Club.objects.get(name="BDE"),
creater=self.user,
attendees_club_id=1,
date_start=timezone.now(),
date_end=timezone.now(),
name="Test activity",
open=True,
valid=True,
)
self.dish = Dish.objects.create(
main=self.transformedfood,
price=500,
activity=self.activity,
available=True,
)
self.second_dish = Dish.objects.create(
main=self.second_transformedfood,
price=1000,
activity=self.activity,
available=True,
)
self.supplement = Supplement.objects.create(
dish=self.dish,
food=self.basicfood,
price=100,
)
self.order = Order.objects.create(
user=self.user,
activity=self.activity,
dish=self.dish,
)
self.order.supplements.add(self.supplement)
self.order.save()
def test_dish_list(self):
"""
Try to display dish list
"""
response = self.client.get(reverse("food:dish_list", kwargs={"activity_pk": self.activity.pk}))
self.assertEqual(response.status_code, 200)
def test_dish_create(self):
"""
Try to create a dish
"""
response = self.client.get(reverse("food:dish_create", kwargs={"activity_pk": self.activity.pk}))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("food:dish_create", kwargs={"activity_pk": self.activity.pk}), data={
"main": self.third_transformedfood.pk,
"price": 4,
"activity": self.activity.pk,
"supplements-0-food": self.basicfood.pk,
"supplements-0-price": 0.5,
"supplements-TOTAL_FORMS": 1,
"supplements-INITIAL_FORMS": 0,
"supplements-MIN_NUM_FORMS": 0,
"supplements-MAX_NUM_FORMS": 1000,
})
self.assertRedirects(response, reverse("food:dish_list", kwargs={"activity_pk": self.activity.pk}), 302, 200)
self.assertTrue(Dish.objects.filter(main=self.third_transformedfood).exists())
self.assertTrue(Supplement.objects.filter(food=self.basicfood, price=50).exists())
def test_dish_update(self):
"""
Try to update a dish
"""
response = self.client.get(reverse("food:dish_update", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("food:dish_update", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}), data={
"price": 6,
"supplements-0-food": self.basicfood.pk,
"supplements-0-price": 1,
"supplements-1-food": self.basicfood.pk,
"supplements-1-price": 0.25,
"supplements-TOTAL_FORMS": 2,
"supplements-INITIAL_FORMS": 0,
"supplements-MIN_NUM_FORMS": 0,
"supplements-MAX_NUM_FORMS": 1000,
})
self.assertRedirects(response, reverse("food:dish_detail", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}), 302, 200)
self.dish.refresh_from_db()
self.assertTrue(Dish.objects.filter(main=self.transformedfood, price=600).exists())
self.assertTrue(Supplement.objects.filter(dish=self.dish, food=self.basicfood, price=25).exists())
def test_dish_detail(self):
"""
Try to display dish details
"""
response = self.client.get(reverse("food:dish_detail", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
self.assertEqual(response.status_code, 200)
def test_dish_delete(self):
"""
Try to delete a dish
"""
response = self.client.get(reverse("food:dish_delete", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
self.assertEqual(response.status_code, 200)
# Cannot delete already ordered Dish
response = self.client.delete(reverse("food:dish_delete", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
self.assertEqual(response.status_code, 403)
self.assertTrue(Dish.objects.filter(pk=self.dish.pk).exists())
# Can delete a Dish with no order
response = self.client.delete(reverse("food:dish_delete", kwargs={"activity_pk": self.activity.pk, "pk": self.second_dish.pk}))
self.assertRedirects(response, reverse("food:dish_list", kwargs={"activity_pk": self.activity.pk}))
self.assertFalse(Dish.objects.filter(pk=self.second_dish.pk).exists())
def test_order_food(self):
"""
Try to make an order
"""
response = self.client.get(reverse("food:order_create", kwargs={"activity_pk": self.activity.pk}))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("food:order_create", kwargs={"activity_pk": self.activity.pk}), data=dict(
user=self.user.pk,
activity=self.activity.pk,
dish=self.second_dish.pk,
supplements=self.supplement.pk
))
self.assertRedirects(response, reverse("food:food_list"))
self.assertTrue(Order.objects.filter(user=self.user, dish=self.second_dish, activity=self.activity).exists())
def test_order_list(self):
"""
Try to display order list
"""
response = self.client.get(reverse("food:order_list", kwargs={"activity_pk": self.activity.pk}))
self.assertEqual(response.status_code, 200)
def test_served_order_list(self):
"""
Try to display served order list
"""
response = self.client.get(reverse("food:served_order_list", kwargs={"activity_pk": self.activity.pk}))
self.assertEqual(response.status_code, 200)
def test_serve_order(self):
"""
Try to serve an order, then to unserve it
"""
response = self.client.patch("/api/food/order/" + str(self.order.pk) + "/", data=dict(
served=True
), content_type="application/json")
self.assertEqual(response.status_code, 200)
self.order.refresh_from_db()
self.assertTrue(Order.objects.filter(dish=self.dish, user=self.user, served=True).exists())
self.assertIsNotNone(self.order.served_at)
self.assertTrue(FoodTransaction.objects.filter(order=self.order, valid=True).exists())
response = self.client.patch("/api/food/order/" + str(self.order.pk) + "/", data=dict(
served=False
), content_type="application/json")
self.assertEqual(response.status_code, 200)
self.assertTrue(Order.objects.filter(dish=self.dish, user=self.user, served=False).exists())
self.assertTrue(FoodTransaction.objects.filter(order=self.order, valid=False).exists())
class TestFoodAPI(TestAPI): class TestFoodAPI(TestAPI):
def setUp(self) -> None: def setUp(self) -> None:
super().setUP() super().setUp()
self.allergen = Allergen.objects.create( self.allergen = Allergen.objects.create(
name='name', name='name',
@@ -145,6 +368,39 @@ class TestFoodAPI(TestAPI):
food_container=self.basicfood, food_container=self.basicfood,
) )
self.activity = Activity.objects.create(
activity_type=ActivityType.objects.get(name="Perm bouffe"),
organizer=Club.objects.get(name="BDE"),
creater=self.user,
attendees_club_id=1,
date_start=timezone.now(),
date_end=timezone.now(),
name="Test activity",
open=True,
valid=True,
)
self.dish = Dish.objects.create(
main=self.transformedfood,
price=500,
activity=self.activity,
available=True,
)
self.supplement = Supplement.objects.create(
dish=self.dish,
food=self.basicfood,
price=100,
)
self.order = Order.objects.create(
user=self.user,
activity=self.activity,
dish=self.dish,
)
self.order.supplements.add(self.supplement)
self.order.save()
def test_allergen_api(self): def test_allergen_api(self):
""" """
Load Allergen API page and test all filters and permissions Load Allergen API page and test all filters and permissions
@@ -157,6 +413,7 @@ class TestFoodAPI(TestAPI):
""" """
self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/') self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/')
# TODO Repair and detabulate this test
def test_transformedfood_api(self): def test_transformedfood_api(self):
""" """
Load TransformedFood API page and test all filters and permissions Load TransformedFood API page and test all filters and permissions
@@ -168,3 +425,27 @@ class TestFoodAPI(TestAPI):
Load QRCode API page and test all filters and permissions Load QRCode API page and test all filters and permissions
""" """
self.check_viewset(QRCodeViewSet, '/api/food/qrcode/') self.check_viewset(QRCodeViewSet, '/api/food/qrcode/')
def test_dish_api(self):
"""
Load Dish API page and test all filters and permissions
"""
self.check_viewset(DishViewSet, '/api/food/dish/')
def test_supplement_api(self):
"""
Load Supplement API page and test all filters and permissions
"""
self.check_viewset(SupplementViewSet, '/api/food/supplement/')
def test_order_api(self):
"""
Load Order API page and test all filters and permissions
"""
self.check_viewset(OrderViewSet, '/api/food/order/')
def test_foodtransaction_api(self):
"""
Load FoodTransaction API page and test all filters and permissions
"""
self.check_viewset(FoodTransactionViewSet, '/api/food/foodtransaction/')

View File

@@ -28,5 +28,5 @@ urlpatterns = [
path('activity/<int:activity_pk>/order/', views.OrderCreateView.as_view(), name='order_create'), path('activity/<int:activity_pk>/order/', views.OrderCreateView.as_view(), name='order_create'),
path('activity/<int:activity_pk>/orders/', views.OrderListView.as_view(), name='order_list'), path('activity/<int:activity_pk>/orders/', views.OrderListView.as_view(), name='order_list'),
path('activity/<int:activity_pk>/orders/served', views.ServedOrderListView.as_view(), name='served_order_list'), path('activity/<int:activity_pk>/orders/served', views.ServedOrderListView.as_view(), name='served_order_list'),
path('activity/orders/<int:pk>/delete/', views.OrderDeleteView.as_view(), name='order_delete'), path('activity/<int:activity_pk>/kitchen/', views.KitchenView.as_view(), name='kitchen'),
] ]

View File

@@ -8,7 +8,7 @@ from crispy_forms.helper import FormHelper
from django_tables2.views import SingleTableView, MultiTableMixin from django_tables2.views import SingleTableView, MultiTableMixin
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 from django.db.models import Q, Count
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect, Http404
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
@@ -22,7 +22,7 @@ from activity.models import Activity
from permission.backends import PermissionBackend 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, Order, Dish from .models import Food, BasicFood, TransformedFood, QRCode, Order, Dish, Supplement
from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \
ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \
BasicFoodUpdateForms, TransformedFoodUpdateForms, \ BasicFoodUpdateForms, TransformedFoodUpdateForms, \
@@ -307,6 +307,14 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView):
elif form.data[prefix + 'name'] != '': elif form.data[prefix + 'name'] != '':
ingredient = Food.objects.get(pk=form.data[prefix + 'name']) ingredient = Food.objects.get(pk=form.data[prefix + 'name'])
if form.data.get(prefix + 'add_all_same_name') == 'on':
ingredients = Food.objects.filter(name=ingredient.name, owner=ingredient.owner, end_of_life='')
for ingredient in ingredients:
self.object.ingredients.add(ingredient)
if form.data.get(prefix + 'fully_used') == 'on':
ingredient.end_of_life = _('Fully used in {meal}'.format(meal=self.object.name))
ingredient.save()
else:
self.object.ingredients.add(ingredient) self.object.ingredients.add(ingredient)
if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on': if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
ingredient.end_of_life = _('Fully used in {meal}'.format( ingredient.end_of_life = _('Fully used in {meal}'.format(
@@ -591,7 +599,8 @@ class DishCreateView(ProtectQuerysetMixin, ProtectedCreateView):
formset = SupplementFormSet(self.request.POST, instance=form.instance) formset = SupplementFormSet(self.request.POST, instance=form.instance)
if formset.is_valid(): if formset.is_valid():
for f in formset: for f in formset:
if f.is_valid(): # We don't save the product if the price is not entered, ie. if the line is empty
if f.is_valid() and f.instance.price:
f.save() f.save()
f.instance.save() f.instance.save()
else: else:
@@ -656,12 +665,54 @@ class DishUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form_class = DishForm form_class = DishForm
extra_context = {"title": _("Update a dish")} extra_context = {"title": _("Update a dish")}
def get_form(self, **kwargs): def get_context_data(self, **kwargs):
form = super().get_form(**kwargs) context = super().get_context_data(**kwargs)
form = context['form']
form.helper = FormHelper()
# Remove form tag on the generation of the form in the template (already present on the template)
form.helper.form_tag = False
# The formset handles the set of the supplements
form_set = SupplementFormSet(instance=form.instance)
context['formset'] = form_set
context['helper'] = SupplementFormSetHelper()
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
if 'main' in form.fields: if 'main' in form.fields:
del form.fields["main"] del form.fields["main"]
return form return form
@transaction.atomic
def form_valid(self, form):
activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
form.instance.activity = activity
ret = super().form_valid(form)
# For each supplement, we save it
formset = SupplementFormSet(self.request.POST, instance=form.instance)
saved = []
if formset.is_valid():
for f in formset:
# We don't save the product if the price is not entered, ie. if the line is empty
if f.is_valid() and f.instance.price:
f.save()
f.instance.save()
saved.append(f.instance.pk)
else:
f.instance = None
# Remove old supplements that weren't given in the form
Supplement.objects.filter(~Q(pk__in=saved), dish=form.instance).delete()
return ret
def get_success_url(self):
return reverse_lazy('food:dish_detail', kwargs={"activity_pk": self.kwargs["activity_pk"], "pk": self.kwargs["pk"]})
class DishDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): class DishDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
""" """
@@ -726,7 +777,7 @@ class OrderListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, L
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
return Order.objects.filter(activity=activity) return Order.objects.filter(activity=activity).order_by('number')
def get_tables(self): def get_tables(self):
activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
@@ -787,17 +838,32 @@ class ServedOrderListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
return context return context
class OrderDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): class KitchenView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
Delete an order The view to display useful information for the kitchen
""" """
model = Order model = Order
extra_context = {"title": _('Delete dish')} table_class = OrderTable
template_name = 'food/kitchen.html'
extra_context = {'title': _('Kitchen')}
def delete(self, request, *args, **kwargs): def get_queryset(self):
if self.get_object().served: return super().get_queryset().filter(~Q(supplements__isnull=True, request=''), activity__pk=self.kwargs["activity_pk"])
raise PermissionDenied(_("This order cannot be deleted because it has already been served"))
return super().delete(request, *args, **kwargs)
def get_success_url(self): def get_context_data(self, **kwargs):
return reverse_lazy('food:order_list', kwargs={"activity_pk": self.kwargs["activity_pk"]}) context = super().get_context_data(**kwargs)
orders_count = Order.objects.values('dish__main__name').annotate(quantity=Count('id'))
context["orders"] = {o['dish__main__name']: o['quantity'] for o in orders_count}
return context
def get_table(self, **kwargs):
table = super().get_table(**kwargs)
hide = ["ordered_at", "serve", "delete"]
for field in hide:
table.columns.hide(field)
return table

View File

@@ -74,7 +74,6 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
# For each product, we save it # For each product, we save it
formset = ProductFormSet(self.request.POST, instance=form.instance) formset = ProductFormSet(self.request.POST, instance=form.instance)
print(formset)
if formset.is_valid(): if formset.is_valid():
for f in formset: for f in formset:
# We don't save the product if the designation is not entered, ie. if the line is empty # We don't save the product if the designation is not entered, ie. if the line is empty

View File

@@ -13,11 +13,14 @@ $(document).ready(function () {
target.addClass('is-invalid') target.addClass('is-invalid')
target.removeClass('is-valid') target.removeClass('is-valid')
const isManageIngredients = target.hasClass('manageingredients-autocomplete')
$.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) { $.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
let html = '<ul class="list-group list-group-flush" id="' + prefix + '_list">' let html = '<ul class="list-group list-group-flush" id="' + prefix + '_list">'
objects.results.forEach(function (obj) { objects.results.forEach(function (obj) {
html += li(prefix + '_' + obj.id, obj[name_field]) const extra = isManageIngredients ? ` (${obj.owner_name})` : ''
html += li(`${prefix}_${obj.id}`, `${obj[name_field]}${extra}`)
}) })
html += '</ul>' html += '</ul>'