Coverage for cookbook/serializer.py: 85%

941 statements  

« 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 

8 

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 

23 

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 

40 

41 

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 

48 

49 image = serializers.SerializerMethodField('get_image') 

50 numrecipe = serializers.ReadOnlyField(source='recipe_count') 

51 

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 

70 

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 

79 

80 

81class OpenDataModelMixin(serializers.ModelSerializer): 

82 

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) 

87 

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) 

92 

93 

94class CustomDecimalField(serializers.Field): 

95 """ 

96 Custom decimal field to normalize useless decimal places 

97 and allow commas as decimal separators 

98 """ 

99 

100 def to_representation(self, value): 

101 if not isinstance(value, Decimal): 

102 value = Decimal(value) 

103 return round(value, 2).normalize() 

104 

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') 

115 

116 

117class CustomOnHandField(serializers.Field): 

118 def get_attribute(self, instance): 

119 return instance 

120 

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() 

138 

139 def to_internal_value(self, data): 

140 return data 

141 

142 

143class SpaceFilterSerializer(serializers.ListSerializer): 

144 

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) 

159 

160 

161class UserSerializer(WritableNestedModelSerializer): 

162 display_name = serializers.SerializerMethodField('get_user_label') 

163 

164 def get_user_label(self, obj): 

165 return obj.get_user_display_name() 

166 

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',) 

172 

173 

174class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer): 

175 def create(self, validated_data): 

176 raise ValidationError('Cannot create using this endpoint') 

177 

178 def update(self, instance, validated_data): 

179 return instance # cannot update group 

180 

181 class Meta: 

182 model = Group 

183 fields = ('id', 'name') 

184 

185 

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) 

189 

190 def create(self, validated_data): 

191 raise ValidationError('Cannot create using this endpoint') 

192 

193 def update(self, instance, validated_data): 

194 return instance 

195 

196 class Meta: 

197 model = FoodInheritField 

198 fields = ('id', 'name', 'field',) 

199 read_only_fields = ['id'] 

200 

201 

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') 

206 

207 def get_download_link(self, obj): 

208 return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk})) 

209 

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

217 

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.')) 

222 

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 

229 

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.')) 

233 

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) 

239 

240 def update(self, instance, validated_data): 

241 self.check_file_limit(validated_data) 

242 return super().update(instance, validated_data) 

243 

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, }} 

249 

250 

251class UserFileViewSerializer(serializers.ModelSerializer): 

252 file_download = serializers.SerializerMethodField('get_download_link') 

253 preview = serializers.SerializerMethodField('get_preview_link') 

254 

255 def get_download_link(self, obj): 

256 return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk})) 

257 

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

265 

266 def create(self, validated_data): 

267 raise ValidationError('Cannot create File over this view') 

268 

269 def update(self, instance, validated_data): 

270 return instance 

271 

272 class Meta: 

273 model = UserFile 

274 fields = ('id', 'name', 'file_download', 'preview') 

275 read_only_fields = ('id', 'file') 

276 

277 

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) 

284 

285 def get_user_count(self, obj): 

286 return UserSpace.objects.filter(space=obj).count() 

287 

288 def get_recipe_count(self, obj): 

289 return Recipe.objects.filter(space=obj).count() 

290 

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 

296 

297 def create(self, validated_data): 

298 raise ValidationError('Cannot create using this endpoint') 

299 

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',) 

309 

310 

311class UserSpaceSerializer(WritableNestedModelSerializer): 

312 user = UserSerializer(read_only=True) 

313 groups = GroupSerializer(many=True) 

314 

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) 

319 

320 def create(self, validated_data): 

321 raise ValidationError('Cannot create using this endpoint') 

322 

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') 

327 

328 

329class SpacedModelSerializer(serializers.ModelSerializer): 

330 def create(self, validated_data): 

331 validated_data['space'] = self.context['request'].space 

332 return super().create(validated_data) 

333 

334 

335class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer): 

336 

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 

343 

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',) 

349 

350 

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) 

357 

358 def get_food_inherit_defaults(self, obj): 

359 return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data 

360 

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() 

364 

365 def update(self, instance, validated_data): 

366 with scopes_disabled(): 

367 return super().update(instance, validated_data) 

368 

369 def create(self, validated_data): 

370 raise ValidationError('Cannot create using this endpoint') 

371 

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 ) 

383 

384 

385class StorageSerializer(SpacedModelSerializer): 

386 

387 def create(self, validated_data): 

388 validated_data['created_by'] = self.context['request'].user 

389 return super().create(validated_data) 

390 

391 class Meta: 

392 model = Storage 

393 fields = ( 

394 'id', 'name', 'method', 'username', 'password', 

395 'token', 'created_by' 

396 ) 

397 

398 read_only_fields = ('created_by',) 

399 

400 extra_kwargs = { 

401 'password': {'write_only': True}, 

402 'token': {'write_only': True}, 

403 } 

404 

405 

406class SyncSerializer(SpacedModelSerializer): 

407 class Meta: 

408 model = Sync 

409 fields = ( 

410 'id', 'storage', 'path', 'active', 'last_checked', 

411 'created_at', 'updated_at' 

412 ) 

413 

414 

415class SyncLogSerializer(SpacedModelSerializer): 

416 class Meta: 

417 model = SyncLog 

418 fields = ('id', 'sync', 'status', 'msg', 'created_at') 

419 

420 

421class KeywordLabelSerializer(serializers.ModelSerializer): 

422 label = serializers.SerializerMethodField('get_label') 

423 

424 def get_label(self, obj): 

425 return str(obj) 

426 

427 class Meta: 

428 list_serializer_class = SpaceFilterSerializer 

429 model = Keyword 

430 fields = ( 

431 'id', 'label', 

432 ) 

433 read_only_fields = ('id', 'label') 

434 

435 

436class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin): 

437 label = serializers.SerializerMethodField('get_label') 

438 recipe_filter = 'keywords' 

439 

440 def get_label(self, obj): 

441 return str(obj) 

442 

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 

450 

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') 

457 

458 

459class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin): 

460 recipe_filter = 'steps__ingredients__unit' 

461 

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() 

469 

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 

472 

473 obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data) 

474 return obj 

475 

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) 

481 

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') 

486 

487 

488class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin): 

489 

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 

495 

496 def update(self, instance, validated_data): 

497 return super(SupermarketCategorySerializer, self).update(instance, validated_data) 

498 

499 class Meta: 

500 model = SupermarketCategory 

501 fields = ('id', 'name', 'description') 

502 

503 

504class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer): 

505 category = SupermarketCategorySerializer() 

506 

507 class Meta: 

508 model = SupermarketCategoryRelation 

509 fields = ('id', 'category', 'supermarket', 'order') 

510 

511 

512class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin): 

513 category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True) 

514 

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 

520 

521 class Meta: 

522 model = Supermarket 

523 fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug') 

524 

525 

526class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin): 

527 id = serializers.IntegerField(required=False) 

528 order = IntegerField(default=0, required=False) 

529 

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 

535 

536 class Meta: 

537 model = PropertyType 

538 fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug', 'fdc_id',) 

539 

540 

541class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): 

542 property_type = PropertyTypeSerializer() 

543 property_amount = CustomDecimalField() 

544 

545 def create(self, validated_data): 

546 validated_data['space'] = self.context['request'].space 

547 return super().create(validated_data) 

548 

549 class Meta: 

550 model = Property 

551 fields = ('id', 'property_amount', 'property_type') 

552 

553 

554class RecipeSimpleSerializer(WritableNestedModelSerializer): 

555 url = serializers.SerializerMethodField('get_url') 

556 

557 def get_url(self, obj): 

558 return reverse('view_recipe', args=[obj.id]) 

559 

560 def create(self, validated_data): 

561 # don't allow writing to Recipe via this API 

562 return Recipe.objects.get(**validated_data) 

563 

564 def update(self, instance, validated_data): 

565 # don't allow writing to Recipe via this API 

566 return Recipe.objects.get(**validated_data) 

567 

568 class Meta: 

569 model = Recipe 

570 fields = ('id', 'name', 'url') 

571 

572 

573class FoodSimpleSerializer(serializers.ModelSerializer): 

574 class Meta: 

575 model = Food 

576 fields = ('id', 'name', 'plural_name') 

577 

578 

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) 

589 

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) 

593 

594 recipe_filter = 'steps__ingredients__food' 

595 images = ['recipe__image'] 

596 

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() 

620 

621 def get_shopping_status(self, obj): 

622 return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).exists() 

623 

624 def create(self, validated_data): 

625 name = validated_data['name'].strip() 

626 

627 if plural_name := validated_data.pop('plural_name', None): 

628 plural_name = plural_name.strip() 

629 

630 if food := Food.objects.filter(Q(name=name) | Q(plural_name=name)).first(): 

631 return food 

632 

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) 

644 

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

656 

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() 

659 

660 properties = validated_data.pop('properties', None) 

661 

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) 

664 

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

668 

669 return obj 

670 

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

685 

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 

691 

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') 

702 

703 

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') 

710 

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 

717 

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 [] 

727 

728 def create(self, validated_data): 

729 validated_data['space'] = self.context['request'].space 

730 return super().create(validated_data) 

731 

732 def update(self, instance, validated_data): 

733 validated_data.pop('original_text', None) 

734 return super().update(instance, validated_data) 

735 

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', ] 

744 

745 

746class IngredientSerializer(IngredientSimpleSerializer): 

747 food = FoodSerializer(allow_null=True) 

748 

749 

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' 

757 

758 def create(self, validated_data): 

759 validated_data['space'] = self.context['request'].space 

760 return super().create(validated_data) 

761 

762 def get_ingredients_vue(self, obj): 

763 return obj.get_instruction_render() 

764 

765 def get_ingredients_markdown(self, obj): 

766 return obj.get_instruction_render() 

767 

768 def get_step_recipes(self, obj): 

769 return list(obj.recipe_set.values_list('id', flat=True).all()) 

770 

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 

776 

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 ) 

784 

785 

786class StepRecipeSerializer(WritableNestedModelSerializer): 

787 steps = StepSerializer(many=True) 

788 

789 class Meta: 

790 model = Recipe 

791 fields = ( 

792 'id', 'name', 'steps', 

793 ) 

794 

795 

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() 

803 

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}' 

809 

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) 

822 

823 class Meta: 

824 model = UnitConversion 

825 fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug') 

826 

827 

828class NutritionInformationSerializer(serializers.ModelSerializer): 

829 carbohydrates = CustomDecimalField() 

830 fats = CustomDecimalField() 

831 proteins = CustomDecimalField() 

832 calories = CustomDecimalField() 

833 

834 def create(self, validated_data): 

835 validated_data['space'] = self.context['request'].space 

836 return super().create(validated_data) 

837 

838 class Meta: 

839 model = NutritionInformation 

840 fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source') 

841 

842 

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 

850 

851 

852class RecipeOverviewSerializer(RecipeBaseSerializer): 

853 keywords = KeywordLabelSerializer(many=True) 

854 new = serializers.SerializerMethodField('is_recipe_new') 

855 recent = serializers.ReadOnlyField() 

856 

857 rating = CustomDecimalField(required=False, allow_null=True) 

858 last_cooked = serializers.DateTimeField(required=False, allow_null=True) 

859 

860 def create(self, validated_data): 

861 pass 

862 

863 def update(self, instance, validated_data): 

864 return instance 

865 

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'] 

874 

875 

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') 

885 

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) 

889 

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'] 

900 

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) 

906 

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) 

911 

912 

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) 

916 

917 class Meta: 

918 model = Recipe 

919 fields = ['image', 'image_url', ] 

920 

921 

922class RecipeImportSerializer(SpacedModelSerializer): 

923 class Meta: 

924 model = RecipeImport 

925 fields = '__all__' 

926 

927 

928class CommentSerializer(serializers.ModelSerializer): 

929 class Meta: 

930 model = Comment 

931 fields = '__all__' 

932 

933 

934class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer): 

935 shared = UserSerializer(many=True, required=False) 

936 

937 def create(self, validated_data): 

938 validated_data['created_by'] = self.context['request'].user 

939 return super().create(validated_data) 

940 

941 class Meta: 

942 model = CustomFilter 

943 fields = ('id', 'name', 'search', 'shared', 'created_by') 

944 read_only_fields = ('created_by',) 

945 

946 

947class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer): 

948 shared = UserSerializer(many=True) 

949 filter = CustomFilterSerializer(allow_null=True, required=False) 

950 

951 def create(self, validated_data): 

952 validated_data['created_by'] = self.context['request'].user 

953 return super().create(validated_data) 

954 

955 class Meta: 

956 model = RecipeBook 

957 fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter') 

958 read_only_fields = ('created_by',) 

959 

960 

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) 

964 

965 def get_book_content(self, obj): 

966 return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book) 

967 

968 def get_recipe_content(self, obj): 

969 return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe) 

970 

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 

978 

979 class Meta: 

980 model = RecipeBookEntry 

981 fields = ('id', 'book', 'book_content', 'recipe', 'recipe_content',) 

982 

983 

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') 

993 

994 to_date = serializers.DateField(required=False) 

995 

996 def get_note_markdown(self, obj): 

997 return markdown(obj.note) 

998 

999 def in_shopping(self, obj): 

1000 return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists() 

1001 

1002 def create(self, validated_data): 

1003 validated_data['created_by'] = self.context['request'].user 

1004 

1005 if 'to_date' not in validated_data or validated_data['to_date'] is None: 

1006 validated_data['to_date'] = validated_data['from_date'] 

1007 

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 

1013 

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',) 

1022 

1023 

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() 

1032 

1033 

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() 

1039 

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})' 

1051 

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) 

1058 

1059 class Meta: 

1060 model = ShoppingListRecipe 

1061 fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note') 

1062 read_only_fields = ('id',) 

1063 

1064 

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) 

1073 

1074 def get_fields(self, *args, **kwargs): 

1075 fields = super().get_fields(*args, **kwargs) 

1076 

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 

1082 

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() 

1092 

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'] 

1100 

1101 return super().run_validation(data) 

1102 

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) 

1107 

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) 

1117 

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',) 

1126 

1127 

1128# TODO deprecate 

1129class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer): 

1130 class Meta: 

1131 model = ShoppingListEntry 

1132 fields = ('id', 'checked') 

1133 

1134 

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) 

1141 

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) 

1146 

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',) 

1154 

1155 

1156# TODO deprecate 

1157class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer): 

1158 entries = ShoppingListEntryCheckedSerializer(many=True, allow_null=True) 

1159 

1160 class Meta: 

1161 model = ShoppingList 

1162 fields = ('id', 'entries',) 

1163 read_only_fields = ('id',) 

1164 

1165 

1166class ShareLinkSerializer(SpacedModelSerializer): 

1167 class Meta: 

1168 model = ShareLink 

1169 fields = '__all__' 

1170 

1171 

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) 

1177 

1178 class Meta: 

1179 model = CookLog 

1180 fields = ('id', 'recipe', 'servings', 'rating', 'created_by', 'created_at') 

1181 read_only_fields = ('id', 'created_by') 

1182 

1183 

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) 

1189 

1190 class Meta: 

1191 model = ViewLog 

1192 fields = ('id', 'recipe', 'created_by', 'created_at') 

1193 read_only_fields = ('created_by',) 

1194 

1195 

1196class ImportLogSerializer(serializers.ModelSerializer): 

1197 keyword = KeywordSerializer(read_only=True) 

1198 

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) 

1203 

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',) 

1209 

1210 

1211class ExportLogSerializer(serializers.ModelSerializer): 

1212 

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) 

1217 

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',) 

1225 

1226 

1227class AutomationSerializer(serializers.ModelSerializer): 

1228 

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) 

1233 

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',) 

1239 

1240 

1241class InviteLinkSerializer(WritableNestedModelSerializer): 

1242 group = GroupSerializer() 

1243 

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) 

1248 

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/' 

1264 

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 

1274 

1275 return obj 

1276 

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',) 

1282 

1283 

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) 

1292 

1293 class Meta: 

1294 model = BookmarkletImport 

1295 fields = ('id', 'url', 'created_by', 'created_at') 

1296 read_only_fields = ('created_by', 'space') 

1297 

1298 

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') 

1304 

1305 

1306# OAuth / Auth Token related Serializers 

1307 

1308class AccessTokenSerializer(serializers.ModelSerializer): 

1309 token = serializers.SerializerMethodField('get_token') 

1310 

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) 

1315 

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:]}' 

1320 

1321 class Meta: 

1322 model = AccessToken 

1323 fields = ('id', 'token', 'expires', 'scope', 'created', 'updated') 

1324 read_only_fields = ('id', 'token',) 

1325 

1326 

1327# Export/Import Serializers 

1328 

1329class KeywordExportSerializer(KeywordSerializer): 

1330 class Meta: 

1331 model = Keyword 

1332 fields = ('name', 'description', 'created_at', 'updated_at') 

1333 

1334 

1335class NutritionInformationExportSerializer(NutritionInformationSerializer): 

1336 class Meta: 

1337 model = NutritionInformation 

1338 fields = ('carbohydrates', 'fats', 'proteins', 'calories', 'source') 

1339 

1340 

1341class SupermarketCategoryExportSerializer(SupermarketCategorySerializer): 

1342 class Meta: 

1343 model = SupermarketCategory 

1344 fields = ('name',) 

1345 

1346 

1347class UnitExportSerializer(UnitSerializer): 

1348 class Meta: 

1349 model = Unit 

1350 fields = ('name', 'plural_name', 'description') 

1351 

1352 

1353class FoodExportSerializer(FoodSerializer): 

1354 supermarket_category = SupermarketCategoryExportSerializer(allow_null=True, required=False) 

1355 

1356 class Meta: 

1357 model = Food 

1358 fields = ('name', 'plural_name', 'ignore_shopping', 'supermarket_category',) 

1359 

1360 

1361class IngredientExportSerializer(WritableNestedModelSerializer): 

1362 food = FoodExportSerializer(allow_null=True) 

1363 unit = UnitExportSerializer(allow_null=True) 

1364 amount = CustomDecimalField() 

1365 

1366 def create(self, validated_data): 

1367 validated_data['space'] = self.context['request'].space 

1368 return super().create(validated_data) 

1369 

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') 

1374 

1375 

1376class StepExportSerializer(WritableNestedModelSerializer): 

1377 ingredients = IngredientExportSerializer(many=True) 

1378 

1379 def create(self, validated_data): 

1380 validated_data['space'] = self.context['request'].space 

1381 return super().create(validated_data) 

1382 

1383 class Meta: 

1384 model = Step 

1385 fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header', 'show_ingredients_table') 

1386 

1387 

1388class RecipeExportSerializer(WritableNestedModelSerializer): 

1389 nutrition = NutritionInformationSerializer(allow_null=True, required=False) 

1390 steps = StepExportSerializer(many=True) 

1391 keywords = KeywordExportSerializer(many=True) 

1392 

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 ) 

1399 

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) 

1404 

1405 

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.")) 

1413 

1414 class Meta: 

1415 model = Recipe 

1416 fields = ['id', 'list_recipe', 'ingredients', 'servings', ] 

1417 

1418 

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.")) 

1426 

1427 class Meta: 

1428 model = Recipe 

1429 fields = ['id', 'amount', 'unit', 'delete', ] 

1430 

1431 

1432# non model serializers 

1433 

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, )