Coverage for cookbook/serializer.py: 85%
941 statements
« prev ^ index » next coverage.py v7.4.0, created at 2023-12-29 01:02 +0100
« prev ^ index » next coverage.py v7.4.0, created at 2023-12-29 01:02 +0100
1import traceback
2import uuid
3from datetime import datetime, timedelta
4from decimal import Decimal
5from gettext import gettext as _
6from html import escape
7from smtplib import SMTPException
9from django.contrib.auth.models import AnonymousUser, Group, User
10from django.core.cache import caches
11from django.core.mail import send_mail
12from django.db.models import Q, QuerySet, Sum
13from django.http import BadHeaderError
14from django.urls import reverse
15from django.utils import timezone
16from django_scopes import scopes_disabled
17from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
18from oauth2_provider.models import AccessToken
19from PIL import Image
20from rest_framework import serializers
21from rest_framework.exceptions import NotFound, ValidationError
22from rest_framework.fields import IntegerField
24from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
25from cookbook.helper.HelperFunctions import str2bool
26from cookbook.helper.permission_helper import above_space_limit
27from cookbook.helper.property_helper import FoodPropertyHelper
28from cookbook.helper.shopping_helper import RecipeShoppingEditor
29from cookbook.helper.unit_conversion_helper import UnitConversionHelper
30from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
31 ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
32 Keyword, MealPlan, MealType, NutritionInformation, Property,
33 PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
34 ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
35 Step, Storage, Supermarket, SupermarketCategory,
36 SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
37 UserFile, UserPreference, UserSpace, ViewLog)
38from cookbook.templatetags.custom_tags import markdown
39from recipes.settings import AWS_ENABLED, MEDIA_URL
42class ExtendedRecipeMixin(serializers.ModelSerializer):
43 # adds image and recipe count to serializer when query param extended=1
44 # ORM path to this object from Recipe
45 recipe_filter = None
46 # list of ORM paths to any image
47 images = None
49 image = serializers.SerializerMethodField('get_image')
50 numrecipe = serializers.ReadOnlyField(source='recipe_count')
52 def get_fields(self, *args, **kwargs):
53 fields = super().get_fields(*args, **kwargs)
54 try:
55 api_serializer = self.context['view'].serializer_class
56 except KeyError:
57 api_serializer = None
58 # extended values are computationally expensive and not needed in normal circumstances
59 try:
60 if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
61 return fields
62 except (AttributeError, KeyError):
63 pass
64 try:
65 del fields['image']
66 del fields['numrecipe']
67 except KeyError:
68 pass
69 return fields
71 def get_image(self, obj):
72 if obj.recipe_image:
73 if AWS_ENABLED:
74 storage = CachedS3Boto3Storage()
75 path = storage.url(obj.recipe_image)
76 else:
77 path = MEDIA_URL + obj.recipe_image
78 return path
81class OpenDataModelMixin(serializers.ModelSerializer):
83 def create(self, validated_data):
84 if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
85 validated_data['open_data_slug'] = None
86 return super().create(validated_data)
88 def update(self, instance, validated_data):
89 if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
90 validated_data['open_data_slug'] = None
91 return super().update(instance, validated_data)
94class CustomDecimalField(serializers.Field):
95 """
96 Custom decimal field to normalize useless decimal places
97 and allow commas as decimal separators
98 """
100 def to_representation(self, value):
101 if not isinstance(value, Decimal):
102 value = Decimal(value)
103 return round(value, 2).normalize()
105 def to_internal_value(self, data):
106 if isinstance(data, int) or isinstance(data, float):
107 return data
108 elif isinstance(data, str):
109 if data == '':
110 return 0
111 try:
112 return float(data.replace(',', '.'))
113 except ValueError:
114 raise ValidationError('A valid number is required')
117class CustomOnHandField(serializers.Field):
118 def get_attribute(self, instance):
119 return instance
121 def to_representation(self, obj):
122 if not self.context["request"].user.is_authenticated:
123 return []
124 shared_users = []
125 if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
126 shared_users = c
127 else:
128 try:
129 shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
130 self.context['request'].user.id]
131 caches['default'].set(
132 f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
133 shared_users, timeout=5 * 60)
134 # TODO ugly hack that improves API performance significantly, should be done properly
135 except AttributeError: # Anonymous users (using share links) don't have shared users
136 pass
137 return obj.onhand_users.filter(id__in=shared_users).exists()
139 def to_internal_value(self, data):
140 return data
143class SpaceFilterSerializer(serializers.ListSerializer):
145 def to_representation(self, data):
146 if self.context.get('request', None) is None:
147 return
148 if (isinstance(data, QuerySet) and data.query.is_sliced):
149 # if query is sliced it came from api request not nested serializer
150 return super().to_representation(data)
151 if self.child.Meta.model == User:
152 if isinstance(self.context['request'].user, AnonymousUser):
153 data = []
154 else:
155 data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
156 else:
157 data = data.filter(**{'__'.join(data.model.get_space_key()): self.context['request'].space})
158 return super().to_representation(data)
161class UserSerializer(WritableNestedModelSerializer):
162 display_name = serializers.SerializerMethodField('get_user_label')
164 def get_user_label(self, obj):
165 return obj.get_user_display_name()
167 class Meta:
168 list_serializer_class = SpaceFilterSerializer
169 model = User
170 fields = ('id', 'username', 'first_name', 'last_name', 'display_name')
171 read_only_fields = ('username',)
174class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
175 def create(self, validated_data):
176 raise ValidationError('Cannot create using this endpoint')
178 def update(self, instance, validated_data):
179 return instance # cannot update group
181 class Meta:
182 model = Group
183 fields = ('id', 'name')
186class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
187 name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
188 field = serializers.CharField(allow_null=True, allow_blank=True, required=False)
190 def create(self, validated_data):
191 raise ValidationError('Cannot create using this endpoint')
193 def update(self, instance, validated_data):
194 return instance
196 class Meta:
197 model = FoodInheritField
198 fields = ('id', 'name', 'field',)
199 read_only_fields = ['id']
202class UserFileSerializer(serializers.ModelSerializer):
203 file = serializers.FileField(write_only=True)
204 file_download = serializers.SerializerMethodField('get_download_link')
205 preview = serializers.SerializerMethodField('get_preview_link')
207 def get_download_link(self, obj):
208 return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
210 def get_preview_link(self, obj):
211 try:
212 Image.open(obj.file.file.file)
213 return self.context['request'].build_absolute_uri(obj.file.url)
214 except Exception:
215 traceback.print_exc()
216 return ""
218 def check_file_limit(self, validated_data):
219 if 'file' in validated_data:
220 if self.context['request'].space.max_file_storage_mb == -1:
221 raise ValidationError(_('File uploads are not enabled for this Space.'))
223 try:
224 current_file_size_mb = \
225 UserFile.objects.filter(space=self.context['request'].space).aggregate(Sum('file_size_kb'))[
226 'file_size_kb__sum'] / 1000
227 except TypeError:
228 current_file_size_mb = 0
230 if ((validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5)
231 > self.context['request'].space.max_file_storage_mb != 0):
232 raise ValidationError(_('You have reached your file upload limit.'))
234 def create(self, validated_data):
235 self.check_file_limit(validated_data)
236 validated_data['created_by'] = self.context['request'].user
237 validated_data['space'] = self.context['request'].space
238 return super().create(validated_data)
240 def update(self, instance, validated_data):
241 self.check_file_limit(validated_data)
242 return super().update(instance, validated_data)
244 class Meta:
245 model = UserFile
246 fields = ('id', 'name', 'file', 'file_download', 'preview', 'file_size_kb')
247 read_only_fields = ('id', 'file_size_kb')
248 extra_kwargs = {"file": {"required": False, }}
251class UserFileViewSerializer(serializers.ModelSerializer):
252 file_download = serializers.SerializerMethodField('get_download_link')
253 preview = serializers.SerializerMethodField('get_preview_link')
255 def get_download_link(self, obj):
256 return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
258 def get_preview_link(self, obj):
259 try:
260 Image.open(obj.file.file.file)
261 return self.context['request'].build_absolute_uri(obj.file.url)
262 except Exception:
263 traceback.print_exc()
264 return ""
266 def create(self, validated_data):
267 raise ValidationError('Cannot create File over this view')
269 def update(self, instance, validated_data):
270 return instance
272 class Meta:
273 model = UserFile
274 fields = ('id', 'name', 'file_download', 'preview')
275 read_only_fields = ('id', 'file')
278class SpaceSerializer(WritableNestedModelSerializer):
279 user_count = serializers.SerializerMethodField('get_user_count')
280 recipe_count = serializers.SerializerMethodField('get_recipe_count')
281 file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
282 food_inherit = FoodInheritFieldSerializer(many=True)
283 image = UserFileViewSerializer(required=False, many=False, allow_null=True)
285 def get_user_count(self, obj):
286 return UserSpace.objects.filter(space=obj).count()
288 def get_recipe_count(self, obj):
289 return Recipe.objects.filter(space=obj).count()
291 def get_file_size_mb(self, obj):
292 try:
293 return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
294 except TypeError:
295 return 0
297 def create(self, validated_data):
298 raise ValidationError('Cannot create using this endpoint')
300 class Meta:
301 model = Space
302 fields = (
303 'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
304 'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
305 'image', 'use_plural',)
306 read_only_fields = (
307 'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
308 'demo',)
311class UserSpaceSerializer(WritableNestedModelSerializer):
312 user = UserSerializer(read_only=True)
313 groups = GroupSerializer(many=True)
315 def validate(self, data):
316 if self.instance.user == self.context['request'].space.created_by: # can't change space owner permission
317 raise serializers.ValidationError(_('Cannot modify Space owner permission.'))
318 return super().validate(data)
320 def create(self, validated_data):
321 raise ValidationError('Cannot create using this endpoint')
323 class Meta:
324 model = UserSpace
325 fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
326 read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
329class SpacedModelSerializer(serializers.ModelSerializer):
330 def create(self, validated_data):
331 validated_data['space'] = self.context['request'].space
332 return super().create(validated_data)
335class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
337 def create(self, validated_data):
338 validated_data['name'] = validated_data['name'].strip()
339 space = validated_data.pop('space', self.context['request'].space)
340 validated_data['created_by'] = self.context['request'].user
341 obj, created = MealType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
342 return obj
344 class Meta:
345 list_serializer_class = SpaceFilterSerializer
346 model = MealType
347 fields = ('id', 'name', 'order', 'color', 'default', 'created_by')
348 read_only_fields = ('created_by',)
351class UserPreferenceSerializer(WritableNestedModelSerializer):
352 food_inherit_default = serializers.SerializerMethodField('get_food_inherit_defaults')
353 plan_share = UserSerializer(many=True, allow_null=True, required=False)
354 shopping_share = UserSerializer(many=True, allow_null=True, required=False)
355 food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
356 image = UserFileViewSerializer(required=False, allow_null=True, many=False)
358 def get_food_inherit_defaults(self, obj):
359 return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
361 def get_food_children_exist(self, obj):
362 space = getattr(self.context.get('request', None), 'space', None)
363 return Food.objects.filter(depth__gt=0, space=space).exists()
365 def update(self, instance, validated_data):
366 with scopes_disabled():
367 return super().update(instance, validated_data)
369 def create(self, validated_data):
370 raise ValidationError('Cannot create using this endpoint')
372 class Meta:
373 model = UserPreference
374 fields = (
375 'user', 'image', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
376 'plan_share', 'sticky_navbar',
377 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
378 'food_inherit_default', 'default_delay',
379 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
380 'csv_delim', 'csv_prefix',
381 'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients', 'food_children_exist', 'ingredient_context'
382 )
385class StorageSerializer(SpacedModelSerializer):
387 def create(self, validated_data):
388 validated_data['created_by'] = self.context['request'].user
389 return super().create(validated_data)
391 class Meta:
392 model = Storage
393 fields = (
394 'id', 'name', 'method', 'username', 'password',
395 'token', 'created_by'
396 )
398 read_only_fields = ('created_by',)
400 extra_kwargs = {
401 'password': {'write_only': True},
402 'token': {'write_only': True},
403 }
406class SyncSerializer(SpacedModelSerializer):
407 class Meta:
408 model = Sync
409 fields = (
410 'id', 'storage', 'path', 'active', 'last_checked',
411 'created_at', 'updated_at'
412 )
415class SyncLogSerializer(SpacedModelSerializer):
416 class Meta:
417 model = SyncLog
418 fields = ('id', 'sync', 'status', 'msg', 'created_at')
421class KeywordLabelSerializer(serializers.ModelSerializer):
422 label = serializers.SerializerMethodField('get_label')
424 def get_label(self, obj):
425 return str(obj)
427 class Meta:
428 list_serializer_class = SpaceFilterSerializer
429 model = Keyword
430 fields = (
431 'id', 'label',
432 )
433 read_only_fields = ('id', 'label')
436class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
437 label = serializers.SerializerMethodField('get_label')
438 recipe_filter = 'keywords'
440 def get_label(self, obj):
441 return str(obj)
443 def create(self, validated_data):
444 # since multi select tags dont have id's
445 # duplicate names might be routed to create
446 name = validated_data.pop('name').strip()
447 space = validated_data.pop('space', self.context['request'].space)
448 obj, created = Keyword.objects.get_or_create(name=name, space=space, defaults=validated_data)
449 return obj
451 class Meta:
452 model = Keyword
453 fields = (
454 'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
455 'updated_at', 'full_name')
456 read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
459class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin):
460 recipe_filter = 'steps__ingredients__unit'
462 def create(self, validated_data):
463 # get_or_create drops any field that contains '__' when creating so values must be included in validated data
464 space = validated_data.pop('space', self.context['request'].space)
465 if x := validated_data.get('name', None):
466 validated_data['name'] = x.strip()
467 if x := validated_data.get('name', None):
468 validated_data['plural_name'] = x.strip()
470 if unit := Unit.objects.filter(Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']), space=space).first():
471 return unit
473 obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
474 return obj
476 def update(self, instance, validated_data):
477 validated_data['name'] = validated_data['name'].strip()
478 if plural_name := validated_data.get('plural_name', None):
479 validated_data['plural_name'] = plural_name.strip()
480 return super(UnitSerializer, self).update(instance, validated_data)
482 class Meta:
483 model = Unit
484 fields = ('id', 'name', 'plural_name', 'description', 'base_unit', 'numrecipe', 'image', 'open_data_slug')
485 read_only_fields = ('id', 'numrecipe', 'image')
488class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin):
490 def create(self, validated_data):
491 validated_data['name'] = validated_data['name'].strip()
492 space = validated_data.pop('space', self.context['request'].space)
493 obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
494 return obj
496 def update(self, instance, validated_data):
497 return super(SupermarketCategorySerializer, self).update(instance, validated_data)
499 class Meta:
500 model = SupermarketCategory
501 fields = ('id', 'name', 'description')
504class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
505 category = SupermarketCategorySerializer()
507 class Meta:
508 model = SupermarketCategoryRelation
509 fields = ('id', 'category', 'supermarket', 'order')
512class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin):
513 category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
515 def create(self, validated_data):
516 validated_data['name'] = validated_data['name'].strip()
517 space = validated_data.pop('space', self.context['request'].space)
518 obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
519 return obj
521 class Meta:
522 model = Supermarket
523 fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
526class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
527 id = serializers.IntegerField(required=False)
528 order = IntegerField(default=0, required=False)
530 def create(self, validated_data):
531 validated_data['name'] = validated_data['name'].strip()
532 space = validated_data.pop('space', self.context['request'].space)
533 obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
534 return obj
536 class Meta:
537 model = PropertyType
538 fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug', 'fdc_id',)
541class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
542 property_type = PropertyTypeSerializer()
543 property_amount = CustomDecimalField()
545 def create(self, validated_data):
546 validated_data['space'] = self.context['request'].space
547 return super().create(validated_data)
549 class Meta:
550 model = Property
551 fields = ('id', 'property_amount', 'property_type')
554class RecipeSimpleSerializer(WritableNestedModelSerializer):
555 url = serializers.SerializerMethodField('get_url')
557 def get_url(self, obj):
558 return reverse('view_recipe', args=[obj.id])
560 def create(self, validated_data):
561 # don't allow writing to Recipe via this API
562 return Recipe.objects.get(**validated_data)
564 def update(self, instance, validated_data):
565 # don't allow writing to Recipe via this API
566 return Recipe.objects.get(**validated_data)
568 class Meta:
569 model = Recipe
570 fields = ('id', 'name', 'url')
573class FoodSimpleSerializer(serializers.ModelSerializer):
574 class Meta:
575 model = Food
576 fields = ('id', 'name', 'plural_name')
579class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
580 supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
581 recipe = RecipeSimpleSerializer(allow_null=True, required=False)
582 shopping = serializers.SerializerMethodField('get_shopping_status')
583 # shopping = serializers.ReadOnlyField(source='shopping_status') # reverting to serializer method as annotations on get_queryset don't execute when on nested serializers
584 inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
585 child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
586 food_onhand = CustomOnHandField(required=False, allow_null=True)
587 substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
588 substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
590 properties = PropertySerializer(many=True, allow_null=True, required=False)
591 properties_food_unit = UnitSerializer(allow_null=True, required=False)
592 properties_food_amount = CustomDecimalField(required=False)
594 recipe_filter = 'steps__ingredients__food'
595 images = ['recipe__image']
597 def get_substitute_onhand(self, obj):
598 if not self.context["request"].user.is_authenticated:
599 return []
600 shared_users = []
601 if c := caches['default'].get(
602 f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
603 shared_users = c
604 else:
605 try:
606 shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
607 self.context['request'].user.id]
608 caches['default'].set(
609 f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
610 shared_users, timeout=5 * 60)
611 # TODO ugly hack that improves API performance significantly, should be done properly
612 except AttributeError: # Anonymous users (using share links) don't have shared users
613 pass
614 filter = Q(id__in=obj.substitute.all())
615 if obj.substitute_siblings:
616 filter |= Q(path__startswith=obj.path[:Food.steplen * (obj.depth - 1)], depth=obj.depth)
617 if obj.substitute_children:
618 filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
619 return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exclude(id=obj.id).exists()
621 def get_shopping_status(self, obj):
622 return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).exists()
624 def create(self, validated_data):
625 name = validated_data['name'].strip()
627 if plural_name := validated_data.pop('plural_name', None):
628 plural_name = plural_name.strip()
630 if food := Food.objects.filter(Q(name=name) | Q(plural_name=name)).first():
631 return food
633 space = validated_data.pop('space', self.context['request'].space)
634 # supermarket category needs to be handled manually as food.get or create does not create nested serializers unlike a super.create of serializer
635 if 'supermarket_category' in validated_data and validated_data['supermarket_category']:
636 sm_category = validated_data['supermarket_category']
637 sc_name = sm_category.pop('name', None)
638 validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
639 name=sc_name,
640 space=space, defaults=sm_category)
641 onhand = validated_data.pop('food_onhand', None)
642 if recipe := validated_data.get('recipe', None):
643 validated_data['recipe'] = Recipe.objects.get(**recipe)
645 # assuming if on hand for user also onhand for shopping_share users
646 if onhand is not None:
647 shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
648 if self.instance:
649 onhand_users = self.instance.onhand_users.all()
650 else:
651 onhand_users = []
652 if onhand:
653 validated_data['onhand_users'] = list(onhand_users) + shared_users
654 else:
655 validated_data['onhand_users'] = list(set(onhand_users) - set(shared_users))
657 if properties_food_unit := validated_data.pop('properties_food_unit', None):
658 properties_food_unit = Unit.objects.filter(name=properties_food_unit['name']).first()
660 properties = validated_data.pop('properties', None)
662 obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, properties_food_unit=properties_food_unit,
663 defaults=validated_data)
665 if properties and len(properties) > 0:
666 for p in properties:
667 obj.properties.add(Property.objects.create(property_type_id=p['property_type']['id'], property_amount=p['property_amount'], space=space))
669 return obj
671 def update(self, instance, validated_data):
672 if name := validated_data.get('name', None):
673 validated_data['name'] = name.strip()
674 if plural_name := validated_data.get('plural_name', None):
675 validated_data['plural_name'] = plural_name.strip()
676 # assuming if on hand for user also onhand for shopping_share users
677 onhand = validated_data.get('food_onhand', None)
678 reset_inherit = self.initial_data.get('reset_inherit', False)
679 if onhand is not None:
680 shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
681 if onhand:
682 validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
683 else:
684 validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users))
686 # update before resetting inheritance
687 saved_instance = super(FoodSerializer, self).update(instance, validated_data)
688 if reset_inherit and (r := self.context.get('request', None)):
689 Food.reset_inheritance(food=saved_instance, space=r.space)
690 return saved_instance
692 class Meta:
693 model = Food
694 fields = (
695 'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
696 'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
697 'food_onhand', 'supermarket_category',
698 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
699 'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
700 )
701 read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
704class IngredientSimpleSerializer(WritableNestedModelSerializer):
705 food = FoodSimpleSerializer(allow_null=True)
706 unit = UnitSerializer(allow_null=True)
707 used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
708 amount = CustomDecimalField()
709 conversions = serializers.SerializerMethodField('get_conversions')
711 def get_used_in_recipes(self, obj):
712 used_in = []
713 for s in obj.step_set.all():
714 for r in s.recipe_set.all():
715 used_in.append({'id': r.id, 'name': r.name})
716 return used_in
718 def get_conversions(self, obj):
719 if obj.unit and obj.food:
720 uch = UnitConversionHelper(self.context['request'].space)
721 conversions = []
722 for c in uch.get_conversions(obj):
723 conversions.append({'food': c.food.name, 'unit': c.unit.name, 'amount': c.amount}) # TODO do formatting in helper
724 return conversions
725 else:
726 return []
728 def create(self, validated_data):
729 validated_data['space'] = self.context['request'].space
730 return super().create(validated_data)
732 def update(self, instance, validated_data):
733 validated_data.pop('original_text', None)
734 return super().update(instance, validated_data)
736 class Meta:
737 model = Ingredient
738 fields = (
739 'id', 'food', 'unit', 'amount', 'conversions', 'note', 'order',
740 'is_header', 'no_amount', 'original_text', 'used_in_recipes',
741 'always_use_plural_unit', 'always_use_plural_food',
742 )
743 read_only_fields = ['conversions', ]
746class IngredientSerializer(IngredientSimpleSerializer):
747 food = FoodSerializer(allow_null=True)
750class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
751 ingredients = IngredientSerializer(many=True)
752 ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
753 ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
754 file = UserFileViewSerializer(allow_null=True, required=False)
755 step_recipe_data = serializers.SerializerMethodField('get_step_recipe_data')
756 recipe_filter = 'steps'
758 def create(self, validated_data):
759 validated_data['space'] = self.context['request'].space
760 return super().create(validated_data)
762 def get_ingredients_vue(self, obj):
763 return obj.get_instruction_render()
765 def get_ingredients_markdown(self, obj):
766 return obj.get_instruction_render()
768 def get_step_recipes(self, obj):
769 return list(obj.recipe_set.values_list('id', flat=True).all())
771 def get_step_recipe_data(self, obj):
772 # check if root type is recipe to prevent infinite recursion
773 # can be improved later to allow multi level embedding
774 if obj.step_recipe and isinstance(self.parent.root, RecipeSerializer):
775 return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
777 class Meta:
778 model = Step
779 fields = (
780 'id', 'name', 'instruction', 'ingredients', 'ingredients_markdown',
781 'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe',
782 'step_recipe_data', 'numrecipe', 'show_ingredients_table'
783 )
786class StepRecipeSerializer(WritableNestedModelSerializer):
787 steps = StepSerializer(many=True)
789 class Meta:
790 model = Recipe
791 fields = (
792 'id', 'name', 'steps',
793 )
796class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin):
797 name = serializers.SerializerMethodField('get_conversion_name')
798 base_unit = UnitSerializer()
799 converted_unit = UnitSerializer()
800 food = FoodSerializer(allow_null=True, required=False)
801 base_amount = CustomDecimalField()
802 converted_amount = CustomDecimalField()
804 def get_conversion_name(self, obj):
805 text = f'{round(obj.base_amount)} {obj.base_unit} '
806 if obj.food:
807 text += f' {obj.food}'
808 return text + f' = {round(obj.converted_amount)} {obj.converted_unit}'
810 def create(self, validated_data):
811 validated_data['space'] = validated_data.pop('space', self.context['request'].space)
812 try:
813 return UnitConversion.objects.get(
814 food__name__iexact=validated_data.get('food', {}).get('name', None),
815 base_unit__name__iexact=validated_data.get('base_unit', {}).get('name', None),
816 converted_unit__name__iexact=validated_data.get('converted_unit', {}).get('name', None),
817 space=validated_data['space']
818 )
819 except UnitConversion.DoesNotExist:
820 validated_data['created_by'] = self.context['request'].user
821 return super().create(validated_data)
823 class Meta:
824 model = UnitConversion
825 fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
828class NutritionInformationSerializer(serializers.ModelSerializer):
829 carbohydrates = CustomDecimalField()
830 fats = CustomDecimalField()
831 proteins = CustomDecimalField()
832 calories = CustomDecimalField()
834 def create(self, validated_data):
835 validated_data['space'] = self.context['request'].space
836 return super().create(validated_data)
838 class Meta:
839 model = NutritionInformation
840 fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source')
843class RecipeBaseSerializer(WritableNestedModelSerializer):
844 # TODO make days of new recipe a setting
845 def is_recipe_new(self, obj):
846 if getattr(obj, 'new_recipe', None) or obj.created_at > (timezone.now() - timedelta(days=7)):
847 return True
848 else:
849 return False
852class RecipeOverviewSerializer(RecipeBaseSerializer):
853 keywords = KeywordLabelSerializer(many=True)
854 new = serializers.SerializerMethodField('is_recipe_new')
855 recent = serializers.ReadOnlyField()
857 rating = CustomDecimalField(required=False, allow_null=True)
858 last_cooked = serializers.DateTimeField(required=False, allow_null=True)
860 def create(self, validated_data):
861 pass
863 def update(self, instance, validated_data):
864 return instance
866 class Meta:
867 model = Recipe
868 fields = (
869 'id', 'name', 'description', 'image', 'keywords', 'working_time',
870 'waiting_time', 'created_by', 'created_at', 'updated_at',
871 'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
872 )
873 read_only_fields = ['image', 'created_by', 'created_at']
876class RecipeSerializer(RecipeBaseSerializer):
877 nutrition = NutritionInformationSerializer(allow_null=True, required=False)
878 properties = PropertySerializer(many=True, required=False)
879 steps = StepSerializer(many=True)
880 keywords = KeywordSerializer(many=True)
881 shared = UserSerializer(many=True, required=False)
882 rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
883 last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
884 food_properties = serializers.SerializerMethodField('get_food_properties')
886 def get_food_properties(self, obj):
887 fph = FoodPropertyHelper(obj.space) # initialize with object space since recipes might be viewed anonymously
888 return fph.calculate_recipe_properties(obj)
890 class Meta:
891 model = Recipe
892 fields = (
893 'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
894 'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
895 'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
896 'last_cooked',
897 'private', 'shared',
898 )
899 read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
901 def validate(self, data):
902 above_limit, msg = above_space_limit(self.context['request'].space)
903 if above_limit:
904 raise serializers.ValidationError(msg)
905 return super().validate(data)
907 def create(self, validated_data):
908 validated_data['created_by'] = self.context['request'].user
909 validated_data['space'] = self.context['request'].space
910 return super().create(validated_data)
913class RecipeImageSerializer(WritableNestedModelSerializer):
914 image = serializers.ImageField(required=False, allow_null=True)
915 image_url = serializers.CharField(max_length=4096, required=False, allow_null=True)
917 class Meta:
918 model = Recipe
919 fields = ['image', 'image_url', ]
922class RecipeImportSerializer(SpacedModelSerializer):
923 class Meta:
924 model = RecipeImport
925 fields = '__all__'
928class CommentSerializer(serializers.ModelSerializer):
929 class Meta:
930 model = Comment
931 fields = '__all__'
934class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
935 shared = UserSerializer(many=True, required=False)
937 def create(self, validated_data):
938 validated_data['created_by'] = self.context['request'].user
939 return super().create(validated_data)
941 class Meta:
942 model = CustomFilter
943 fields = ('id', 'name', 'search', 'shared', 'created_by')
944 read_only_fields = ('created_by',)
947class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
948 shared = UserSerializer(many=True)
949 filter = CustomFilterSerializer(allow_null=True, required=False)
951 def create(self, validated_data):
952 validated_data['created_by'] = self.context['request'].user
953 return super().create(validated_data)
955 class Meta:
956 model = RecipeBook
957 fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter')
958 read_only_fields = ('created_by',)
961class RecipeBookEntrySerializer(serializers.ModelSerializer):
962 book_content = serializers.SerializerMethodField(method_name='get_book_content', read_only=True)
963 recipe_content = serializers.SerializerMethodField(method_name='get_recipe_content', read_only=True)
965 def get_book_content(self, obj):
966 return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book)
968 def get_recipe_content(self, obj):
969 return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
971 def create(self, validated_data):
972 book = validated_data['book']
973 recipe = validated_data['recipe']
974 if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
975 raise NotFound(detail=None, code=None)
976 obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
977 return obj
979 class Meta:
980 model = RecipeBookEntry
981 fields = ('id', 'book', 'book_content', 'recipe', 'recipe_content',)
984class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
985 recipe = RecipeOverviewSerializer(required=False, allow_null=True)
986 recipe_name = serializers.ReadOnlyField(source='recipe.name')
987 meal_type = MealTypeSerializer()
988 meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
989 note_markdown = serializers.SerializerMethodField('get_note_markdown')
990 servings = CustomDecimalField()
991 shared = UserSerializer(many=True, required=False, allow_null=True)
992 shopping = serializers.SerializerMethodField('in_shopping')
994 to_date = serializers.DateField(required=False)
996 def get_note_markdown(self, obj):
997 return markdown(obj.note)
999 def in_shopping(self, obj):
1000 return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists()
1002 def create(self, validated_data):
1003 validated_data['created_by'] = self.context['request'].user
1005 if 'to_date' not in validated_data or validated_data['to_date'] is None:
1006 validated_data['to_date'] = validated_data['from_date']
1008 mealplan = super().create(validated_data)
1009 if self.context['request'].data.get('addshopping', False) and self.context['request'].data.get('recipe', None):
1010 SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
1011 SLR.create(mealplan=mealplan, servings=validated_data['servings'])
1012 return mealplan
1014 class Meta:
1015 model = MealPlan
1016 fields = (
1017 'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
1018 'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name',
1019 'meal_type_name', 'shopping'
1020 )
1021 read_only_fields = ('created_by',)
1024class AutoMealPlanSerializer(serializers.Serializer):
1025 start_date = serializers.DateField()
1026 end_date = serializers.DateField()
1027 meal_type_id = serializers.IntegerField()
1028 keywords = KeywordSerializer(many=True)
1029 servings = CustomDecimalField()
1030 shared = UserSerializer(many=True, required=False, allow_null=True)
1031 addshopping = serializers.BooleanField()
1034class ShoppingListRecipeSerializer(serializers.ModelSerializer):
1035 name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
1036 recipe_name = serializers.ReadOnlyField(source='recipe.name')
1037 mealplan_note = serializers.ReadOnlyField(source='mealplan.note')
1038 servings = CustomDecimalField()
1040 def get_name(self, obj):
1041 if not isinstance(value := obj.servings, Decimal):
1042 value = Decimal(value)
1043 value = value.quantize(
1044 Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
1045 return (
1046 obj.name
1047 or getattr(obj.mealplan, 'title', None)
1048 or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
1049 or obj.recipe.name
1050 ) + f' ({value:.2g})'
1052 def update(self, instance, validated_data):
1053 # TODO remove once old shopping list
1054 if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
1055 SLR = RecipeShoppingEditor(user=self.context['request'].user, space=self.context['request'].space)
1056 SLR.edit_servings(servings=validated_data['servings'], id=instance.id)
1057 return super().update(instance, validated_data)
1059 class Meta:
1060 model = ShoppingListRecipe
1061 fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note')
1062 read_only_fields = ('id',)
1065class ShoppingListEntrySerializer(WritableNestedModelSerializer):
1066 food = FoodSerializer(allow_null=True)
1067 unit = UnitSerializer(allow_null=True, required=False)
1068 ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
1069 recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
1070 amount = CustomDecimalField()
1071 created_by = UserSerializer(read_only=True)
1072 completed_at = serializers.DateTimeField(allow_null=True, required=False)
1074 def get_fields(self, *args, **kwargs):
1075 fields = super().get_fields(*args, **kwargs)
1077 # autosync values are only needed for frequent 'checked' value updating
1078 if self.context['request'] and bool(int(self.context['request'].query_params.get('autosync', False))):
1079 for f in list(set(fields) - set(['id', 'checked'])):
1080 del fields[f]
1081 return fields
1083 def run_validation(self, data):
1084 if self.root.instance.__class__.__name__ == 'ShoppingListEntry':
1085 if (
1086 data.get('checked', False)
1087 and self.root.instance
1088 and not self.root.instance.checked
1089 ):
1090 # if checked flips from false to true set completed datetime
1091 data['completed_at'] = timezone.now()
1093 elif not data.get('checked', False):
1094 # if not checked set completed to None
1095 data['completed_at'] = None
1096 else:
1097 # otherwise don't write anything
1098 if 'completed_at' in data:
1099 del data['completed_at']
1101 return super().run_validation(data)
1103 def create(self, validated_data):
1104 validated_data['space'] = self.context['request'].space
1105 validated_data['created_by'] = self.context['request'].user
1106 return super().create(validated_data)
1108 def update(self, instance, validated_data):
1109 user = self.context['request'].user
1110 # update the onhand for food if shopping_add_onhand is True
1111 if user.userpreference.shopping_add_onhand:
1112 if checked := validated_data.get('checked', None):
1113 instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
1114 elif checked == False:
1115 instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)
1116 return super().update(instance, validated_data)
1118 class Meta:
1119 model = ShoppingListEntry
1120 fields = (
1121 'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked',
1122 'recipe_mealplan',
1123 'created_by', 'created_at', 'completed_at', 'delay_until'
1124 )
1125 read_only_fields = ('id', 'created_by', 'created_at',)
1128# TODO deprecate
1129class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
1130 class Meta:
1131 model = ShoppingListEntry
1132 fields = ('id', 'checked')
1135# TODO deprecate
1136class ShoppingListSerializer(WritableNestedModelSerializer):
1137 recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
1138 entries = ShoppingListEntrySerializer(many=True, allow_null=True)
1139 shared = UserSerializer(many=True)
1140 supermarket = SupermarketSerializer(allow_null=True)
1142 def create(self, validated_data):
1143 validated_data['space'] = self.context['request'].space
1144 validated_data['created_by'] = self.context['request'].user
1145 return super().create(validated_data)
1147 class Meta:
1148 model = ShoppingList
1149 fields = (
1150 'id', 'uuid', 'note', 'recipes', 'entries',
1151 'shared', 'finished', 'supermarket', 'created_by', 'created_at'
1152 )
1153 read_only_fields = ('id', 'created_by',)
1156# TODO deprecate
1157class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
1158 entries = ShoppingListEntryCheckedSerializer(many=True, allow_null=True)
1160 class Meta:
1161 model = ShoppingList
1162 fields = ('id', 'entries',)
1163 read_only_fields = ('id',)
1166class ShareLinkSerializer(SpacedModelSerializer):
1167 class Meta:
1168 model = ShareLink
1169 fields = '__all__'
1172class CookLogSerializer(serializers.ModelSerializer):
1173 def create(self, validated_data):
1174 validated_data['created_by'] = self.context['request'].user
1175 validated_data['space'] = self.context['request'].space
1176 return super().create(validated_data)
1178 class Meta:
1179 model = CookLog
1180 fields = ('id', 'recipe', 'servings', 'rating', 'created_by', 'created_at')
1181 read_only_fields = ('id', 'created_by')
1184class ViewLogSerializer(serializers.ModelSerializer):
1185 def create(self, validated_data):
1186 validated_data['created_by'] = self.context['request'].user
1187 validated_data['space'] = self.context['request'].space
1188 return super().create(validated_data)
1190 class Meta:
1191 model = ViewLog
1192 fields = ('id', 'recipe', 'created_by', 'created_at')
1193 read_only_fields = ('created_by',)
1196class ImportLogSerializer(serializers.ModelSerializer):
1197 keyword = KeywordSerializer(read_only=True)
1199 def create(self, validated_data):
1200 validated_data['created_by'] = self.context['request'].user
1201 validated_data['space'] = self.context['request'].space
1202 return super().create(validated_data)
1204 class Meta:
1205 model = ImportLog
1206 fields = (
1207 'id', 'type', 'msg', 'running', 'keyword', 'total_recipes', 'imported_recipes', 'created_by', 'created_at')
1208 read_only_fields = ('created_by',)
1211class ExportLogSerializer(serializers.ModelSerializer):
1213 def create(self, validated_data):
1214 validated_data['created_by'] = self.context['request'].user
1215 validated_data['space'] = self.context['request'].space
1216 return super().create(validated_data)
1218 class Meta:
1219 model = ExportLog
1220 fields = (
1221 'id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration',
1222 'possibly_not_expired',
1223 'created_by', 'created_at')
1224 read_only_fields = ('created_by',)
1227class AutomationSerializer(serializers.ModelSerializer):
1229 def create(self, validated_data):
1230 validated_data['created_by'] = self.context['request'].user
1231 validated_data['space'] = self.context['request'].space
1232 return super().create(validated_data)
1234 class Meta:
1235 model = Automation
1236 fields = (
1237 'id', 'type', 'name', 'description', 'param_1', 'param_2', 'param_3', 'order', 'disabled', 'created_by',)
1238 read_only_fields = ('created_by',)
1241class InviteLinkSerializer(WritableNestedModelSerializer):
1242 group = GroupSerializer()
1244 def create(self, validated_data):
1245 validated_data['created_by'] = self.context['request'].user
1246 validated_data['space'] = self.context['request'].space
1247 obj = super().create(validated_data)
1249 if obj.email:
1250 try:
1251 if InviteLink.objects.filter(space=self.context['request'].space,
1252 created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
1253 message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(
1254 self.context['request'].user.get_user_display_name())
1255 message += _(' to join their Tandoor Recipes space ') + escape(
1256 self.context['request'].space.name) + '.\n\n'
1257 message += _('Click the following link to activate your account: ') + self.context[
1258 'request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
1259 message += _('If the link does not work use the following code to manually join the space: ') + str(
1260 obj.uuid) + '\n\n'
1261 message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n'
1262 message += _(
1263 'Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
1265 send_mail(
1266 _('Tandoor Recipes Invite'),
1267 message,
1268 None,
1269 [obj.email],
1270 fail_silently=True,
1271 )
1272 except (SMTPException, BadHeaderError, TimeoutError):
1273 pass
1275 return obj
1277 class Meta:
1278 model = InviteLink
1279 fields = (
1280 'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by', 'created_at',)
1281 read_only_fields = ('id', 'uuid', 'created_by', 'created_at',)
1284# CORS, REST and Scopes aren't currently working
1285# Scopes are evaluating before REST has authenticated the user assigning a None space
1286# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
1287class BookmarkletImportListSerializer(serializers.ModelSerializer):
1288 def create(self, validated_data):
1289 validated_data['created_by'] = self.context['request'].user
1290 validated_data['space'] = self.context['request'].space
1291 return super().create(validated_data)
1293 class Meta:
1294 model = BookmarkletImport
1295 fields = ('id', 'url', 'created_by', 'created_at')
1296 read_only_fields = ('created_by', 'space')
1299class BookmarkletImportSerializer(BookmarkletImportListSerializer):
1300 class Meta:
1301 model = BookmarkletImport
1302 fields = ('id', 'url', 'html', 'created_by', 'created_at')
1303 read_only_fields = ('created_by', 'space')
1306# OAuth / Auth Token related Serializers
1308class AccessTokenSerializer(serializers.ModelSerializer):
1309 token = serializers.SerializerMethodField('get_token')
1311 def create(self, validated_data):
1312 validated_data['token'] = f'tda_{str(uuid.uuid4()).replace("-", "_")}'
1313 validated_data['user'] = self.context['request'].user
1314 return super().create(validated_data)
1316 def get_token(self, obj):
1317 if (timezone.now() - obj.created).seconds < 15:
1318 return obj.token
1319 return f'tda_************_******_***********{obj.token[len(obj.token) - 4:]}'
1321 class Meta:
1322 model = AccessToken
1323 fields = ('id', 'token', 'expires', 'scope', 'created', 'updated')
1324 read_only_fields = ('id', 'token',)
1327# Export/Import Serializers
1329class KeywordExportSerializer(KeywordSerializer):
1330 class Meta:
1331 model = Keyword
1332 fields = ('name', 'description', 'created_at', 'updated_at')
1335class NutritionInformationExportSerializer(NutritionInformationSerializer):
1336 class Meta:
1337 model = NutritionInformation
1338 fields = ('carbohydrates', 'fats', 'proteins', 'calories', 'source')
1341class SupermarketCategoryExportSerializer(SupermarketCategorySerializer):
1342 class Meta:
1343 model = SupermarketCategory
1344 fields = ('name',)
1347class UnitExportSerializer(UnitSerializer):
1348 class Meta:
1349 model = Unit
1350 fields = ('name', 'plural_name', 'description')
1353class FoodExportSerializer(FoodSerializer):
1354 supermarket_category = SupermarketCategoryExportSerializer(allow_null=True, required=False)
1356 class Meta:
1357 model = Food
1358 fields = ('name', 'plural_name', 'ignore_shopping', 'supermarket_category',)
1361class IngredientExportSerializer(WritableNestedModelSerializer):
1362 food = FoodExportSerializer(allow_null=True)
1363 unit = UnitExportSerializer(allow_null=True)
1364 amount = CustomDecimalField()
1366 def create(self, validated_data):
1367 validated_data['space'] = self.context['request'].space
1368 return super().create(validated_data)
1370 class Meta:
1371 model = Ingredient
1372 fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit',
1373 'always_use_plural_food')
1376class StepExportSerializer(WritableNestedModelSerializer):
1377 ingredients = IngredientExportSerializer(many=True)
1379 def create(self, validated_data):
1380 validated_data['space'] = self.context['request'].space
1381 return super().create(validated_data)
1383 class Meta:
1384 model = Step
1385 fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header', 'show_ingredients_table')
1388class RecipeExportSerializer(WritableNestedModelSerializer):
1389 nutrition = NutritionInformationSerializer(allow_null=True, required=False)
1390 steps = StepExportSerializer(many=True)
1391 keywords = KeywordExportSerializer(many=True)
1393 class Meta:
1394 model = Recipe
1395 fields = (
1396 'name', 'description', 'keywords', 'steps', 'working_time',
1397 'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text', 'source_url',
1398 )
1400 def create(self, validated_data):
1401 validated_data['created_by'] = self.context['request'].user
1402 validated_data['space'] = self.context['request'].space
1403 return super().create(validated_data)
1406class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
1407 list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False,
1408 help_text=_("Existing shopping list to update"))
1409 ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
1410 "List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
1411 servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_(
1412 "Providing a list_recipe ID and servings of 0 will delete that shopping list."))
1414 class Meta:
1415 model = Recipe
1416 fields = ['id', 'list_recipe', 'ingredients', 'servings', ]
1419class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
1420 amount = serializers.IntegerField(write_only=True, allow_null=True, required=False,
1421 help_text=_("Amount of food to add to the shopping list"))
1422 unit = serializers.IntegerField(write_only=True, allow_null=True, required=False,
1423 help_text=_("ID of unit to use for the shopping list"))
1424 delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True,
1425 help_text=_("When set to true will delete all food from active shopping lists."))
1427 class Meta:
1428 model = Recipe
1429 fields = ['id', 'amount', 'unit', 'delete', ]
1432# non model serializers
1434class RecipeFromSourceSerializer(serializers.Serializer):
1435 url = serializers.CharField(max_length=4096, required=False, allow_null=True, allow_blank=True)
1436 data = serializers.CharField(required=False, allow_null=True, allow_blank=True)
1437 bookmarklet = serializers.IntegerField(required=False, allow_null=True, )