model changes and GenericAutoSchema

This commit is contained in:
smilerz
2021-10-07 12:35:46 -05:00
parent 1642224205
commit 3d674cfca6
6 changed files with 391 additions and 200 deletions

View File

@ -781,17 +781,16 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.get_owner().userpreference.mealplan_autoadd_shopping:
kwargs = {
'mealplan': self,
'space': self.space,
'created_by': self.get_owner()
}
if self.get_owner().userpreference.mealplan_autoexclude_onhand:
kwargs['ingredients'] = Ingredient.objects.filter(step__recipe=self.recipe, food__on_hand=False, space=self.space).values_list('id', flat=True)
ShoppingListEntry.list_from_recipe(**kwargs)
# TODO override create method to check if recipes are always added
# @classmethod
# def generate_shoppinglist(self, ingredients=None):
# recipe_list = ShoppingListRecipe.objects.create()
# if not ingredients:
# ingredients = Ingredient.objects.filter(step__recipe=self.recipe)
# for i in ingredients:
# ShoppingListEntry.objects.create(
# )
def get_label(self):
if self.title:
@ -831,20 +830,50 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True) # TODO deprecate
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True) # TODO remove when shoppinglist is deprecated
food = models.ForeignKey(Food, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, null=True, blank=True)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
order = models.IntegerField(default=0)
checked = models.BooleanField(default=False)
recipe = models.ForeignKey(Recipe, on_delete=models.SET_NULL, null=True, blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='shoppinglist__space')
objects = ScopedManager(space='space')
@classmethod
def list_from_recipe(self, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None):
try:
r = recipe or mealplan.recipe
except AttributeError:
raise ValueError(_("You must supply a recipe or mealplan"))
created_by = created_by or getattr(mealplan, 'created_by', None)
if not created_by:
raise ValueError(_("You must supply a created_by"))
servings = servings or getattr(mealplan, 'servings', 1.0)
if ingredients:
ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
else:
ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
shoppinglist = [
ShoppingListEntry(
list_recipe=list_recipe,
food=i.food,
unit=i.unit,
ingredient=i,
amount=i.amount * Decimal(servings),
created_by=created_by,
space=space
)
for i in ingredients
]
return ShoppingListEntry.objects.bulk_create(shoppinglist)
@classmethod
@atomic

View File

@ -3,11 +3,10 @@ from rest_framework.schemas.utils import is_list_view
class QueryParam(object):
def __init__(self, name, description=None, qtype='string', required=False):
def __init__(self, name, description=None, qtype='string'):
self.name = name
self.description = description
self.qtype = qtype
self.required = required
def __str__(self):
return f'{self.name}, {self.qtype}, {self.description}'
@ -20,7 +19,7 @@ class QueryParamAutoSchema(AutoSchema):
parameters = super().get_path_parameters(path, method)
for q in self.view.query_params:
parameters.append({
"name": q.name, "in": "query", "required": q.required,
"name": q.name, "in": "query", "required": False,
"description": q.description,
'schema': {'type': q.qtype, },
})

View File

@ -35,20 +35,13 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
except KeyError:
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
if bool(int(
self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
except AttributeError:
pass
except KeyError:
pass
try:
# another choice is to only return the fields when self.__class__ = serializer and not worry about 'extended'
if self.context['request'] and bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
else:
del fields['image']
del fields['numrecipe']
except KeyError:
pass
return fields
return fields
def get_image(self, obj):
# TODO add caching
@ -634,37 +627,18 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
read_only_fields = ('created_by',)
# TODO deprecate
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
recipe_name = serializers.ReadOnlyField(source='recipe.name')
mealplan_note = serializers.ReadOnlyField(source='mealplan.note')
mealplan_note = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()
def get_name(self, obj):
if not isinstance(value := obj.servings, Decimal):
value = Decimal(value)
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
def update(self, instance, validated_data):
if 'servings' in validated_data:
ShoppingListEntry.list_from_recipe(
list_recipe=instance,
servings=validated_data['servings'],
created_by=self.context['request'].user,
space=self.context['request'].space
)
return super().update(instance, validated_data)
def get_note_markdown(self, obj):
return obj.mealplan and markdown(obj.mealplan.note)
class Meta:
model = ShoppingListRecipe
fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note')
fields = ('id', 'recipe', 'mealplan', 'recipe_name', 'servings', 'mealplan_note')
read_only_fields = ('id',)
@ -674,36 +648,63 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserNameSerializer(read_only=True)
completed_at = serializers.DateTimeField(allow_null=True)
created_by = UserNameSerializer()
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
print('shoppinglist', self.__class__, self.parent.__class__)
# try:
# # this serializer is the parent serializer for the API
# api_serializer = self.context['view'].serializer_class
# except Exception:
# # this serializer is probably nested or a foreign key
# api_serializer = None
# autosync values are only needed for frequent 'checked' value updating
if self.context['request'] and bool(int(self.context['request'].query_params.get('autosync', False))):
for f in list(set(fields) - set(['id', 'checked'])):
del fields[f]
# extended values are computationally expensive and not needed in normal circumstances
elif not bool(int(self.context['request'].query_params.get('extended', False))) or not self.parent:
del fields['notes']
# elif bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
# pass
# else:
# del fields['recipe_mealplan']
return fields
def run_validation(self, data):
if (
data.get('checked', False)
and not (id := data.get('id', None))
and not ShoppingListEntry.objects.get(id=id).checked
):
# if checked flips from false to true set completed datetime
data['completed_at'] = timezone.now()
############################################################
# temporary while old and new shopping lists are both in use
try:
# this serializer is the parent serializer for the API
api_serializer = self.context['view'].serializer_class
except Exception:
# this serializer is probably nested or a foreign key
api_serializer = None
if self.context['request'].method == 'POST' and not self.__class__ == api_serializer:
data['space'] = self.context['request'].space.id
data['created_by'] = self.context['request'].user.id
############################################################
if self.context['request'].method == 'POST' and self.__class__ == api_serializer:
data['created_by'] = {'id': self.context['request'].user.id}
return super().run_validation(data)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
def update(self, instance, validated_data):
if validated_data['checked']:
validated_data['completed_at'] = timezone.now()
return super().update(instance, validated_data)
class Meta:
model = ShoppingListEntry
fields = (
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked',
'created_by', 'created_at', 'notes', 'completed_at'
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
'created_by', 'created_at', 'completed_at'
)
read_only_fields = ('id', 'created_by', 'created_at',)

View File

@ -120,4 +120,7 @@ def test_delete(u1_s1, u1_s2, obj_1):
# test sharing
# test completed entries still visible if today, but not yesterday
# test completed entries still visible if today, but not yesterday
# test create shopping list from recipe
# test create shopping list from mealplan
# test create shopping list from recipe, excluding ingredients