mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-06 14:48:02 -05:00
ensure that all schema fields are typed correctly
This commit is contained in:
@@ -126,7 +126,7 @@ class TreeModel(MP_Node):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a string representation of a tree node and it's ancestors,
|
Returns a string representation of a tree node and it's ancestors,
|
||||||
e.g. 'Cuisine > Asian > Chinese > Catonese'.
|
e.g. 'Cuisine > Asian > Chinese > Catonese'.
|
||||||
@@ -1461,19 +1461,21 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
|||||||
UNIT_REPLACE = 'UNIT_REPLACE'
|
UNIT_REPLACE = 'UNIT_REPLACE'
|
||||||
NAME_REPLACE = 'NAME_REPLACE'
|
NAME_REPLACE = 'NAME_REPLACE'
|
||||||
|
|
||||||
|
automation_types = (
|
||||||
|
(FOOD_ALIAS, _('Food Alias')),
|
||||||
|
(UNIT_ALIAS, _('Unit Alias')),
|
||||||
|
(KEYWORD_ALIAS, _('Keyword Alias')),
|
||||||
|
(DESCRIPTION_REPLACE, _('Description Replace')),
|
||||||
|
(INSTRUCTION_REPLACE, _('Instruction Replace')),
|
||||||
|
(NEVER_UNIT, _('Never Unit')),
|
||||||
|
(TRANSPOSE_WORDS, _('Transpose Words')),
|
||||||
|
(FOOD_REPLACE, _('Food Replace')),
|
||||||
|
(UNIT_REPLACE, _('Unit Replace')),
|
||||||
|
(NAME_REPLACE, _('Name Replace')),
|
||||||
|
)
|
||||||
|
|
||||||
type = models.CharField(max_length=128,
|
type = models.CharField(max_length=128,
|
||||||
choices=(
|
choices=automation_types)
|
||||||
(FOOD_ALIAS, _('Food Alias')),
|
|
||||||
(UNIT_ALIAS, _('Unit Alias')),
|
|
||||||
(KEYWORD_ALIAS, _('Keyword Alias')),
|
|
||||||
(DESCRIPTION_REPLACE, _('Description Replace')),
|
|
||||||
(INSTRUCTION_REPLACE, _('Instruction Replace')),
|
|
||||||
(NEVER_UNIT, _('Never Unit')),
|
|
||||||
(TRANSPOSE_WORDS, _('Transpose Words')),
|
|
||||||
(FOOD_REPLACE, _('Food Replace')),
|
|
||||||
(UNIT_REPLACE, _('Unit Replace')),
|
|
||||||
(NAME_REPLACE, _('Name Replace')),
|
|
||||||
))
|
|
||||||
name = models.CharField(max_length=128, default='')
|
name = models.CharField(max_length=128, default='')
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
|||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from html import escape
|
from html import escape
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
from django.contrib.auth.models import AnonymousUser, Group, User
|
from django.contrib.auth.models import AnonymousUser, Group, User
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
@@ -75,7 +75,7 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
|||||||
images = None
|
images = None
|
||||||
|
|
||||||
image = serializers.SerializerMethodField('get_image')
|
image = serializers.SerializerMethodField('get_image')
|
||||||
numrecipe = serializers.ReadOnlyField(source='recipe_count')
|
numrecipe = serializers.IntegerField(source='recipe_count', read_only=True)
|
||||||
|
|
||||||
def get_fields(self, *args, **kwargs):
|
def get_fields(self, *args, **kwargs):
|
||||||
fields = super().get_fields(*args, **kwargs)
|
fields = super().get_fields(*args, **kwargs)
|
||||||
@@ -119,6 +119,7 @@ class OpenDataModelMixin(serializers.ModelSerializer):
|
|||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(float)
|
||||||
class CustomDecimalField(serializers.Field):
|
class CustomDecimalField(serializers.Field):
|
||||||
"""
|
"""
|
||||||
Custom decimal field to normalize useless decimal places
|
Custom decimal field to normalize useless decimal places
|
||||||
@@ -142,6 +143,7 @@ class CustomDecimalField(serializers.Field):
|
|||||||
raise ValidationError('A valid number is required')
|
raise ValidationError('A valid number is required')
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(bool)
|
||||||
class CustomOnHandField(serializers.Field):
|
class CustomOnHandField(serializers.Field):
|
||||||
def get_attribute(self, instance):
|
def get_attribute(self, instance):
|
||||||
return instance
|
return instance
|
||||||
@@ -186,6 +188,7 @@ class SpaceFilterSerializer(serializers.ListSerializer):
|
|||||||
class UserSerializer(WritableNestedModelSerializer):
|
class UserSerializer(WritableNestedModelSerializer):
|
||||||
display_name = serializers.SerializerMethodField('get_user_label')
|
display_name = serializers.SerializerMethodField('get_user_label')
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_user_label(self, obj):
|
def get_user_label(self, obj):
|
||||||
return obj.get_user_display_name()
|
return obj.get_user_display_name()
|
||||||
|
|
||||||
@@ -229,9 +232,11 @@ class UserFileSerializer(serializers.ModelSerializer):
|
|||||||
file_download = serializers.SerializerMethodField('get_download_link')
|
file_download = serializers.SerializerMethodField('get_download_link')
|
||||||
preview = serializers.SerializerMethodField('get_preview_link')
|
preview = serializers.SerializerMethodField('get_preview_link')
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_download_link(self, obj):
|
def get_download_link(self, obj):
|
||||||
return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
|
return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_preview_link(self, obj):
|
def get_preview_link(self, obj):
|
||||||
try:
|
try:
|
||||||
Image.open(obj.file.file.file)
|
Image.open(obj.file.file.file)
|
||||||
@@ -277,9 +282,11 @@ class UserFileViewSerializer(serializers.ModelSerializer):
|
|||||||
file_download = serializers.SerializerMethodField('get_download_link')
|
file_download = serializers.SerializerMethodField('get_download_link')
|
||||||
preview = serializers.SerializerMethodField('get_preview_link')
|
preview = serializers.SerializerMethodField('get_preview_link')
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_download_link(self, obj):
|
def get_download_link(self, obj):
|
||||||
return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
|
return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_preview_link(self, obj):
|
def get_preview_link(self, obj):
|
||||||
try:
|
try:
|
||||||
Image.open(obj.file.file.file)
|
Image.open(obj.file.file.file)
|
||||||
@@ -316,12 +323,15 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
|||||||
logo_color_512 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
logo_color_512 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
logo_color_svg = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
logo_color_svg = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
|
||||||
|
@extend_schema_field(int)
|
||||||
def get_user_count(self, obj):
|
def get_user_count(self, obj):
|
||||||
return UserSpace.objects.filter(space=obj).count()
|
return UserSpace.objects.filter(space=obj).count()
|
||||||
|
|
||||||
|
@extend_schema_field(int)
|
||||||
def get_recipe_count(self, obj):
|
def get_recipe_count(self, obj):
|
||||||
return Recipe.objects.filter(space=obj).count()
|
return Recipe.objects.filter(space=obj).count()
|
||||||
|
|
||||||
|
@extend_schema_field(float)
|
||||||
def get_file_size_mb(self, obj):
|
def get_file_size_mb(self, obj):
|
||||||
try:
|
try:
|
||||||
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
|
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
|
||||||
@@ -390,9 +400,11 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
|||||||
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
||||||
image = UserFileViewSerializer(required=False, allow_null=True, many=False)
|
image = UserFileViewSerializer(required=False, allow_null=True, many=False)
|
||||||
|
|
||||||
|
@extend_schema_field(FoodInheritFieldSerializer)
|
||||||
def get_food_inherit_defaults(self, obj):
|
def get_food_inherit_defaults(self, obj):
|
||||||
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
|
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
|
||||||
|
|
||||||
|
@extend_schema_field(bool)
|
||||||
def get_food_children_exist(self, obj):
|
def get_food_children_exist(self, obj):
|
||||||
space = getattr(self.context.get('request', None), 'space', None)
|
space = getattr(self.context.get('request', None), 'space', None)
|
||||||
return Food.objects.filter(depth__gt=0, space=space).exists()
|
return Food.objects.filter(depth__gt=0, space=space).exists()
|
||||||
@@ -479,22 +491,24 @@ class SyncLogSerializer(SpacedModelSerializer):
|
|||||||
class KeywordLabelSerializer(serializers.ModelSerializer):
|
class KeywordLabelSerializer(serializers.ModelSerializer):
|
||||||
label = serializers.SerializerMethodField('get_label')
|
label = serializers.SerializerMethodField('get_label')
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_label(self, obj):
|
def get_label(self, obj):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
list_serializer_class = SpaceFilterSerializer
|
list_serializer_class = SpaceFilterSerializer
|
||||||
model = Keyword
|
model = Keyword
|
||||||
fields = (
|
fields = ('id', 'label')
|
||||||
'id', 'label',
|
|
||||||
)
|
|
||||||
read_only_fields = ('id', 'label')
|
read_only_fields = ('id', 'label')
|
||||||
|
|
||||||
|
|
||||||
class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||||
label = serializers.SerializerMethodField('get_label', allow_null=False)
|
label = serializers.SerializerMethodField('get_label', allow_null=False)
|
||||||
|
parent = IntegerField(read_only=True)
|
||||||
|
|
||||||
recipe_filter = 'keywords'
|
recipe_filter = 'keywords'
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_label(self, obj):
|
def get_label(self, obj):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
@@ -618,6 +632,7 @@ class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
|||||||
class RecipeSimpleSerializer(WritableNestedModelSerializer):
|
class RecipeSimpleSerializer(WritableNestedModelSerializer):
|
||||||
url = serializers.SerializerMethodField('get_url')
|
url = serializers.SerializerMethodField('get_url')
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_url(self, obj):
|
def get_url(self, obj):
|
||||||
return reverse('view_recipe', args=[obj.id])
|
return reverse('view_recipe', args=[obj.id])
|
||||||
|
|
||||||
@@ -658,12 +673,13 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
|
|||||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
|
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
|
||||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||||
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
|
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
|
||||||
shopping = serializers.ReadOnlyField(source='shopping_status')
|
shopping = serializers.CharField(source='shopping_status', read_only=True)
|
||||||
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||||
child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||||
food_onhand = CustomOnHandField(required=False, allow_null=True)
|
food_onhand = CustomOnHandField(required=False, allow_null=True)
|
||||||
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
|
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
|
||||||
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
|
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
|
||||||
|
parent = IntegerField(read_only=True)
|
||||||
|
|
||||||
properties = PropertySerializer(many=True, allow_null=True, required=False)
|
properties = PropertySerializer(many=True, allow_null=True, required=False)
|
||||||
properties_food_unit = UnitSerializer(allow_null=True, required=False)
|
properties_food_unit = UnitSerializer(allow_null=True, required=False)
|
||||||
@@ -672,6 +688,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
recipe_filter = 'steps__ingredients__food'
|
recipe_filter = 'steps__ingredients__food'
|
||||||
images = ['recipe__image']
|
images = ['recipe__image']
|
||||||
|
|
||||||
|
@extend_schema_field(bool)
|
||||||
def get_substitute_onhand(self, obj):
|
def get_substitute_onhand(self, obj):
|
||||||
if not self.context["request"].user.is_authenticated:
|
if not self.context["request"].user.is_authenticated:
|
||||||
return []
|
return []
|
||||||
@@ -769,12 +786,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Food
|
model = Food
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
|
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url', 'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
|
||||||
'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
|
'food_onhand', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
||||||
'food_onhand', 'supermarket_category',
|
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
|
||||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
|
||||||
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields',
|
|
||||||
'open_data_slug',
|
|
||||||
)
|
)
|
||||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||||
|
|
||||||
@@ -786,6 +800,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
|||||||
amount = CustomDecimalField()
|
amount = CustomDecimalField()
|
||||||
conversions = serializers.SerializerMethodField('get_conversions')
|
conversions = serializers.SerializerMethodField('get_conversions')
|
||||||
|
|
||||||
|
@extend_schema_field(list)
|
||||||
def get_used_in_recipes(self, obj):
|
def get_used_in_recipes(self, obj):
|
||||||
used_in = []
|
used_in = []
|
||||||
for s in obj.step_set.all():
|
for s in obj.step_set.all():
|
||||||
@@ -793,6 +808,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
|||||||
used_in.append({'id': r.id, 'name': r.name})
|
used_in.append({'id': r.id, 'name': r.name})
|
||||||
return used_in
|
return used_in
|
||||||
|
|
||||||
|
@extend_schema_field(list)
|
||||||
def get_conversions(self, obj):
|
def get_conversions(self, obj):
|
||||||
if obj.unit and obj.food:
|
if obj.unit and obj.food:
|
||||||
uch = UnitConversionHelper(self.context['request'].space)
|
uch = UnitConversionHelper(self.context['request'].space)
|
||||||
@@ -837,12 +853,16 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
|||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_instructions_markdown(self, obj):
|
def get_instructions_markdown(self, obj):
|
||||||
return obj.get_instruction_render()
|
return obj.get_instruction_render()
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.ListField)
|
||||||
def get_step_recipes(self, obj):
|
def get_step_recipes(self, obj):
|
||||||
return list(obj.recipe_set.values_list('id', flat=True).all())
|
return list(obj.recipe_set.values_list('id', flat=True).all())
|
||||||
|
|
||||||
|
# couldn't set proper serializer StepRecipeSerializer because of circular reference
|
||||||
|
@extend_schema_field(serializers.JSONField)
|
||||||
def get_step_recipe_data(self, obj):
|
def get_step_recipe_data(self, obj):
|
||||||
# check if root type is recipe to prevent infinite recursion
|
# check if root type is recipe to prevent infinite recursion
|
||||||
# can be improved later to allow multi level embedding
|
# can be improved later to allow multi level embedding
|
||||||
@@ -852,8 +872,7 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Step
|
model = Step
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'instruction', 'ingredients', 'instructions_markdown',
|
'id', 'name', 'instruction', 'ingredients', 'instructions_markdown', 'time', 'order', 'show_as_header', 'file', 'step_recipe',
|
||||||
'time', 'order', 'show_as_header', 'file', 'step_recipe',
|
|
||||||
'step_recipe_data', 'numrecipe', 'show_ingredients_table'
|
'step_recipe_data', 'numrecipe', 'show_ingredients_table'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -863,9 +882,7 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
fields = (
|
fields = ('id', 'name', 'steps')
|
||||||
'id', 'name', 'steps',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin):
|
class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin):
|
||||||
@@ -876,6 +893,7 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
|
|||||||
base_amount = CustomDecimalField()
|
base_amount = CustomDecimalField()
|
||||||
converted_amount = CustomDecimalField()
|
converted_amount = CustomDecimalField()
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_conversion_name(self, obj):
|
def get_conversion_name(self, obj):
|
||||||
text = f'{round(obj.base_amount)} {obj.base_unit} '
|
text = f'{round(obj.base_amount)} {obj.base_unit} '
|
||||||
if obj.food:
|
if obj.food:
|
||||||
@@ -917,6 +935,7 @@ class NutritionInformationSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class RecipeBaseSerializer(WritableNestedModelSerializer):
|
class RecipeBaseSerializer(WritableNestedModelSerializer):
|
||||||
# TODO make days of new recipe a setting
|
# TODO make days of new recipe a setting
|
||||||
|
@extend_schema_field(bool)
|
||||||
def is_recipe_new(self, obj):
|
def is_recipe_new(self, obj):
|
||||||
if getattr(obj, 'new_recipe', None) or obj.created_at > (timezone.now() - timedelta(days=7)):
|
if getattr(obj, 'new_recipe', None) or obj.created_at > (timezone.now() - timedelta(days=7)):
|
||||||
return True
|
return True
|
||||||
@@ -934,7 +953,7 @@ class CommentSerializer(serializers.ModelSerializer):
|
|||||||
class RecipeOverviewSerializer(RecipeBaseSerializer):
|
class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||||
keywords = KeywordLabelSerializer(many=True, read_only=True)
|
keywords = KeywordLabelSerializer(many=True, read_only=True)
|
||||||
new = serializers.SerializerMethodField('is_recipe_new', read_only=True)
|
new = serializers.SerializerMethodField('is_recipe_new', read_only=True)
|
||||||
recent = serializers.ReadOnlyField()
|
recent = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
|
rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
|
||||||
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
|
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
|
||||||
@@ -967,6 +986,7 @@ class RecipeSerializer(RecipeBaseSerializer):
|
|||||||
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
|
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
|
||||||
food_properties = serializers.SerializerMethodField('get_food_properties')
|
food_properties = serializers.SerializerMethodField('get_food_properties')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField)
|
||||||
def get_food_properties(self, obj):
|
def get_food_properties(self, obj):
|
||||||
fph = FoodPropertyHelper(obj.space) # initialize with object space since recipes might be viewed anonymously
|
fph = FoodPropertyHelper(obj.space) # initialize with object space since recipes might be viewed anonymously
|
||||||
return fph.calculate_recipe_properties(obj)
|
return fph.calculate_recipe_properties(obj)
|
||||||
@@ -974,12 +994,9 @@ class RecipeSerializer(RecipeBaseSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
|
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
|
||||||
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
|
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
|
||||||
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings',
|
'last_cooked', 'private', 'shared'
|
||||||
'file_path', 'servings_text', 'rating',
|
|
||||||
'last_cooked',
|
|
||||||
'private', 'shared',
|
|
||||||
)
|
)
|
||||||
read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
|
read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
|
||||||
|
|
||||||
@@ -1041,9 +1058,11 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
|||||||
book_content = serializers.SerializerMethodField(method_name='get_book_content', read_only=True)
|
book_content = serializers.SerializerMethodField(method_name='get_book_content', read_only=True)
|
||||||
recipe_content = serializers.SerializerMethodField(method_name='get_recipe_content', read_only=True)
|
recipe_content = serializers.SerializerMethodField(method_name='get_recipe_content', read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(RecipeBookSerializer)
|
||||||
def get_book_content(self, obj):
|
def get_book_content(self, obj):
|
||||||
return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book)
|
return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book)
|
||||||
|
|
||||||
|
@extend_schema_field(RecipeOverviewSerializer)
|
||||||
def get_recipe_content(self, obj):
|
def get_recipe_content(self, obj):
|
||||||
return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
|
return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
|
||||||
|
|
||||||
@@ -1063,9 +1082,9 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||||
recipe = RecipeOverviewSerializer(required=False, allow_null=True)
|
recipe = RecipeOverviewSerializer(required=False, allow_null=True)
|
||||||
recipe_name = serializers.ReadOnlyField(source='recipe.name')
|
recipe_name = serializers.CharField(source='recipe.name', read_only=True)
|
||||||
meal_type = MealTypeSerializer()
|
meal_type = MealTypeSerializer()
|
||||||
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
|
meal_type_name = serializers.CharField(source='meal_type.name', read_only=True) # TODO deprecate once old meal plan was removed
|
||||||
note_markdown = serializers.SerializerMethodField('get_note_markdown')
|
note_markdown = serializers.SerializerMethodField('get_note_markdown')
|
||||||
servings = CustomDecimalField()
|
servings = CustomDecimalField()
|
||||||
shared = UserSerializer(many=True, required=False, allow_null=True)
|
shared = UserSerializer(many=True, required=False, allow_null=True)
|
||||||
@@ -1073,9 +1092,11 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
|||||||
|
|
||||||
to_date = serializers.DateField(required=False)
|
to_date = serializers.DateField(required=False)
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_note_markdown(self, obj):
|
def get_note_markdown(self, obj):
|
||||||
return markdown(obj.note)
|
return markdown(obj.note)
|
||||||
|
|
||||||
|
@extend_schema_field(bool)
|
||||||
def in_shopping(self, obj):
|
def in_shopping(self, obj):
|
||||||
return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists()
|
return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists()
|
||||||
|
|
||||||
@@ -1119,6 +1140,7 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
|||||||
mealplan_type = serializers.ReadOnlyField(source='mealplan.meal_type.name')
|
mealplan_type = serializers.ReadOnlyField(source='mealplan.meal_type.name')
|
||||||
servings = CustomDecimalField()
|
servings = CustomDecimalField()
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
if not isinstance(value := obj.servings, Decimal):
|
if not isinstance(value := obj.servings, Decimal):
|
||||||
value = Decimal(value)
|
value = Decimal(value)
|
||||||
@@ -1372,6 +1394,7 @@ class AccessTokenSerializer(serializers.ModelSerializer):
|
|||||||
validated_data['user'] = self.context['request'].user
|
validated_data['user'] = self.context['request'].user
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
@extend_schema_field(str)
|
||||||
def get_token(self, obj):
|
def get_token(self, obj):
|
||||||
if (timezone.now() - obj.created).seconds < 15:
|
if (timezone.now() - obj.created).seconds < 15:
|
||||||
return obj.token
|
return obj.token
|
||||||
|
|||||||
@@ -223,9 +223,12 @@ class FuzzyFilterMixin(viewsets.ModelViewSet, ExtendedRecipeMixin):
|
|||||||
|
|
||||||
class MergeMixin(ViewSetMixin):
|
class MergeMixin(ViewSetMixin):
|
||||||
|
|
||||||
|
@extend_schema(parameters=[
|
||||||
|
OpenApiParameter(name="target", description='The ID of the {obj} you want to merge with.', type=OpenApiTypes.INT, location=OpenApiParameter.PATH)
|
||||||
|
])
|
||||||
@decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
|
@decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
|
||||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||||
def merge(self, request, pk, target):
|
def merge(self, request, pk, target: int):
|
||||||
self.description = f"Merge {self.basename} onto target {self.basename} with ID of [int]."
|
self.description = f"Merge {self.basename} onto target {self.basename} with ID of [int]."
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -294,12 +297,16 @@ class MergeMixin(ViewSetMixin):
|
|||||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(list=extend_schema(parameters=[
|
@extend_schema_view(
|
||||||
OpenApiParameter(name='root', description='Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.', type=int),
|
list=extend_schema(parameters=[
|
||||||
OpenApiParameter(name='tree', description='Return all self and children of {} with ID [int].', type=int),
|
OpenApiParameter(name='root', description='Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.', type=int),
|
||||||
]))
|
OpenApiParameter(name='tree', description='Return all self and children of {obj} with ID [int].', type=int),
|
||||||
|
]),
|
||||||
|
move=extend_schema(parameters=[
|
||||||
|
OpenApiParameter(name="parent", description='The ID of the desired parent of the {obj}.', type=OpenApiTypes.INT, location=OpenApiParameter.PATH)
|
||||||
|
])
|
||||||
|
)
|
||||||
class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||||
# schema = TreeSchema()
|
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -329,9 +336,10 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
|||||||
|
|
||||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
||||||
|
|
||||||
|
|
||||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||||
def move(self, request, pk, parent):
|
def move(self, request, pk, parent: int):
|
||||||
self.description = f"Move {self.basename} to be a child of {self.basename} with ID of [int]. Use ID: 0 to move {self.basename} to the root."
|
self.description = f"Move {self.basename} to be a child of {self.basename} with ID of [int]. Use ID: 0 to move {self.basename} to the root."
|
||||||
if self.model.node_order_by:
|
if self.model.node_order_by:
|
||||||
node_location = 'sorted'
|
node_location = 'sorted'
|
||||||
@@ -775,6 +783,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
|||||||
return meal_plans_to_ical(self.get_queryset(), f'meal_plan_{from_date}-{to_date}.ics')
|
return meal_plans_to_ical(self.get_queryset(), f'meal_plan_{from_date}-{to_date}.ics')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO create proper schema
|
||||||
class AutoPlanViewSet(viewsets.ViewSet):
|
class AutoPlanViewSet(viewsets.ViewSet):
|
||||||
http_method_names = ['post', 'options']
|
http_method_names = ['post', 'options']
|
||||||
|
|
||||||
@@ -1273,25 +1282,6 @@ class UserFileViewSet(StandardFilterModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class AutomationViewSet(StandardFilterModelViewSet):
|
class AutomationViewSet(StandardFilterModelViewSet):
|
||||||
"""
|
|
||||||
list:
|
|
||||||
optional parameters
|
|
||||||
|
|
||||||
- **automation_type**: Return the Automations matching the automation type. Multiple values allowed.
|
|
||||||
|
|
||||||
*Automation Types:*
|
|
||||||
- FS: Food Alias
|
|
||||||
- UA: Unit Alias
|
|
||||||
- KA: Keyword Alias
|
|
||||||
- DR: Description Replace
|
|
||||||
- IR: Instruction Replace
|
|
||||||
- NU: Never Unit
|
|
||||||
- TW: Transpose Words
|
|
||||||
- FR: Food Replace
|
|
||||||
- UR: Unit Replace
|
|
||||||
- NR: Name Replace
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = Automation.objects
|
queryset = Automation.objects
|
||||||
serializer_class = AutomationSerializer
|
serializer_class = AutomationSerializer
|
||||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||||
@@ -1311,15 +1301,20 @@ class AutomationViewSet(StandardFilterModelViewSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
parameters=[OpenApiParameter(name='automation_type', description=_('Return the Automations matching the automation type. Multiple values allowed.'), type=str)]
|
parameters=[OpenApiParameter(
|
||||||
|
name='type',
|
||||||
|
description=_('Return the Automations matching the automation type. Multiple values allowed.'),
|
||||||
|
type=str,
|
||||||
|
enum=[a[0] for a in Automation.automation_types])
|
||||||
|
]
|
||||||
)
|
)
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
automation_type = self.request.query_params.getlist('automation_type', [])
|
automation_type = self.request.query_params.getlist('type', [])
|
||||||
if automation_type:
|
if automation_type:
|
||||||
self.queryset = self.queryset.filter(type__in=[self.auto_type[x.upper()] for x in automation_type])
|
self.queryset = self.queryset.filter(type__in=automation_type)
|
||||||
self.queryset = self.queryset.filter(space=self.request.space).all()
|
self.queryset = self.queryset.filter(space=self.request.space).all()
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
@@ -1397,6 +1392,7 @@ class CustomAuthToken(ObtainAuthToken):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# TODO implement proper schema https://drf-spectacular.readthedocs.io/en/latest/customization.html#replace-views-with-openapiviewextension
|
||||||
class RecipeUrlImportView(APIView):
|
class RecipeUrlImportView(APIView):
|
||||||
throttle_classes = [RecipeImportThrottle]
|
throttle_classes = [RecipeImportThrottle]
|
||||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||||
@@ -1575,6 +1571,7 @@ def import_files(request):
|
|||||||
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO implement schema https://drf-spectacular.readthedocs.io/en/latest/customization.html#replace-views-with-openapiviewextension
|
||||||
class ImportOpenData(APIView):
|
class ImportOpenData(APIView):
|
||||||
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
|
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||||
|
|
||||||
@@ -1625,6 +1622,7 @@ def get_recipe_provider(recipe):
|
|||||||
raise Exception('Provider not implemented')
|
raise Exception('Provider not implemented')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO implement proper schema https://drf-spectacular.readthedocs.io/en/latest/customization.html#replace-views-with-openapiviewextension
|
||||||
def update_recipe_links(recipe):
|
def update_recipe_links(recipe):
|
||||||
if not recipe.link:
|
if not recipe.link:
|
||||||
# TODO response validation in apis
|
# TODO response validation in apis
|
||||||
@@ -1683,7 +1681,7 @@ def sync_all(request):
|
|||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
# @schema(AutoSchema()) #TODO add proper schema
|
# @schema(AutoSchema()) #TODO add proper schema https://drf-spectacular.readthedocs.io/en/latest/customization.html#replace-views-with-openapiviewextension
|
||||||
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
|
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
|
||||||
def share_link(request, pk):
|
def share_link(request, pk):
|
||||||
if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
|
if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
|
||||||
@@ -1714,6 +1712,7 @@ def log_cooking(request, recipe_id):
|
|||||||
return {'error': 'recipe does not exist'}
|
return {'error': 'recipe does not exist'}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO implement proper schema
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
|
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
|
||||||
def get_plan_ical(request, from_date=datetime.date.today(), to_date=None):
|
def get_plan_ical(request, from_date=datetime.date.today(), to_date=None):
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import re
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import socket
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -325,7 +324,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SECURITY": [{ "ApiKeyAuth": [] }],
|
"SECURITY": [{"ApiKeyAuth": []}],
|
||||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||||
'REDOC_DIST': 'SIDECAR',
|
'REDOC_DIST': 'SIDECAR',
|
||||||
@@ -473,7 +472,7 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
print("Vite Dev Server is running")
|
print("Vite Dev Server is running")
|
||||||
DJANGO_VITE['default']['dev_mode'] = True
|
DJANGO_VITE['default']['dev_mode'] = True
|
||||||
except:
|
except Exception:
|
||||||
print("Running django-vite in production mode (no HMR)")
|
print("Running django-vite in production mode (no HMR)")
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
|
|||||||
Reference in New Issue
Block a user