Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
vabene1111 committed Nov 21, 2023
1 parent 9132ab8 commit 58e70c9
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 61 deletions.
19 changes: 19 additions & 0 deletions cookbook/migrations/0204_space_favicon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.5 on 2023-11-21 21:18

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('cookbook', '0203_alter_unique_contstraints'),
]

operations = [
migrations.AddField(
model_name='space',
name='favicon',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_favicon', to='cookbook.userfile'),
),
]
82 changes: 56 additions & 26 deletions cookbook/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ def get_or_create(self, *args, **kwargs):
if defaults:
kwargs = {**kwargs, **defaults}
# ManyToMany fields can't be set this way, so pop them out to save for later
fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
fields = [field.name for field in self.model._meta.get_fields() if
issubclass(type(field), ManyToManyField)]
many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
obj = self.model.add_root(**kwargs)
for field in many_to_many:
Expand Down Expand Up @@ -253,11 +254,13 @@ def get_name(self):
class Space(ExportModelOperationsMixin('space'), models.Model):
name = models.CharField(max_length=128, default='Default')
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_image')
favicon = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_favicon')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
created_at = models.DateTimeField(auto_now_add=True)
message = models.CharField(max_length=512, default='', blank=True)
max_recipes = models.IntegerField(default=0)
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
max_file_storage_mb = models.IntegerField(default=0, help_text=_(
'Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
max_users = models.IntegerField(default=0)
use_plural = models.BooleanField(default=True)
allow_sharing = models.BooleanField(default=True)
Expand Down Expand Up @@ -471,7 +474,8 @@ def __str__(self):
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_category_unique_open_data_slug_per_space')
models.UniqueConstraint(fields=['space', 'open_data_slug'],
name='supermarket_category_unique_open_data_slug_per_space')
]


Expand All @@ -490,7 +494,8 @@ def __str__(self):
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_unique_open_data_slug_per_space')
models.UniqueConstraint(fields=['space', 'open_data_slug'],
name='supermarket_unique_open_data_slug_per_space')
]


Expand Down Expand Up @@ -575,7 +580,8 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
url = models.CharField(max_length=1024, blank=True, null=True, default='')
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True,
on_delete=models.SET_NULL) # inherited field
ignore_shopping = models.BooleanField(default=False) # inherited field
onhand_users = models.ManyToManyField(User, blank=True)
description = models.TextField(default='', blank=True)
Expand All @@ -589,8 +595,10 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
properties_food_amount = models.DecimalField(default=100, max_digits=16, decimal_places=2, blank=True)
properties_food_unit = models.ForeignKey(Unit, on_delete=models.PROTECT, blank=True, null=True)

preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit')
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None,
related_name='preferred_unit')
preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None,
related_name='preferred_shopping_unit')
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)

open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
Expand All @@ -602,7 +610,8 @@ def __str__(self):

def delete(self):
if self.ingredient_set.all().exclude(step=None).count() > 0:
raise ProtectedError(self.name + _(" is part of a recipe step and cannot be deleted"), self.ingredient_set.all().exclude(step=None))
raise ProtectedError(self.name + _(" is part of a recipe step and cannot be deleted"),
self.ingredient_set.all().exclude(step=None))
else:
return super().delete()

Expand Down Expand Up @@ -652,16 +661,21 @@ def reset_inheritance(space=None, food=None):
food.get_descendants().update(**{f"{field}": False})
else:
# get food at root that have children that need updated
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, **{f"{field}": True}, space=space)).update(**{f"{field}": True})
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, **{f"{field}": False}, space=space)).update(**{f"{field}": False})
Food.include_descendants(
queryset=Food.objects.filter(depth=1, numchild__gt=0, **{f"{field}": True},
space=space)).update(**{f"{field}": True})
Food.include_descendants(
queryset=Food.objects.filter(depth=1, numchild__gt=0, **{f"{field}": False},
space=space)).update(**{f"{field}": False})

if 'supermarket_category' in inherit:
# when supermarket_category is null or blank assuming it is not set and not intended to be blank for all descedants
if food and food.supermarket_category:
food.get_descendants().update(supermarket_category=food.supermarket_category)
elif food is None:
# find top node that has category set
category_roots = Food.exclude_descendants(queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space))
category_roots = Food.exclude_descendants(
queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space))
for root in category_roots:
root.get_descendants().update(supermarket_category=root.supermarket_category)

Expand All @@ -680,7 +694,8 @@ class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model
base_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
base_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_base_relation')
converted_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
converted_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_converted_relation')
converted_unit = models.ForeignKey('Unit', on_delete=models.CASCADE,
related_name='unit_conversion_converted_relation')

food = models.ForeignKey('Food', on_delete=models.CASCADE, null=True, blank=True)

Expand All @@ -697,8 +712,10 @@ def __str__(self):

class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'base_unit', 'converted_unit', 'food'], name='f_unique_conversion_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_conversion_unique_open_data_slug_per_space')
models.UniqueConstraint(fields=['space', 'base_unit', 'converted_unit', 'food'],
name='f_unique_conversion_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'],
name='unit_conversion_unique_open_data_slug_per_space')
]


Expand Down Expand Up @@ -764,7 +781,8 @@ class PropertyType(models.Model, PermissionModelMixin):
order = models.IntegerField(default=0)
description = models.CharField(max_length=512, blank=True, null=True)
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')),
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))),
null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)

# TODO show if empty property?
Expand All @@ -779,7 +797,8 @@ def __str__(self):
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='property_type_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='property_type_unique_open_data_slug_per_space')
models.UniqueConstraint(fields=['space', 'open_data_slug'],
name='property_type_unique_open_data_slug_per_space')
]
ordering = ('order',)

Expand All @@ -788,7 +807,8 @@ class Property(models.Model, PermissionModelMixin):
property_amount = models.DecimalField(default=0, decimal_places=4, max_digits=32)
property_type = models.ForeignKey(PropertyType, on_delete=models.PROTECT)

import_food_id = models.IntegerField(null=True, blank=True) # field to hold food id when importing properties from the open data project
import_food_id = models.IntegerField(null=True,
blank=True) # field to hold food id when importing properties from the open data project

space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
Expand All @@ -798,7 +818,8 @@ def __str__(self):

class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'property_type', 'import_food_id'], name='property_unique_import_food_per_space')
models.UniqueConstraint(fields=['space', 'property_type', 'import_food_id'],
name='property_unique_import_food_per_space')
]


Expand Down Expand Up @@ -830,7 +851,8 @@ def __str__(self):

class RecipeManager(models.Manager.from_queryset(models.QuerySet)):
def get_queryset(self):
return super(RecipeManager, self).get_queryset().annotate(rating=Avg('cooklog__rating')).annotate(last_cooked=Max('cooklog__created_at'))
return super(RecipeManager, self).get_queryset().annotate(rating=Avg('cooklog__rating')).annotate(
last_cooked=Max('cooklog__created_at'))


class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin):
Expand Down Expand Up @@ -873,15 +895,19 @@ def get_related_recipes(self, levels=1):
# recipes for step recipe
step_recipes = Q(id__in=self.steps.exclude(step_recipe=None).values_list('step_recipe'))
# recipes for foods
food_recipes = Q(id__in=Food.objects.filter(ingredient__step__recipe=self).exclude(recipe=None).values_list('recipe'))
food_recipes = Q(
id__in=Food.objects.filter(ingredient__step__recipe=self).exclude(recipe=None).values_list('recipe'))
related_recipes = Recipe.objects.filter(step_recipes | food_recipes)
if levels == 1:
return related_recipes

# this can loop over multiple levels if you update the value of related_recipes at each step (maybe an array?)
# for now keeping it at 2 levels max, should be sufficient in 99.9% of scenarios
sub_step_recipes = Q(id__in=Step.objects.filter(recipe__in=related_recipes.values_list('steps')).exclude(step_recipe=None).values_list('step_recipe'))
sub_food_recipes = Q(id__in=Food.objects.filter(ingredient__step__recipe__in=related_recipes).exclude(recipe=None).values_list('recipe'))
sub_step_recipes = Q(id__in=Step.objects.filter(recipe__in=related_recipes.values_list('steps')).exclude(
step_recipe=None).values_list('step_recipe'))
sub_food_recipes = Q(
id__in=Food.objects.filter(ingredient__step__recipe__in=related_recipes).exclude(recipe=None).values_list(
'recipe'))
return Recipe.objects.filter(Q(id__in=related_recipes.values_list('id')) | sub_step_recipes | sub_food_recipes)

class Meta():
Expand Down Expand Up @@ -1016,7 +1042,8 @@ def __str__(self):

class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=32, blank=True, default='')
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True) # TODO make required after old shoppinglist deprecated
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True,
blank=True) # TODO make required after old shoppinglist deprecated
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
mealplan = models.ForeignKey(MealPlan, on_delete=models.CASCADE, null=True, blank=True)

Expand All @@ -1034,13 +1061,15 @@ def __str__(self):

def get_owner(self):
try:
return getattr(self.entries.first(), 'created_by', None) or getattr(self.shoppinglist_set.first(), 'created_by', None)
return getattr(self.entries.first(), 'created_by', None) or getattr(self.shoppinglist_set.first(),
'created_by', None)
except AttributeError:
return None


class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True,
related_name='entries')
food = models.ForeignKey(Food, on_delete=models.CASCADE, related_name='shopping_entries')
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)
Expand Down Expand Up @@ -1305,7 +1334,8 @@ def is_image(self):
return False

def save(self, *args, **kwargs):
if hasattr(self.file, 'file') and isinstance(self.file.file, UploadedFile) or isinstance(self.file.file, InMemoryUploadedFile):
if hasattr(self.file, 'file') and isinstance(self.file.file, UploadedFile) or isinstance(self.file.file,
InMemoryUploadedFile):
self.file.name = f'{uuid.uuid4()}' + pathlib.Path(self.file.name).suffix
self.file_size_kb = round(self.file.size / 1000)
super(UserFile, self).save(*args, **kwargs)
Expand Down
Loading

0 comments on commit 58e70c9

Please sign in to comment.