diff --git a/apps/food/admin.py b/apps/food/admin.py index d5005adc..2bb1e302 100644 --- a/apps/food/admin.py +++ b/apps/food/admin.py @@ -4,28 +4,31 @@ from django.contrib import admin from note_kfet.admin import admin_site -from .models import QR_code, BasicFood, TransformedFood, Allergen +from .models import Allergen, BasicFood, QRCode, TransformedFood -@admin.register(QR_code, site = admin_site) -class QR_codeAdmin(admin.ModelAdmin): +@admin.register(QRCode, site=admin_site) +class QRCodeAdmin(admin.ModelAdmin): """ TEMPORARY """ -@admin.register(BasicFood, site = admin_site) -class Basic_foodAdmin(admin.ModelAdmin): + +@admin.register(BasicFood, site=admin_site) +class BasicFoodAdmin(admin.ModelAdmin): """ TEMPORARY """ -@admin.register(TransformedFood, site = admin_site) -class Transformed_foodAdmin(admin.ModelAdmin): + +@admin.register(TransformedFood, site=admin_site) +class TransformedFoodAdmin(admin.ModelAdmin): """ TEMPORARY """ -@admin.register(Allergen, site = admin_site) + +@admin.register(Allergen, site=admin_site) class AllergenAdmin(admin.ModelAdmin): """ TEMPORARY diff --git a/apps/food/apps.py b/apps/food/apps.py index a423e253..62ede85f 100644 --- a/apps/food/apps.py +++ b/apps/food/apps.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later +from django.utils.translation import gettext_lazy as _ from django.apps import AppConfig diff --git a/apps/food/fixtures/initial.json b/apps/food/fixtures/initial.json new file mode 100644 index 00000000..c91a2dec --- /dev/null +++ b/apps/food/fixtures/initial.json @@ -0,0 +1,107 @@ +[ + { + "model": "food.allergen", + "pk": 1, + "fields": { + "name": "alcohol" + } + }, + { + "model": "food.allergen", + "pk": 2, + "fields": { + "name": "celery" + } + }, + { + "model": "food.allergen", + "pk": 3, + "fields": { + "name": "crustecean" + } + }, + { + "model": "food.allergen", + "pk": 4, + "fields": { + "name": "egg" + } + }, + { + "model": "food.allergen", + "pk": 5, + "fields": { + "name": "fish" + } + }, + { + "model": "food.allergen", + "pk": 6, + "fields": { + "name": "gluten" + } + }, + { + "model": "food.allergen", + "pk": 7, + "fields": { + "name": "groundnut" + } + }, + { + "model": "food.allergen", + "pk": 8, + "fields": { + "name": "lupine" + } + }, + { + "model": "food.allergen", + "pk": 9, + "fields": { + "name": "milk" + } + }, + { + "model": "food.allergen", + "pk": 10, + "fields": { + "name": "mollusc" + } + }, + { + "model": "food.allergen", + "pk": 11, + "fields": { + "name": "mustard" + } + }, + { + "model": "food.allergen", + "pk": 12, + "fields": { + "name": "nut" + } + }, + { + "model": "food.allergen", + "pk": 13, + "fields": { + "name": "sesame" + } + }, + { + "model": "food.allergen", + "pk": 14, + "fields": { + "name": "soy" + } + }, + { + "model": "food.allergen", + "pk": 15, + "fields": { + "name": "sulphite" + } + } +] diff --git a/apps/food/forms.py b/apps/food/forms.py index 5855c936..ad308727 100644 --- a/apps/food/forms.py +++ b/apps/food/forms.py @@ -7,11 +7,11 @@ from django import forms from django.utils.translation import gettext_lazy as _ from django.utils import timezone from member.models import Club -from note_kfet.inputs import Autocomplete, DatePickerInput, DateTimePickerInput +from note_kfet.inputs import Autocomplete, DateTimePickerInput from note_kfet.middlewares import get_current_request from permission.backends import PermissionBackend -from .models import QR_code, Allergen, BasicFood, TransformedFood +from .models import BasicFood, TransformedFood class BasicFoodForms(forms.ModelForm): @@ -23,33 +23,25 @@ class BasicFoodForms(forms.ModelForm): self.fields['name'].widget.attrs.update({"autofocus": "autofocus"}) self.fields['name'].required = True self.fields['owner'].required = True - self.fields['label'].help_text = _('The lot number must be contained in the picture') # Some example self.fields['name'].widget.attrs.update({"placeholder": _("pasta")}) clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all()) shuffle(clubs) self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..." - def clean_dlm_or_dlc(self): - is_dlc = self.cleaned_data["is_DLC"] - is_ddm = self.cleaned_data["is_DDM"] - if is_dlc and is_ddm: - self.add_error("is_ddm", _("the product cannot be a DLC and a DDM")) - return is_ddm class Meta: model = BasicFood - fields = ('name', 'owner', 'is_DLC', 'is_DDM', 'expiry_date', 'label') + fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens') widgets = { "owner": Autocomplete( - model = Club, - attrs = {"api_url": "/api/members/club/"}, + model=Club, + attrs={"api_url": "/api/members/club/"}, ), - 'expiry_date': DatePickerInput(), + 'expiry_date': DateTimePickerInput(), } - class TransformedFoodForms(forms.ModelForm): """ Form for add transformed food @@ -69,24 +61,13 @@ class TransformedFoodForms(forms.ModelForm): shuffle(clubs) self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..." - class Meta: model = TransformedFood - fields = ('name', 'creation_date', 'owner', 'is_active',) + fields = ('name', 'creation_date', 'owner', 'is_active', 'allergens') widgets = { "owner": Autocomplete( - model = Club, - attrs = {"api_url": "/api/members/club/"}, + model=Club, + attrs={"api_url": "/api/members/club/"}, ), 'creation_date': DateTimePickerInput(), } -class AllergenForms(forms.ModelForm): - """ - Form for allergen - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - class Meta: - model = Allergen - exclude = ['basic_food', 'transformed_food'] diff --git a/apps/food/migrations/0001_initial.py b/apps/food/migrations/0001_initial.py index b51c8a59..7d7d6745 100644 --- a/apps/food/migrations/0001_initial.py +++ b/apps/food/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.28 on 2024-05-25 20:32 +# Generated by Django 2.2.28 on 2024-07-03 07:40 from django.db import migrations, models import django.db.models.deletion @@ -11,87 +11,62 @@ class Migration(migrations.Migration): dependencies = [ ('member', '0011_profile_vss_charter_read'), + ('contenttypes', '0002_remove_content_type_name'), ] operations = [ - migrations.CreateModel( - name='BasicFood', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='name')), - ('is_DLC', models.BooleanField(default=False, verbose_name='is DLC')), - ('is_DDM', models.BooleanField(default=False, verbose_name='is DDM')), - ('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')), - ('expiry_date', models.DateTimeField(blank=True, null=True, verbose_name='expiry date')), - ('label', models.ImageField(max_length=255, upload_to='label/', verbose_name='food label')), - ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')), - ], - options={ - 'verbose_name': 'Basic food', - 'verbose_name_plural': 'Basic foods', - }, - ), - migrations.CreateModel( - name='TransformedFood', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='name')), - ('creation_date', models.DateTimeField(verbose_name='creation date')), - ('expiry_date', models.DateTimeField(verbose_name='expiry date')), - ('is_active', models.BooleanField(default=True, verbose_name='is active')), - ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')), - ('transformed_ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.TransformedFood', verbose_name='transformed ingredient')), - ], - options={ - 'verbose_name': 'Transformed food', - 'verbose_name_plural': 'Transformed foods', - }, - ), - migrations.CreateModel( - name='QR_code', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('qr_code_number', models.PositiveIntegerField(verbose_name='QR-code number')), - ('basic_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.BasicFood', verbose_name='basic food')), - ('transformed_food_container', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.TransformedFood', verbose_name='transformed food container')), - ], - options={ - 'verbose_name': 'QR-code', - 'verbose_name_plural': 'QR-codes', - }, - ), - migrations.AddField( - model_name='basicfood', - name='transformed_food', - field=models.ManyToManyField(blank=True, related_name='BasicFood', to='food.TransformedFood', verbose_name='transformed food'), - ), migrations.CreateModel( name='Allergen', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('gluten', models.BooleanField(default=False, verbose_name='gluten')), - ('nut', models.BooleanField(default=False, verbose_name='nut')), - ('crustecean', models.BooleanField(default=False, verbose_name='crustacean')), - ('celery', models.BooleanField(default=False, verbose_name='celery')), - ('egg', models.BooleanField(default=False, verbose_name='egg')), - ('mustard', models.BooleanField(default=False, verbose_name='mustard')), - ('fish', models.BooleanField(default=False, verbose_name='fish')), - ('soy', models.BooleanField(default=False, verbose_name='soy')), - ('milk', models.BooleanField(default=False, verbose_name='milk')), - ('sulphite', models.BooleanField(default=False, verbose_name='sulphite')), - ('lupine', models.BooleanField(default=False, verbose_name='lupine')), - ('mollusc', models.BooleanField(default=False, verbose_name='mollusc')), - ('groundnut', models.BooleanField(default=False, verbose_name='groundnut')), - ('sesame', models.BooleanField(default=False, verbose_name='sesame')), - ('alcohol', models.BooleanField(default=False, verbose_name='alcohol')), - ('basic_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Allergen', to='food.BasicFood', verbose_name='basic food')), - ('transformed_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Allergen', to='food.TransformedFood', verbose_name='transformed food')), + ('name', models.CharField(max_length=255, null=True, verbose_name='name')), ], options={ 'verbose_name': 'Allergen', 'verbose_name_plural': 'Allergens', }, ), + migrations.CreateModel( + name='Food', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('expiry_date', models.DateTimeField(verbose_name='expiry date')), + ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')), + ('code', models.IntegerField(unique=True, verbose_name='code')), + ('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_food.food_set+', to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'foods', + }, + ), + migrations.CreateModel( + name='BasicFood', + fields=[ + ('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')), + ('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)), + ('arrival_date', models.DateTimeField(blank=True, default=django.utils.timezone.now, verbose_name='arrival date')), + ], + options={ + 'verbose_name': 'Basic food', + 'verbose_name_plural': 'Basic foods', + }, + bases=('food.food',), + ), + migrations.CreateModel( + name='TransformedFood', + fields=[ + ('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')), + ('creation_date', models.DateTimeField(verbose_name='creation date')), + ('is_active', models.BooleanField(default=True, verbose_name='is active')), + ('ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.Food', verbose_name='transformed ingredient')), + ], + options={ + 'verbose_name': 'Transformed food', + 'verbose_name_plural': 'Transformed foods', + }, + bases=('food.food',), + ), ] diff --git a/apps/food/migrations/0002_auto_20240703_1549.py b/apps/food/migrations/0002_auto_20240703_1549.py new file mode 100644 index 00000000..fd3732c2 --- /dev/null +++ b/apps/food/migrations/0002_auto_20240703_1549.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.28 on 2024-07-03 13:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('food', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='food', + name='code', + ), + migrations.CreateModel( + name='QRCode', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('qr_code_number', models.PositiveIntegerField(verbose_name='QR-code number')), + ('food_container', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', unique=True, verbose_name='food container')), + ], + options={ + 'verbose_name': 'QR-code', + 'verbose_name_plural': 'QR-codes', + }, + ), + ] diff --git a/apps/food/models.py b/apps/food/models.py index 44a9f595..367e47f9 100644 --- a/apps/food/models.py +++ b/apps/food/models.py @@ -1,17 +1,11 @@ # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from datetime import date - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator from django.db import models, transaction -from django.db.models import Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ from member.models import Club +from polymorphic.models import PolymorphicModel ################################################################# # TO DO @@ -20,30 +14,21 @@ from member.models import Club # - check on transformed food ################################################################# -class QR_code(models.Model): + +class QRCode(models.Model): """ - An QR_code model + An QRCode model """ qr_code_number = models.PositiveIntegerField( verbose_name=_("QR-code number"), ) - transformed_food_container = models.ForeignKey( - 'TransformedFood', - on_delete = models.PROTECT, - related_name = 'QR_code', - null = True, - blank = True, - verbose_name = _('transformed food container'), - ) - - basic_food = models.ForeignKey( - 'BasicFood', - on_delete = models.PROTECT, - related_name = 'QR_code', - null = True, - blank = True, - verbose_name = _('basic food'), + food_container = models.ForeignKey( + 'Food', + on_delete=models.PROTECT, + related_name='QR_code', + unique=True, + verbose_name=_('food container'), ) class Meta: @@ -53,102 +38,14 @@ class QR_code(models.Model): def __str__(self): return _("QR-code number {qr_code_number}").format(qr_code_number=self.qr_code_number) + class Allergen(models.Model): """ A list of allergen and alimentary restrictions """ - - gluten = models.BooleanField( - default = False, - verbose_name = _('gluten'), - ) - - nut = models.BooleanField( - default = False, - verbose_name = _('nut'), - ) - - crustecean = models.BooleanField( - default = False, - verbose_name = _('crustacean'), - ) - - celery = models.BooleanField( - default = False, - verbose_name = _('celery'), - ) - - egg = models.BooleanField( - default = False, - verbose_name = _('egg'), - ) - - mustard = models.BooleanField( - default = False, - verbose_name = _('mustard'), - ) - - fish = models.BooleanField( - default = False, - verbose_name = _('fish'), - ) - - soy = models.BooleanField( - default = False, - verbose_name = _('soy'), - ) - - milk = models.BooleanField( - default = False, - verbose_name = _('milk'), - ) - - sulphite = models.BooleanField( - default = False, - verbose_name = _('sulphite'), - ) - - lupine = models.BooleanField( - default = False, - verbose_name = _('lupine'), - ) - - mollusc = models.BooleanField( - default = False, - verbose_name = _('mollusc'), - ) - - groundnut = models.BooleanField( - default = False, - verbose_name = _('groundnut'), - ) - - sesame = models.BooleanField( - default = False, - verbose_name = _('sesame'), - ) - - alcohol = models.BooleanField( - default = False, - verbose_name = _('alcohol'), - ) - - transformed_food = models.ForeignKey( - 'TransformedFood', - on_delete = models.CASCADE, - related_name = 'Allergen', - blank = True, - null = True, - verbose_name = _('transformed food'), - ) - - basic_food = models.ForeignKey( - 'BasicFood', - on_delete = models.CASCADE, - related_name = 'Allergen', - blank = True, - null = True, - verbose_name = _('basic food'), + name = models.CharField( + verbose_name=_('name'), + max_length=255, ) class Meta: @@ -156,134 +53,101 @@ class Allergen(models.Model): verbose_name_plural = _('Allergens') def __str__(self): - return _('Allergens of #{id}').format(id=self.id) - - + return self.name -class BasicFood(models.Model): - """ - Food which has been directly buy on supermarket - """ +class Food(PolymorphicModel): name = models.CharField( verbose_name=_('name'), max_length=255, ) - is_DLC = models.BooleanField( - verbose_name=_("is DLC"), - default=False, - ) - - is_DDM = models.BooleanField( - verbose_name=_("is DDM"), - default=False, - ) - - arrival_date = models.DateTimeField( - verbose_name=_('arrival date'), - default=timezone.now, - ) - - expiry_date = models.DateTimeField( - verbose_name=_('expiry date'), - blank=True, - null = True, - ) - owner = models.ForeignKey( Club, on_delete=models.PROTECT, - related_name= '+', + related_name='+', verbose_name=_('owner'), ) - label = models.ImageField( - verbose_name=_('food label'), - max_length=255, - blank=False, - null=False, - upload_to='label/', - ) - - was_eaten = models.BooleanField( - verbose_name=_('was eaten'), - default = False, - ) - - transformed_food = models.ManyToManyField( - 'TransformedFood', - related_name= 'BasicFood', - blank = True, - verbose_name = _('transformed food'), - ) - - - class Meta: - verbose_name=_('Basic food') - verbose_name_plural=_('Basic foods') - - def __str__(self): - return self.name - - @transaction.atomic - def save(self, force_insert=False, force_update=False, using= None, update_fields=None): - # Check if is_DLC and is DDM are not both True - if self.is_DLC and self.is_DDM: - raise ValidationError("The product cannot be a DLC and a DDM") - return super().save(force_insert, force_update, using, update_fields) - - -class TransformedFood(models.Model): - """ - Transformed food are a mix between basic food and meal - """ - name = models.CharField( - max_length = 255, - verbose_name =_('name'), - ) - - creation_date = models.DateTimeField( - verbose_name =_('creation date'), + allergens = models.ManyToManyField( + Allergen, + blank=True, + verbose_name=_('allergen'), ) expiry_date = models.DateTimeField( - verbose_name =_('expiry date'), - ) - - owner = models.ForeignKey( - Club, - on_delete = models.PROTECT, - related_name = '+', - verbose_name =_('owner'), - ) - - transformed_ingredient = models.ManyToManyField( - "self", - blank = True, - symmetrical = False, - related_name = 'transformed_ingredient_inv', - verbose_name = _('transformed ingredient'), - ) - - is_active = models.BooleanField( - default = True, - verbose_name = _('is active'), + verbose_name=_('expiry date'), ) was_eaten = models.BooleanField( - default = False, - verbose_name = _('was eaten'), + default=False, + verbose_name=_('was eaten'), ) - - class Meta: - verbose_name = _('Transformed food') - verbose_name_plural = _('Transformed foods') - def __str__(self): return self.name @transaction.atomic - def save(self, force_insert=False, force_update=False, using= None, update_fields=None): + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): return super().save(force_insert, force_update, using, update_fields) + + class Meta: + verbose_name = _('food') + verbose_name = _('foods') + + +class BasicFood(Food): + """ + Food which has been directly buy on supermarket + """ + date_type = models.CharField( + max_length=255, + choices=( + ("DLC", "DLC"), + ("DDM", "DDM"), + ) + ) + + arrival_date = models.DateTimeField( + verbose_name=_('arrival date'), + default=timezone.now, + blank=True, # TEMPORARY + ) + + # label = models.ImageField( + # verbose_name=_('food label'), + # max_length=255, + # blank=False, + # null=False, + # upload_to='label/', + # ) + + class Meta: + verbose_name = _('Basic food') + verbose_name_plural = _('Basic foods') + + +class TransformedFood(Food): + """ + Transformed food are a mix between basic food and meal + """ + creation_date = models.DateTimeField( + verbose_name=_('creation date'), + ) + + ingredient = models.ManyToManyField( + Food, + blank=True, + symmetrical=False, + related_name='transformed_ingredient_inv', + verbose_name=_('transformed ingredient'), + ) + + is_active = models.BooleanField( + default=True, + verbose_name=_('is active'), + ) + + class Meta: + verbose_name = _('Transformed food') + verbose_name_plural = _('Transformed foods') diff --git a/apps/food/static/food/js/food.js b/apps/food/static/food/js/food.js new file mode 100644 index 00000000..509d9b48 --- /dev/null +++ b/apps/food/static/food/js/food.js @@ -0,0 +1,422 @@ +var LOCK = false + +sources = [] +sources_notes_display = [] +dests = [] +dests_notes_display = [] + +function refreshHistory () { + $('#history').load('/note/transfer/ #history') +} + +function reset (refresh = true) { + sources_notes_display.length = 0 + sources.length = 0 + dests_notes_display.length = 0 + dests.length = 0 + $('#source_note_list').html('') + $('#dest_note_list').html('') + const source_field = $('#source_note') + source_field.val('') + const event = jQuery.Event('keyup') + event.originalEvent = { charCode: 97 } + source_field.trigger(event) + source_field.removeClass('is-invalid') + source_field.attr('data-original-title', '').tooltip('hide') + const dest_field = $('#dest_note') + dest_field.val('') + dest_field.trigger(event) + dest_field.removeClass('is-invalid') + dest_field.attr('data-original-title', '').tooltip('hide') + const amount_field = $('#amount') + amount_field.val('') + amount_field.removeClass('is-invalid') + $('#amount-required').html('') + const reason_field = $('#reason') + reason_field.val('') + reason_field.removeClass('is-invalid') + $('#reason-required').html('') + $('#last_name').val('') + $('#first_name').val('') + $('#bank').val('') + $('#user_note').val('') + $('#profile_pic').attr('src', '/static/member/img/default_picture.png') + $('#profile_pic_link').attr('href', '#') + if (refresh) { + refreshBalance() + refreshHistory() + } + + LOCK = false +} + +$(document).ready(function () { + /** + * If we are in credit/debit mode, check that only one note is entered. + * More over, get first name and last name to autocomplete fields. + */ + function checkUniqueNote () { + if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) { + const arr = $('#type_credit').is(':checked') ? dests_notes_display : sources_notes_display + + if (arr.length === 0) { return } + + const last = arr[arr.length - 1] + arr.length = 0 + arr.push(last) + + last.quantity = 1 + + if (last.note.club) { + $('#last_name').val(last.note.name) + $('#first_name').val(last.note.name) + } + else if (!last.note.user) { + $.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) { + last.note.user = note.user + $.getJSON('/api/user/' + last.note.user + '/', function (user) { + $('#last_name').val(user.last_name) + $('#first_name').val(user.first_name) + }) + }) + } else { + $.getJSON('/api/user/' + last.note.user + '/', function (user) { + $('#last_name').val(user.last_name) + $('#first_name').val(user.first_name) + }) + } + } + + return true + } + + autoCompleteNote('source_note', 'source_note_list', sources, sources_notes_display, + 'source_alias', 'source_note', 'user_note', 'profile_pic', checkUniqueNote) + autoCompleteNote('dest_note', 'dest_note_list', dests, dests_notes_display, + 'dest_alias', 'dest_note', 'user_note', 'profile_pic', checkUniqueNote) + + const source = $('#source_note') + const dest = $('#dest_note') + + $('#type_transfer').change(function () { + if (LOCK) { return } + + $('#source_me_div').removeClass('d-none') + $('#source_note').removeClass('is-invalid') + $('#dest_note').removeClass('is-invalid') + $('#special_transaction_div').addClass('d-none') + source.removeClass('d-none') + $('#source_note_list').removeClass('d-none') + $('#credit_type').addClass('d-none') + dest.removeClass('d-none') + $('#dest_note_list').removeClass('d-none') + $('#debit_type').addClass('d-none') + + $('#source_note_label').text(select_emitters_label) + $('#dest_note_label').text(select_receveirs_label) + + location.hash = 'transfer' + }) + + $('#type_credit').change(function () { + if (LOCK) { return } + + $('#source_me_div').addClass('d-none') + $('#source_note').removeClass('is-invalid') + $('#dest_note').removeClass('is-invalid') + $('#special_transaction_div').removeClass('d-none') + $('#source_note_list').addClass('d-none') + $('#dest_note_list').removeClass('d-none') + source.addClass('d-none') + source.tooltip('hide') + $('#credit_type').removeClass('d-none') + dest.removeClass('d-none') + dest.val('') + dest.tooltip('hide') + $('#debit_type').addClass('d-none') + + $('#source_note_label').text(transfer_type_label) + $('#dest_note_label').text(select_receveir_label) + + if (dests_notes_display.length > 1) { + $('#dest_note_list').html('') + dests_notes_display.length = 0 + } + + location.hash = 'credit' + }) + + $('#type_debit').change(function () { + if (LOCK) { return } + + $('#source_me_div').addClass('d-none') + $('#source_note').removeClass('is-invalid') + $('#dest_note').removeClass('is-invalid') + $('#special_transaction_div').removeClass('d-none') + $('#source_note_list').removeClass('d-none') + $('#dest_note_list').addClass('d-none') + source.removeClass('d-none') + source.val('') + source.tooltip('hide') + $('#credit_type').addClass('d-none') + dest.addClass('d-none') + dest.tooltip('hide') + $('#debit_type').removeClass('d-none') + + $('#source_note_label').text(select_emitter_label) + $('#dest_note_label').text(transfer_type_label) + + if (sources_notes_display.length > 1) { + $('#source_note_list').html('') + sources_notes_display.length = 0 + } + + location.hash = 'debit' + }) + + $('#credit_type').change(function () { + const type = $('#credit_type option:selected').text() + if ($('#type_credit').is(':checked')) { source.val(type) } else { dest.val(type) } + }) + + // Ensure we begin in transfer mode. Removing these lines may cause problems when reloading. + const type_transfer = $('#type_transfer') // Default mode + type_transfer.removeAttr('checked') + $('#type_credit').removeAttr('checked') + $('#type_debit').removeAttr('checked') + + if (location.hash) { $('#type_' + location.hash.substr(1)).click() } else { type_transfer.click() } + + $('#source_me').click(function () { + if (LOCK) { return } + + // Shortcut to set the current user as the only emitter + sources_notes_display.length = 0 + sources.length = 0 + $('#source_note_list').html('') + + const source_note = $('#source_note') + source_note.focus() + source_note.val('') + let event = jQuery.Event('keyup') + event.originalEvent = { charCode: 97 } + source_note.trigger(event) + source_note.val(username) + event = jQuery.Event('keyup') + event.originalEvent = { charCode: 97 } + source_note.trigger(event) + const fill_note = function () { + if (sources.length === 0) { + setTimeout(fill_note, 100) + return + } + event = jQuery.Event('keypress') + event.originalEvent = { charCode: 13 } + source_note.trigger(event) + + source_note.tooltip('hide') + source_note.val('') + $('#dest_note').focus() + } + fill_note() + }) +}) + +// Make transfer when pressing Enter on the amount section +$('#amount, #reason, #last_name, #first_name, #bank').keypress((event) => { + if (event.originalEvent.charCode === 13) { + $('#btn_transfer').click() + } +}) + +$('#btn_transfer').click(function () { + if (LOCK) { return } + + LOCK = true + + let error = false + + const amount_field = $('#amount') + amount_field.removeClass('is-invalid') + $('#amount-required').html('') + + const reason_field = $('#reason') + reason_field.removeClass('is-invalid') + $('#reason-required').html('') + + if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) { + amount_field.addClass('is-invalid') + $('#amount-required').html('' + gettext('This field is required and must contain a decimal positive number.') + '') + error = true + } + + const amount = Math.round(100 * amount_field.val()) + if (amount > 2147483647) { + amount_field.addClass('is-invalid') + $('#amount-required').html('' + gettext('The amount must stay under 21,474,836.47 €.') + '') + error = true + } + + if (!reason_field.val() && $('#type_transfer').is(':checked')) { + reason_field.addClass('is-invalid') + $('#reason-required').html('' + gettext('This field is required.') + '') + error = true + } + + if (!sources_notes_display.length && !$('#type_credit').is(':checked')) { + $('#source_note').addClass('is-invalid') + error = true + } + + if (!dests_notes_display.length && !$('#type_debit').is(':checked')) { + $('#dest_note').addClass('is-invalid') + error = true + } + + if (error) { + LOCK = false + return + } + + let reason = reason_field.val() + + if ($('#type_transfer').is(':checked')) { + // We copy the arrays to ensure that transactions are well-processed even if the form is reset + [...sources_notes_display].forEach(function (source) { + [...dests_notes_display].forEach(function (dest) { + if (source.note.id === dest.note.id) { + addMsg(interpolate(gettext('Warning: the transaction of %s from %s to %s was not made because ' + + 'it is the same source and destination note.'), [pretty_money(amount), source.name, dest.name]), 'warning', 10000) + LOCK = false + return + } + + $.post('/api/note/transaction/transaction/', + { + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: source.quantity * dest.quantity, + amount: amount, + reason: reason, + valid: true, + polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE, + resourcetype: 'Transaction', + source: source.note.id, + source_alias: source.name, + destination: dest.note.id, + destination_alias: dest.name + }).done(function () { + if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) { + addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000) + } + if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) { + addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000) + } + + if (!isNaN(source.note.balance)) { + const newBalance = source.note.balance - source.quantity * dest.quantity * amount + if (newBalance <= -2000) { + addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'), + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000) + reset() + return + } else if (newBalance < 0) { + addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is negative.'), + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000) + reset() + return + } + } + addMsg(interpolate(gettext('Transfer of %s from %s to %s succeed!'), + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name]), 'success', 10000) + + reset() + }).fail(function (err) { // do it again but valid = false + const errObj = JSON.parse(err.responseText) + if (errObj.non_field_errors) { + addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, errObj.non_field_errors]), 'danger') + LOCK = false + return + } + + $.post('/api/note/transaction/transaction/', + { + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: source.quantity * dest.quantity, + amount: amount, + reason: reason, + valid: false, + invalidity_reason: 'Solde insuffisant', + polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE, + resourcetype: 'Transaction', + source: source.note.id, + source_alias: source.name, + destination: dest.note.id, + destination_alias: dest.name + }).done(function () { + addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, gettext('insufficient funds')]), 'danger', 10000) + reset() + }).fail(function (err) { + const errObj = JSON.parse(err.responseText) + let error = errObj.detail ? errObj.detail : errObj.non_field_errors + if (!error) { error = err.responseText } + addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, error]), 'danger') + LOCK = false + }) + }) + }) + }) + } else if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) { + let special_note + let user_note + let alias + const given_reason = reason + let source_id, dest_id + if ($('#type_credit').is(':checked')) { + special_note = $('#credit_type').val() + user_note = dests_notes_display[0].note + alias = dests_notes_display[0].name + source_id = special_note + dest_id = user_note.id + reason = 'Crédit ' + $('#credit_type option:selected').text().toLowerCase() + if (given_reason.length > 0) { reason += ' (' + given_reason + ')' } + } else { + special_note = $('#debit_type').val() + user_note = sources_notes_display[0].note + alias = sources_notes_display[0].name + source_id = user_note.id + dest_id = special_note + reason = 'Retrait ' + $('#debit_type option:selected').text().toLowerCase() + if (given_reason.length > 0) { reason += ' (' + given_reason + ')' } + } + $.post('/api/note/transaction/transaction/', + { + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: 1, + amount: amount, + reason: reason, + valid: true, + polymorphic_ctype: SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, + resourcetype: 'SpecialTransaction', + source: source_id, + source_alias: sources_notes_display.length ? alias : null, + destination: dest_id, + destination_alias: dests_notes_display.length ? alias : null, + last_name: $('#last_name').val(), + first_name: $('#first_name').val(), + bank: $('#bank').val() + }).done(function () { + addMsg(gettext('Credit/debit succeed!'), 'success', 10000) + if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) } + reset() + }).fail(function (err) { + const errObj = JSON.parse(err.responseText) + let error = errObj.detail ? errObj.detail : errObj.non_field_errors + if (!error) { error = err.responseText } + addMsg(interpolate(gettext('Credit/debit failed: %s'), [error]), 'danger', 10000) + LOCK = false + }) + } +}) diff --git a/apps/food/templates/food/basic_food_form.html b/apps/food/templates/food/basic_food_form.html index 43cf1c43..dbfb49e3 100644 --- a/apps/food/templates/food/basic_food_form.html +++ b/apps/food/templates/food/basic_food_form.html @@ -10,11 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later HTML not finished
{{ title }} -
+
{% csrf_token %} {{ form|crispy }} - {{ allergenform|crispy }}
diff --git a/apps/food/templates/food/basicfood_detail.html b/apps/food/templates/food/basicfood_detail.html new file mode 100644 index 00000000..86b7ac9e --- /dev/null +++ b/apps/food/templates/food/basicfood_detail.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} +
+

+ HTML not finished
+ {{ title }} +

+
+

name : {{ food.name }}

+ Update +
+
+{% endblock %} diff --git a/apps/food/templates/food/create_food_form.html b/apps/food/templates/food/create_food_form.html new file mode 100644 index 00000000..137c1880 --- /dev/null +++ b/apps/food/templates/food/create_food_form.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} + +{% block content %} +
+

+ HTML not finished
+ {{ title }} +

+
+
+ +
+
+
+{% endblock %} diff --git a/apps/food/templates/food/create_qrcode_form.html b/apps/food/templates/food/create_qrcode_form.html new file mode 100644 index 00000000..6af507ca --- /dev/null +++ b/apps/food/templates/food/create_qrcode_form.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} + +{% block content %} +
+

+ HTML not finished
+ {{ title }} +

+
+
+ +
+
+
+{% endblock %} diff --git a/apps/food/templates/food/qrcode_detail.html b/apps/food/templates/food/qrcode_detail.html new file mode 100644 index 00000000..5e11ecf4 --- /dev/null +++ b/apps/food/templates/food/qrcode_detail.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} +
+

+ HTML not finished
+ {{ title }} +

+
+

qrcode : {{ qrcode.qr_code_number }}

+

name : {{ qrcode.food_container.name }}

+ {% if qrcode.food_container.polymorphic_ctype.name == 'Basic food' %} + Update + {% else %} + Update + {% endif %} +
+
+{% endblock %} diff --git a/apps/food/templates/food/transformed_food_form.html b/apps/food/templates/food/transformed_food_form.html index fd18a5f8..86e3b03e 100644 --- a/apps/food/templates/food/transformed_food_form.html +++ b/apps/food/templates/food/transformed_food_form.html @@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later HTML not finished
{{ title }} -
+
{% csrf_token %} {{ form|crispy }} diff --git a/apps/food/templates/food/transformedfood_detail.html b/apps/food/templates/food/transformedfood_detail.html new file mode 100644 index 00000000..d73b0d09 --- /dev/null +++ b/apps/food/templates/food/transformedfood_detail.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} +
+

+ HTML not finished
+ {{ title }} +

+
+

name : {{ food.name }}

+

owner : {{ food.owner }}

+ Update +
+
+{% endblock %} diff --git a/apps/food/tests.py b/apps/food/tests.py index 7ce503c2..a79ca8be 100644 --- a/apps/food/tests.py +++ b/apps/food/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/apps/food/urls.py b/apps/food/urls.py index 7d4f54a3..c422e514 100644 --- a/apps/food/urls.py +++ b/apps/food/urls.py @@ -5,15 +5,19 @@ from django.urls import path from . import views -############################### -# TO DO -# - name url correctly, thinking about the scheme of the app -############################### - app_name = 'food' urlpatterns = [ - path('0', views.BasicFoodCreateView.as_view(), name = 'basic_food'), - path('1', views.TransformedFoodCreateView.as_view(), name = 'transformed_food'), -] + path('', views.QRCodeView.as_view(), name='qrcode_view'), + path('detail/', views.FoodView.as_view(), name='food_view'), + path('/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'), + path('create', views.FoodCreateView.as_view(), name='food_create'), + path('/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'), + path('/create_qrcode/transformed', views.QRCodeTransformedFoodCreateView.as_view(), name='qrcode_transformed_create'), + path('create/basic', views.BasicFoodCreateView.as_view(), name='basic_create'), + path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'), + + path('update/basic/', views.BasicFoodUpdateView.as_view(), name='basic_update'), + path('update/transformed/', views.TransformedFoodUpdateView.as_view(), name='transformed_update'), +] diff --git a/apps/food/views.py b/apps/food/views.py index edd00e74..5964f3f7 100644 --- a/apps/food/views.py +++ b/apps/food/views.py @@ -1,26 +1,74 @@ # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django.urls import reverse_lazy +from datetime import timedelta + from django.db import transaction +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponseRedirect +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.utils import timezone -from datetime import timedelta +from django.views.generic import DetailView, UpdateView, TemplateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView -from django.shortcuts import render -from .forms import BasicFoodForms, TransformedFoodForms, AllergenForms -from .models import BasicFood, TransformedFood, Allergen +from .forms import BasicFoodForms, TransformedFoodForms +from .models import BasicFood, Food, QRCode, TransformedFood -class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): -##################################################################### -# TO DO -# - fix picture save -# - implement solution crop and convert image (reuse or recode ImageForm from members apps -# - implement AllergenForms -# - redirect to another view after the poll is submitted -##################################################################### +class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + A view to add a basic food + """ + model = QRCode + extra_context = {"title": _("Add a new meal")} + context_object_name = "qrcode" + slug_field = "qr_code_number" + + def get(self, *args, **kwargs): + qrcode = kwargs["slug"] + if self.model.objects.filter(qr_code_number=qrcode).count() > 0: + return super().get(*args, **kwargs) + else: + return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs)) + + +class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView): + """ + A view to add a basic food + """ + template_name = 'food/create_qrcode_form.html' + extra_context = {"title": _("Add a new aliment")} + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["slug"] = kwargs["slug"] + return context + + +class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + A view to add a basic food + """ + model = Food + extra_context = {"title": _("Add a new meal")} + context_object_name = "food" + + +class FoodCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView): + """ + A view to add a basic food + """ + template_name = 'food/create_food_form.html' + extra_context = {"title": _("Add a new aliment")} + + +class BasicFoodFormView(ProtectQuerysetMixin): + ##################################################################### + # TO DO + # - fix picture save + # - implement solution crop and convert image (reuse or recode ImageForm from members apps) + ##################################################################### """ A view to add a basic food """ @@ -28,28 +76,18 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): form_class = BasicFoodForms template_name = 'food/basic_food_form.html' extra_context = {"title": _("Add a new aliment")} - - def get_sample_object(self): - return BasicFood( - name="", - is_DLC=False, - is_DDM=False, - expiry_date=timezone.now(), - label='pic/default.png', - ) - + @transaction.atomic def form_valid(self, form): form.instance.creater = self.request.user basic_food_form = BasicFoodForms(data=self.request.POST) - allergen_form = AllergenForms(data=self.request.POST) - if not basic_food_form.is_valid() or not allergen_form.is_valid(): + if not basic_food_form.is_valid(): return self.form_invalid(form) # Save the aliment and the allergens associed basic_food = form.save(commit=False) # We assume the date of labeling and the same as the date of arrival - basic_food.arrival_date = timezone.now + basic_food.arrival_date = timezone.now() basic_food._force_save = True basic_food.save() basic_food.refresh_from_db() @@ -57,15 +95,74 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): def get_success_url(self, **kwargs): self.object.refresh_from_db() - # TEMPORARY, I create a fonctionnal view before - # return reverse_lazy('food:basicfood', kwargs={"pk": self.object.pk}) - return '0' + return reverse('food:food_view', kwargs={"pk": self.object.pk}) -class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): -############################################### -# TO DO -# -redirect to another view after submit -############################################### + +class BasicFoodUpdateView(BasicFoodFormView, LoginRequiredMixin, UpdateView): + pass + + +class BasicFoodCreateView(BasicFoodFormView, ProtectedCreateView): + def get_sample_object(self): + return BasicFood( + name="", + expiry_date=timezone.now(), + ) + + +class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): + ##################################################################### + # TO DO + # - fix picture save + # - implement solution crop and convert image (reuse or recode ImageForm from members apps) + ##################################################################### + """ + A view to add a basic food + """ + model = BasicFood + form_class = BasicFoodForms + template_name = 'food/basic_food_form.html' + extra_context = {"title": _("Add a new aliment")} + + @transaction.atomic + def form_valid(self, form): + form.instance.creater = self.request.user + basic_food_form = BasicFoodForms(data=self.request.POST) + if not basic_food_form.is_valid(): + return self.form_invalid(form) + + # Save the aliment and the allergens associed + basic_food = form.save(commit=False) + # We assume the date of labeling and the same as the date of arrival + basic_food.arrival_date = timezone.now() + basic_food._force_save = True + basic_food.save() + basic_food.refresh_from_db() + + qrcode = QRCode() + qrcode.qr_code_number = self.kwargs['slug'] + qrcode.food_container = basic_food + qrcode.save() + + return super().form_valid(form) + + def get_success_url(self, **kwargs): + self.object.refresh_from_db() + return reverse('food:food_view', kwargs={"pk": self.object.pk}) + + def get_sample_object(self): + return BasicFood( + name="", + expiry_date=timezone.now(), + ) + + +class TransformedFoodFormView(ProtectQuerysetMixin): + ##################################################################### + # TO DO + # - fix picture save + # - implement solution crop and convert image (reuse or recode ImageForm from members apps) + ##################################################################### """ A view to add a tranformed food """ @@ -73,13 +170,7 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): template_name = 'food/transformed_food_form.html' form_class = TransformedFoodForms extra_context = {"title": _("Add a new meal")} - - def get_sample_object(self): - return TransformedFood( - name="", - creation_date=timezone.now(), - ) - + @transaction.atomic def form_valid(self, form): form.instance.creater = self.request.user @@ -90,15 +181,71 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): # Save the aliment and allergens associated transformed_food = form.save(commit=False) # Without microbiological analyzes, the storage time is 3 days - transformed_food.expiry_date = transformed_food.creation_date + timedelta(days = 3) - transformed_food._force_save = True + transformed_food.expiry_date = transformed_food.creation_date + timedelta(days=3) + transformed_food._force_save = True transformed_food.save() transformed_food.refresh_from_db() return super().form_valid(form) + def get_success_url(self, **kwargs): + self.object.refresh_from_db() + return reverse('food:food_view', kwargs={"pk": self.object.pk}) + + +class TransformedFoodUpdateView(TransformedFoodFormView, LoginRequiredMixin, UpdateView): + pass + + +class TransformedFoodCreateView(TransformedFoodFormView, ProtectedCreateView): + def get_sample_object(self): + return TransformedFood( + name="", + creation_date=timezone.now(), + ) + + +class QRCodeTransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): + ##################################################################### + # TO DO + # - fix picture save + # - implement solution crop and convert image (reuse or recode ImageForm from members apps) + ##################################################################### + """ + A view to add a basic food + """ + model = TransformedFood + template_name = 'food/transformed_food_form.html' + form_class = TransformedFoodForms + extra_context = {"title": _("Add a new meal")} + + @transaction.atomic + def form_valid(self, form): + form.instance.creater = self.request.user + transformed_food_form = TransformedFoodForms(data=self.request.POST) + if not transformed_food_form.is_valid(): + return self.form_invalid(form) + + # Save the aliment and allergens associated + transformed_food = form.save(commit=False) + # Without microbiological analyzes, the storage time is 3 days + transformed_food.expiry_date = transformed_food.creation_date + timedelta(days=3) + transformed_food._force_save = True + transformed_food.save() + transformed_food.refresh_from_db() + + qrcode = QRCode() + qrcode.qr_code_number = self.kwargs['slug'] + qrcode.food_container = transformed_food + qrcode.save() + + return super().form_valid(form) def get_success_url(self, **kwargs): self.object.refresh_from_db() - # TEMPORARY, I create a fonctionnal view before - # return reverse_lazy('food:tranformed_food', kwargs={"pk": self.object.pk}) - return '1' + return reverse('food:food_view', kwargs={"pk": self.object.pk}) + + def get_sample_object(self): + return BasicFood( + name="", + expiry_date=timezone.now(), + )