From 957c659a62825c40a7a5571a736cd325e69dcd89 Mon Sep 17 00:00:00 2001 From: smilerz Date: Thu, 30 Dec 2021 15:33:34 -0600 Subject: [PATCH] Squashed commit of shoppinglist_v2 --- .env.template | 4 + cookbook/admin.py | 2 +- cookbook/apps.py | 42 +- cookbook/forms.py | 7 +- cookbook/helper/recipe_html_import.py | 11 +- cookbook/helper/shopping_helper.py | 10 +- cookbook/integration/copymethat.py | 84 ++ cookbook/integration/integration.py | 13 +- .../0159_add_shoppinglistentry_fields.py | 17 +- .../0160_delete_shoppinglist_orphans.py | 2 +- cookbook/models.py | 26 +- cookbook/serializer.py | 20 +- cookbook/signals.py | 16 +- cookbook/static/django_js_reverse/reverse.js | 889 +----------------- cookbook/templates/base.html | 2 +- cookbook/templates/shopping_list.html | 2 +- cookbook/templates/url_import.html | 1 + cookbook/tests/api/test_api_food.py | 58 +- .../tests/api/test_api_shopping_recipe.py | 26 +- cookbook/tests/api/test_api_userpreference.py | 31 +- cookbook/views/api.py | 3 +- cookbook/views/import_export.py | 3 + cookbook/views/new.py | 5 +- cookbook/views/views.py | 2 +- docs/features/import_export.md | 5 + docs/install/docker.md | 99 +- recipes/settings.py | 1 + vue/src/apps/ModelListView/ModelListView.vue | 14 +- .../apps/RecipeEditView/RecipeEditView.vue | 15 +- .../RecipeSearchView/RecipeSearchView.vue | 2 +- vue/src/apps/RecipeView/RecipeView.vue | 5 + .../ShoppingListView/ShoppingListView.vue | 66 +- vue/src/components/Badges.vue | 72 +- vue/src/components/Badges/OnHand.vue | 8 +- vue/src/components/Badges/Shopping.vue | 24 +- vue/src/components/ContextMenu/ModelMenu.vue | 90 +- vue/src/components/GenericHorizontalCard.vue | 62 +- vue/src/components/GenericPill.vue | 7 +- vue/src/components/IngredientComponent.vue | 76 +- .../components/Modals/GenericModalForm.vue | 48 +- vue/src/components/Modals/ShoppingModal.vue | 4 +- vue/src/components/Modals/SmallText.vue | 20 + vue/src/components/Modals/TextInput.vue | 56 +- vue/src/components/ShoppingLineItem.vue | 17 +- vue/src/components/StepComponent.vue | 32 +- vue/src/locales/en.json | 8 +- vue/src/utils/models.js | 43 +- vue/src/utils/openapi/api.ts | 114 ++- vue/src/utils/utils.js | 9 +- 49 files changed, 701 insertions(+), 1472 deletions(-) create mode 100644 cookbook/integration/copymethat.py create mode 100644 vue/src/components/Modals/SmallText.vue diff --git a/.env.template b/.env.template index 36c2357d5..635e079c9 100644 --- a/.env.template +++ b/.env.template @@ -7,7 +7,9 @@ SQL_DEBUG=0 ALLOWED_HOSTS=* # random secret key, use for example `base64 /dev/urandom | head -c50` to generate one +# ---------------------------- REQUIRED ------------------------- SECRET_KEY= +# --------------------------------------------------------------- # your default timezone See https://timezonedb.com/time-zones for a list of timezones TIMEZONE=Europe/Berlin @@ -18,7 +20,9 @@ DB_ENGINE=django.db.backends.postgresql POSTGRES_HOST=db_recipes POSTGRES_PORT=5432 POSTGRES_USER=djangouser +# ---------------------------- REQUIRED ------------------------- POSTGRES_PASSWORD= +# --------------------------------------------------------------- POSTGRES_DB=djangodb # database connection string, when used overrides other database settings. diff --git a/cookbook/admin.py b/cookbook/admin.py index 8d521c41d..73e86ecf5 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -259,7 +259,7 @@ admin.site.register(ViewLog, ViewLogAdmin) class InviteLinkAdmin(admin.ModelAdmin): list_display = ( - 'group', 'valid_until', + 'group', 'valid_until', 'space', 'created_by', 'created_at', 'used_by' ) diff --git a/cookbook/apps.py b/cookbook/apps.py index 5654684df..e551319db 100644 --- a/cookbook/apps.py +++ b/cookbook/apps.py @@ -14,24 +14,24 @@ class CookbookConfig(AppConfig): def ready(self): import cookbook.signals # noqa - if not settings.DISABLE_TREE_FIX_STARTUP: - # when starting up run fix_tree to: - # a) make sure that nodes are sorted when switching between sort modes - # b) fix problems, if any, with tree consistency - with scopes_disabled(): - try: - from cookbook.models import Food, Keyword - Keyword.fix_tree(fix_paths=True) - Food.fix_tree(fix_paths=True) - except OperationalError: - if DEBUG: - traceback.print_exc() - pass # if model does not exist there is no need to fix it - except ProgrammingError: - if DEBUG: - traceback.print_exc() - pass # if migration has not been run database cannot be fixed yet - except Exception: - if DEBUG: - traceback.print_exc() - pass # dont break startup just because fix could not run, need to investigate cases when this happens + # if not settings.DISABLE_TREE_FIX_STARTUP: + # # when starting up run fix_tree to: + # # a) make sure that nodes are sorted when switching between sort modes + # # b) fix problems, if any, with tree consistency + # with scopes_disabled(): + # try: + # from cookbook.models import Food, Keyword + # Keyword.fix_tree(fix_paths=True) + # Food.fix_tree(fix_paths=True) + # except OperationalError: + # if DEBUG: + # traceback.print_exc() + # pass # if model does not exist there is no need to fix it + # except ProgrammingError: + # if DEBUG: + # traceback.print_exc() + # pass # if migration has not been run database cannot be fixed yet + # except Exception: + # if DEBUG: + # traceback.print_exc() + # pass # dont break startup just because fix could not run, need to investigate cases when this happens diff --git a/cookbook/forms.py b/cookbook/forms.py index 48172ccbe..ea86d1cb8 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -7,7 +7,7 @@ from django_scopes import scopes_disabled from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField from hcaptcha.fields import hCaptchaField -from .models import (Comment, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook, +from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference) @@ -155,13 +155,14 @@ class ImportExportBase(forms.Form): OPENEATS = 'OPENEATS' PLANTOEAT = 'PLANTOEAT' COOKBOOKAPP = 'COOKBOOKAPP' + COPYMETHAT = 'COPYMETHAT' type = forms.ChoiceField(choices=( (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), - (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), + (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), )) @@ -524,7 +525,7 @@ class SpacePreferenceForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # populates the post - self.fields['food_inherit'].queryset = Food.inherit_fields + self.fields['food_inherit'].queryset = Food.inheritable_fields class Meta: model = Space diff --git a/cookbook/helper/recipe_html_import.py b/cookbook/helper/recipe_html_import.py index 7b779add0..acf72917b 100644 --- a/cookbook/helper/recipe_html_import.py +++ b/cookbook/helper/recipe_html_import.py @@ -1,13 +1,14 @@ import json import re +from json import JSONDecodeError +from urllib.parse import unquote from bs4 import BeautifulSoup from bs4.element import Tag +from recipe_scrapers._utils import get_host_name, normalize_string + from cookbook.helper import recipe_url_import as helper from cookbook.helper.scrapers.scrapers import text_scraper -from json import JSONDecodeError -from recipe_scrapers._utils import get_host_name, normalize_string -from urllib.parse import unquote def get_recipe_from_source(text, url, request): @@ -58,7 +59,7 @@ def get_recipe_from_source(text, url, request): return kid_list recipe_json = { - 'name': '', + 'name': '', 'url': '', 'description': '', 'image': '', @@ -188,6 +189,6 @@ def remove_graph(el): for x in el['@graph']: if '@type' in x and x['@type'] == 'Recipe': el = x - except TypeError: + except (TypeError, JSONDecodeError): pass return el diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py index e19de68eb..75a70d36b 100644 --- a/cookbook/helper/shopping_helper.py +++ b/cookbook/helper/shopping_helper.py @@ -82,7 +82,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None ingredients = Ingredient.objects.filter(step__recipe=r, space=space) if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand: - ingredients = ingredients.exclude(food__on_hand=True) + ingredients = ingredients.exclude(food__food_onhand=True) if related := created_by.userpreference.mealplan_autoinclude_related: # TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans @@ -93,7 +93,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None # TODO once/if Steps can have a serving size this needs to be refactored if exclude_onhand: # if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior - related_step_ing += Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space).values_list('id', flat=True) + related_step_ing += Ingredient.objects.filter(step__recipe=x, food__food_onhand=False, space=space).values_list('id', flat=True) else: related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True) @@ -101,10 +101,10 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None if ingredients.filter(food__recipe=x).exists(): for ing in ingredients.filter(food__recipe=x): if exclude_onhand: - x_ing = Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space) + x_ing = Ingredient.objects.filter(step__recipe=x, food__food_onhand=False, space=space) else: x_ing = Ingredient.objects.filter(step__recipe=x, space=space) - for i in [x for x in x_ing if not x.food.ignore_shopping]: + for i in [x for x in x_ing]: ShoppingListEntry.objects.create( list_recipe=list_recipe, food=i.food, @@ -139,7 +139,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None sle.save() # add any missing Entrys - for i in [x for x in add_ingredients if x.food and not x.food.ignore_shopping]: + for i in [x for x in add_ingredients if x.food]: ShoppingListEntry.objects.create( list_recipe=list_recipe, diff --git a/cookbook/integration/copymethat.py b/cookbook/integration/copymethat.py new file mode 100644 index 000000000..4f4a217e5 --- /dev/null +++ b/cookbook/integration/copymethat.py @@ -0,0 +1,84 @@ +import re +from io import BytesIO +from zipfile import ZipFile + +from bs4 import BeautifulSoup + +from cookbook.helper.ingredient_parser import IngredientParser +from cookbook.helper.recipe_html_import import get_recipe_from_source +from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Ingredient, Keyword +from recipes.settings import DEBUG + + +class CopyMeThat(Integration): + + def import_file_name_filter(self, zip_info_object): + if DEBUG: + print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html') + return zip_info_object.filename == 'recipes.html' + + def get_recipe_from_file(self, file): + # 'file' comes is as a beautifulsoup object + recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, ) + + for category in file.find_all("span", {"class": "recipeCategory"}): + keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space) + recipe.keywords.add(keyword) + + try: + recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip()) + recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip()) + recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip()) + recipe.save() + except AttributeError: + pass + + step = Step.objects.create(instruction='', space=self.request.space, ) + + ingredient_parser = IngredientParser(self.request, True) + for ingredient in file.find_all("li", {"class": "recipeIngredient"}): + if ingredient.text == "": + continue + amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip()) + f = ingredient_parser.get_food(ingredient) + u = ingredient_parser.get_unit(unit) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note, space=self.request.space, + )) + + for s in file.find_all("li", {"class": "instruction"}): + if s.text == "": + continue + step.instruction += s.text.strip() + ' \n\n' + + for s in file.find_all("li", {"class": "recipeNote"}): + if s.text == "": + continue + step.instruction += s.text.strip() + ' \n\n' + + try: + if file.find("a", {"id": "original_link"}).text != '': + step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text + step.save() + except AttributeError: + pass + + recipe.steps.add(step) + + # import the Primary recipe image that is stored in the Zip + try: + for f in self.files: + if '.zip' in f['name']: + import_zip = ZipFile(f['file']) + self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipeImage").get("src"))), filetype='.jpeg') + except Exception as e: + print(recipe.name, ': failed to import image ', str(e)) + + recipe.save() + return recipe + + def split_recipe_file(self, file): + soup = BeautifulSoup(file, "html.parser") + return soup.find_all("div", {"class": "recipe"}) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index deeaca532..cecab4a20 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -5,6 +5,7 @@ import uuid from io import BytesIO, StringIO from zipfile import BadZipFile, ZipFile +from bs4 import Tag from django.core.exceptions import ObjectDoesNotExist from django.core.files import File from django.db import IntegrityError @@ -16,7 +17,7 @@ from django_scopes import scope from cookbook.forms import ImportExportBase from cookbook.helper.image_processing import get_filetype, handle_image from cookbook.models import Keyword, Recipe -from recipes.settings import DATABASES, DEBUG +from recipes.settings import DEBUG class Integration: @@ -153,9 +154,17 @@ class Integration: file_list.append(z) il.total_recipes += len(file_list) + import cookbook + if isinstance(self, cookbook.integration.copymethat.CopyMeThat): + file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html'))) + il.total_recipes += len(file_list) + for z in file_list: try: - recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) + if isinstance(z, Tag): + recipe = self.get_recipe_from_file(z) + else: + recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) recipe.keywords.add(self.keyword) il.msg += f'{recipe.pk} - {recipe.name} \n' self.handle_duplicates(recipe, import_duplicates) diff --git a/cookbook/migrations/0159_add_shoppinglistentry_fields.py b/cookbook/migrations/0159_add_shoppinglistentry_fields.py index 9df880d08..32b0c9ce5 100644 --- a/cookbook/migrations/0159_add_shoppinglistentry_fields.py +++ b/cookbook/migrations/0159_add_shoppinglistentry_fields.py @@ -28,11 +28,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name='food', - name='on_hand', - field=models.BooleanField(default=False), - ), migrations.AddField( model_name='shoppinglistentry', name='completed_at', @@ -105,11 +100,6 @@ class Migration(migrations.Migration): ], bases=(models.Model, PermissionModelMixin), ), - migrations.AddField( - model_name='food', - name='inherit', - field=models.BooleanField(default=False), - ), migrations.AddField( model_name='userpreference', name='mealplan_autoinclude_related', @@ -117,7 +107,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='food', - name='ignore_inherit', + name='inherit_fields', field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'), ), migrations.AddField( @@ -145,5 +135,10 @@ class Migration(migrations.Migration): name='shopping_recent_days', field=models.PositiveIntegerField(default=7), ), + migrations.RenameField( + model_name='food', + old_name='ignore_shopping', + new_name='food_onhand', + ), migrations.RunPython(copy_values_to_sle), ] diff --git a/cookbook/migrations/0160_delete_shoppinglist_orphans.py b/cookbook/migrations/0160_delete_shoppinglist_orphans.py index 27fb0edb9..26e086564 100644 --- a/cookbook/migrations/0160_delete_shoppinglist_orphans.py +++ b/cookbook/migrations/0160_delete_shoppinglist_orphans.py @@ -21,7 +21,7 @@ def delete_orphaned_sle(apps, schema_editor): def create_inheritfields(apps, schema_editor): FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category') - FoodInheritField.objects.create(name='Ignore Shopping', field='ignore_shopping') + FoodInheritField.objects.create(name='On Hand', field='food_onhand') FoodInheritField.objects.create(name='Diet', field='diet') FoodInheritField.objects.create(name='Substitute', field='substitute') FoodInheritField.objects.create(name='Substitute Children', field='substitute_children') diff --git a/cookbook/models.py b/cookbook/models.py index 78eebae33..bc167ba47 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -482,7 +482,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): # exclude fields not implemented yet - inherit_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings']) + inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings']) # WARNING: Food inheritance relies on post_save signals, avoid using UPDATE to update Food objects unless you intend to bypass those signals if SORT_TREE_BY_NAME: @@ -490,11 +490,9 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL) supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) - ignore_shopping = models.BooleanField(default=False) # inherited field + food_onhand = models.BooleanField(default=False) # inherited field description = models.TextField(default='', blank=True) - on_hand = models.BooleanField(default=False) - inherit = models.BooleanField(default=False) - ignore_inherit = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive? + inherit_fields = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive? space = models.ForeignKey(Space, on_delete=models.CASCADE) objects = ScopedManager(space='space', _manager_class=TreeManager) @@ -512,34 +510,30 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): def reset_inheritance(space=None): # resets inheritted fields to the space defaults and updates all inheritted fields to root object values inherit = space.food_inherit.all() - ignore_inherit = Food.inherit_fields.difference(inherit) + # remove all inherited fields from food + Through = Food.objects.filter(space=space).first().inherit_fields.through + Through.objects.all().delete() # food is going to inherit attributes if space.food_inherit.all().count() > 0: - # using update to avoid creating a N*depth! save signals - Food.objects.filter(space=space).update(inherit=True) # ManyToMany cannot be updated through an UPDATE operation - Through = Food.objects.first().ignore_inherit.through - Through.objects.all().delete() - for i in ignore_inherit: + for i in inherit: Through.objects.bulk_create([ Through(food_id=x, foodinheritfield_id=i.id) for x in Food.objects.filter(space=space).values_list('id', flat=True) ]) inherit = inherit.values_list('field', flat=True) - if 'ignore_shopping' in inherit: + if 'food_onhand' in inherit: # get food at root that have children that need updated - Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=True)).update(ignore_shopping=True) - Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=False)).update(ignore_shopping=False) + Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=True)).update(food_onhand=True) + Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=False)).update(food_onhand=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 # 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)) for root in category_roots: root.get_descendants().update(supermarket_category=root.supermarket_category) - else: # food is not going to inherit any attributes - Food.objects.filter(space=space).update(inherit=False) class Meta: constraints = [ diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 4fe1b8148..c43b52a7c 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -157,15 +157,9 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer): class UserPreferenceSerializer(serializers.ModelSerializer): - # food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True) - food_ignore_default = serializers.SerializerMethodField('get_ignore_default') + food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True) plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True) - # TODO decide: default inherit field values for foods are being handled via VUE client through user preference - # should inherit field instead be set during the django model create? - def get_ignore_default(self, obj): - return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data - def create(self, validated_data): if not validated_data.get('user', None): raise ValidationError(_('A user is required')) @@ -181,7 +175,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer): model = UserPreference fields = ( 'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share', - 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay', + 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay', 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix', 'filter_to_supermarket' ) @@ -305,7 +299,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin): model = Keyword fields = ( 'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', - 'updated_at') + 'updated_at', 'full_name') read_only_fields = ('id', 'label', 'numchild', 'parent', 'image') @@ -376,7 +370,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False) recipe = RecipeSimpleSerializer(allow_null=True, required=False) shopping = serializers.SerializerMethodField('get_shopping_status') - ignore_inherit = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) + inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) recipe_filter = 'steps__ingredients__food' @@ -402,8 +396,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR class Meta: model = Food fields = ( - 'id', 'name', 'description', 'shopping', 'recipe', 'ignore_shopping', 'supermarket_category', - 'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'inherit', 'ignore_inherit', + 'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category', + 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name' ) read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe') @@ -867,7 +861,7 @@ class FoodExportSerializer(FoodSerializer): class Meta: model = Food - fields = ('name', 'ignore_shopping', 'supermarket_category', 'on_hand') + fields = ('name', 'food_onhand', 'supermarket_category',) class IngredientExportSerializer(WritableNestedModelSerializer): diff --git a/cookbook/signals.py b/cookbook/signals.py index 7fe8384dc..47e5bf9ea 100644 --- a/cookbook/signals.py +++ b/cookbook/signals.py @@ -66,17 +66,17 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): if not instance: return - inherit = Food.inherit_fields.difference(instance.ignore_inherit.all()) + inherit = instance.inherit_fields.all() # nothing to apply from parent and nothing to apply to children - if (not instance.inherit or not instance.parent or inherit.count() == 0) and instance.numchild == 0: + if (not instance.parent or inherit.count() == 0) and instance.numchild == 0: return inherit = inherit.values_list('field', flat=True) # apply changes from parent to instance for each inheritted field - if instance.inherit and instance.parent and inherit.count() > 0: + if instance.parent and inherit.count() > 0: parent = instance.get_parent() - if 'ignore_shopping' in inherit: - instance.ignore_shopping = parent.ignore_shopping + if 'food_onhand' in inherit: + instance.food_onhand = parent.food_onhand # if supermarket_category is not set, do not cascade - if this becomes non-intuitive can change if 'supermarket_category' in inherit and parent.supermarket_category: instance.supermarket_category = parent.supermarket_category @@ -89,13 +89,13 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): # TODO figure out how to generalize this # apply changes to direct children - depend on save signals for those objects to cascade inheritance down _save = [] - for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='ignore_shopping'): - child.ignore_shopping = instance.ignore_shopping + for child in instance.get_children().filter(inherit_fields__field='food_onhand'): + child.food_onhand = instance.food_onhand _save.append(child) # don't cascade empty supermarket category if instance.supermarket_category: # apply changes to direct children - depend on save signals for those objects to cascade inheritance down - for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='supermarket_category'): + for child in instance.get_children().filter(inherit_fields__field='supermarket_category'): child.supermarket_category = instance.supermarket_category _save.append(child) for child in set(_save): diff --git a/cookbook/static/django_js_reverse/reverse.js b/cookbook/static/django_js_reverse/reverse.js index 7974ee93e..437faa00a 100644 --- a/cookbook/static/django_js_reverse/reverse.js +++ b/cookbook/static/django_js_reverse/reverse.js @@ -1,875 +1,14 @@ -this.Urls = (function () { - "use strict" - var data = { - urls: [ - ["account_change_password", [["accounts/password/change/", []]]], - ["account_confirm_email", [["accounts/confirm-email/%(key)s/", ["key"]]]], - ["account_email", [["accounts/email/", []]]], - ["account_email_verification_sent", [["accounts/confirm-email/", []]]], - ["account_inactive", [["accounts/inactive/", []]]], - ["account_login", [["accounts/login/", []]]], - ["account_logout", [["accounts/logout/", []]]], - ["account_reset_password", [["accounts/password/reset/", []]]], - ["account_reset_password_done", [["accounts/password/reset/done/", []]]], - ["account_reset_password_from_key", [["accounts/password/reset/key/%(uidb36)s-%(key)s/", ["uidb36", "key"]]]], - ["account_reset_password_from_key_done", [["accounts/password/reset/key/done/", []]]], - ["account_set_password", [["accounts/password/set/", []]]], - ["account_signup", [["accounts/signup/", []]]], - ["admin:account_emailaddress_add", [["admin/account/emailaddress/add/", []]]], - ["admin:account_emailaddress_change", [["admin/account/emailaddress/%(object_id)s/change/", ["object_id"]]]], - ["admin:account_emailaddress_changelist", [["admin/account/emailaddress/", []]]], - ["admin:account_emailaddress_delete", [["admin/account/emailaddress/%(object_id)s/delete/", ["object_id"]]]], - ["admin:account_emailaddress_history", [["admin/account/emailaddress/%(object_id)s/history/", ["object_id"]]]], - ["admin:app_list", [["admin/%(app_label)s/", ["app_label"]]]], - ["admin:auth_user_add", [["admin/auth/user/add/", []]]], - ["admin:auth_user_change", [["admin/auth/user/%(object_id)s/change/", ["object_id"]]]], - ["admin:auth_user_changelist", [["admin/auth/user/", []]]], - ["admin:auth_user_delete", [["admin/auth/user/%(object_id)s/delete/", ["object_id"]]]], - ["admin:auth_user_history", [["admin/auth/user/%(object_id)s/history/", ["object_id"]]]], - ["admin:auth_user_password_change", [["admin/auth/user/%(id)s/password/", ["id"]]]], - ["admin:authtoken_tokenproxy_add", [["admin/authtoken/tokenproxy/add/", []]]], - ["admin:authtoken_tokenproxy_change", [["admin/authtoken/tokenproxy/%(object_id)s/change/", ["object_id"]]]], - ["admin:authtoken_tokenproxy_changelist", [["admin/authtoken/tokenproxy/", []]]], - ["admin:authtoken_tokenproxy_delete", [["admin/authtoken/tokenproxy/%(object_id)s/delete/", ["object_id"]]]], - ["admin:authtoken_tokenproxy_history", [["admin/authtoken/tokenproxy/%(object_id)s/history/", ["object_id"]]]], - ["admin:autocomplete", [["admin/autocomplete/", []]]], - ["admin:cookbook_bookmarkletimport_add", [["admin/cookbook/bookmarkletimport/add/", []]]], - ["admin:cookbook_bookmarkletimport_change", [["admin/cookbook/bookmarkletimport/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_bookmarkletimport_changelist", [["admin/cookbook/bookmarkletimport/", []]]], - ["admin:cookbook_bookmarkletimport_delete", [["admin/cookbook/bookmarkletimport/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_bookmarkletimport_history", [["admin/cookbook/bookmarkletimport/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_comment_add", [["admin/cookbook/comment/add/", []]]], - ["admin:cookbook_comment_change", [["admin/cookbook/comment/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_comment_changelist", [["admin/cookbook/comment/", []]]], - ["admin:cookbook_comment_delete", [["admin/cookbook/comment/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_comment_history", [["admin/cookbook/comment/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_cooklog_add", [["admin/cookbook/cooklog/add/", []]]], - ["admin:cookbook_cooklog_change", [["admin/cookbook/cooklog/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_cooklog_changelist", [["admin/cookbook/cooklog/", []]]], - ["admin:cookbook_cooklog_delete", [["admin/cookbook/cooklog/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_cooklog_history", [["admin/cookbook/cooklog/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_food_add", [["admin/cookbook/food/add/", []]]], - ["admin:cookbook_food_change", [["admin/cookbook/food/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_food_changelist", [["admin/cookbook/food/", []]]], - ["admin:cookbook_food_delete", [["admin/cookbook/food/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_food_history", [["admin/cookbook/food/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_importlog_add", [["admin/cookbook/importlog/add/", []]]], - ["admin:cookbook_importlog_change", [["admin/cookbook/importlog/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_importlog_changelist", [["admin/cookbook/importlog/", []]]], - ["admin:cookbook_importlog_delete", [["admin/cookbook/importlog/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_importlog_history", [["admin/cookbook/importlog/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_ingredient_add", [["admin/cookbook/ingredient/add/", []]]], - ["admin:cookbook_ingredient_change", [["admin/cookbook/ingredient/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_ingredient_changelist", [["admin/cookbook/ingredient/", []]]], - ["admin:cookbook_ingredient_delete", [["admin/cookbook/ingredient/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_ingredient_history", [["admin/cookbook/ingredient/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_invitelink_add", [["admin/cookbook/invitelink/add/", []]]], - ["admin:cookbook_invitelink_change", [["admin/cookbook/invitelink/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_invitelink_changelist", [["admin/cookbook/invitelink/", []]]], - ["admin:cookbook_invitelink_delete", [["admin/cookbook/invitelink/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_invitelink_history", [["admin/cookbook/invitelink/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_keyword_add", [["admin/cookbook/keyword/add/", []]]], - ["admin:cookbook_keyword_change", [["admin/cookbook/keyword/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_keyword_changelist", [["admin/cookbook/keyword/", []]]], - ["admin:cookbook_keyword_delete", [["admin/cookbook/keyword/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_keyword_history", [["admin/cookbook/keyword/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_mealplan_add", [["admin/cookbook/mealplan/add/", []]]], - ["admin:cookbook_mealplan_change", [["admin/cookbook/mealplan/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_mealplan_changelist", [["admin/cookbook/mealplan/", []]]], - ["admin:cookbook_mealplan_delete", [["admin/cookbook/mealplan/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_mealplan_history", [["admin/cookbook/mealplan/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_mealtype_add", [["admin/cookbook/mealtype/add/", []]]], - ["admin:cookbook_mealtype_change", [["admin/cookbook/mealtype/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_mealtype_changelist", [["admin/cookbook/mealtype/", []]]], - ["admin:cookbook_mealtype_delete", [["admin/cookbook/mealtype/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_mealtype_history", [["admin/cookbook/mealtype/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_nutritioninformation_add", [["admin/cookbook/nutritioninformation/add/", []]]], - ["admin:cookbook_nutritioninformation_change", [["admin/cookbook/nutritioninformation/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_nutritioninformation_changelist", [["admin/cookbook/nutritioninformation/", []]]], - ["admin:cookbook_nutritioninformation_delete", [["admin/cookbook/nutritioninformation/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_nutritioninformation_history", [["admin/cookbook/nutritioninformation/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_recipe_add", [["admin/cookbook/recipe/add/", []]]], - ["admin:cookbook_recipe_change", [["admin/cookbook/recipe/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_recipe_changelist", [["admin/cookbook/recipe/", []]]], - ["admin:cookbook_recipe_delete", [["admin/cookbook/recipe/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_recipe_history", [["admin/cookbook/recipe/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_recipebook_add", [["admin/cookbook/recipebook/add/", []]]], - ["admin:cookbook_recipebook_change", [["admin/cookbook/recipebook/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_recipebook_changelist", [["admin/cookbook/recipebook/", []]]], - ["admin:cookbook_recipebook_delete", [["admin/cookbook/recipebook/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_recipebook_history", [["admin/cookbook/recipebook/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_recipebookentry_add", [["admin/cookbook/recipebookentry/add/", []]]], - ["admin:cookbook_recipebookentry_change", [["admin/cookbook/recipebookentry/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_recipebookentry_changelist", [["admin/cookbook/recipebookentry/", []]]], - ["admin:cookbook_recipebookentry_delete", [["admin/cookbook/recipebookentry/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_recipebookentry_history", [["admin/cookbook/recipebookentry/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_recipeimport_add", [["admin/cookbook/recipeimport/add/", []]]], - ["admin:cookbook_recipeimport_change", [["admin/cookbook/recipeimport/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_recipeimport_changelist", [["admin/cookbook/recipeimport/", []]]], - ["admin:cookbook_recipeimport_delete", [["admin/cookbook/recipeimport/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_recipeimport_history", [["admin/cookbook/recipeimport/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_searchpreference_add", [["admin/cookbook/searchpreference/add/", []]]], - ["admin:cookbook_searchpreference_change", [["admin/cookbook/searchpreference/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_searchpreference_changelist", [["admin/cookbook/searchpreference/", []]]], - ["admin:cookbook_searchpreference_delete", [["admin/cookbook/searchpreference/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_searchpreference_history", [["admin/cookbook/searchpreference/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_sharelink_add", [["admin/cookbook/sharelink/add/", []]]], - ["admin:cookbook_sharelink_change", [["admin/cookbook/sharelink/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_sharelink_changelist", [["admin/cookbook/sharelink/", []]]], - ["admin:cookbook_sharelink_delete", [["admin/cookbook/sharelink/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_sharelink_history", [["admin/cookbook/sharelink/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_shoppinglist_add", [["admin/cookbook/shoppinglist/add/", []]]], - ["admin:cookbook_shoppinglist_change", [["admin/cookbook/shoppinglist/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_shoppinglist_changelist", [["admin/cookbook/shoppinglist/", []]]], - ["admin:cookbook_shoppinglist_delete", [["admin/cookbook/shoppinglist/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_shoppinglist_history", [["admin/cookbook/shoppinglist/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_shoppinglistentry_add", [["admin/cookbook/shoppinglistentry/add/", []]]], - ["admin:cookbook_shoppinglistentry_change", [["admin/cookbook/shoppinglistentry/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_shoppinglistentry_changelist", [["admin/cookbook/shoppinglistentry/", []]]], - ["admin:cookbook_shoppinglistentry_delete", [["admin/cookbook/shoppinglistentry/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_shoppinglistentry_history", [["admin/cookbook/shoppinglistentry/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_shoppinglistrecipe_add", [["admin/cookbook/shoppinglistrecipe/add/", []]]], - ["admin:cookbook_shoppinglistrecipe_change", [["admin/cookbook/shoppinglistrecipe/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_shoppinglistrecipe_changelist", [["admin/cookbook/shoppinglistrecipe/", []]]], - ["admin:cookbook_shoppinglistrecipe_delete", [["admin/cookbook/shoppinglistrecipe/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_shoppinglistrecipe_history", [["admin/cookbook/shoppinglistrecipe/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_space_add", [["admin/cookbook/space/add/", []]]], - ["admin:cookbook_space_change", [["admin/cookbook/space/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_space_changelist", [["admin/cookbook/space/", []]]], - ["admin:cookbook_space_delete", [["admin/cookbook/space/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_space_history", [["admin/cookbook/space/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_step_add", [["admin/cookbook/step/add/", []]]], - ["admin:cookbook_step_change", [["admin/cookbook/step/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_step_changelist", [["admin/cookbook/step/", []]]], - ["admin:cookbook_step_delete", [["admin/cookbook/step/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_step_history", [["admin/cookbook/step/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_storage_add", [["admin/cookbook/storage/add/", []]]], - ["admin:cookbook_storage_change", [["admin/cookbook/storage/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_storage_changelist", [["admin/cookbook/storage/", []]]], - ["admin:cookbook_storage_delete", [["admin/cookbook/storage/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_storage_history", [["admin/cookbook/storage/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_supermarket_add", [["admin/cookbook/supermarket/add/", []]]], - ["admin:cookbook_supermarket_change", [["admin/cookbook/supermarket/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_supermarket_changelist", [["admin/cookbook/supermarket/", []]]], - ["admin:cookbook_supermarket_delete", [["admin/cookbook/supermarket/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_supermarket_history", [["admin/cookbook/supermarket/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_supermarketcategory_add", [["admin/cookbook/supermarketcategory/add/", []]]], - ["admin:cookbook_supermarketcategory_change", [["admin/cookbook/supermarketcategory/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_supermarketcategory_changelist", [["admin/cookbook/supermarketcategory/", []]]], - ["admin:cookbook_supermarketcategory_delete", [["admin/cookbook/supermarketcategory/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_supermarketcategory_history", [["admin/cookbook/supermarketcategory/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_sync_add", [["admin/cookbook/sync/add/", []]]], - ["admin:cookbook_sync_change", [["admin/cookbook/sync/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_sync_changelist", [["admin/cookbook/sync/", []]]], - ["admin:cookbook_sync_delete", [["admin/cookbook/sync/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_sync_history", [["admin/cookbook/sync/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_synclog_add", [["admin/cookbook/synclog/add/", []]]], - ["admin:cookbook_synclog_change", [["admin/cookbook/synclog/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_synclog_changelist", [["admin/cookbook/synclog/", []]]], - ["admin:cookbook_synclog_delete", [["admin/cookbook/synclog/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_synclog_history", [["admin/cookbook/synclog/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_telegrambot_add", [["admin/cookbook/telegrambot/add/", []]]], - ["admin:cookbook_telegrambot_change", [["admin/cookbook/telegrambot/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_telegrambot_changelist", [["admin/cookbook/telegrambot/", []]]], - ["admin:cookbook_telegrambot_delete", [["admin/cookbook/telegrambot/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_telegrambot_history", [["admin/cookbook/telegrambot/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_unit_add", [["admin/cookbook/unit/add/", []]]], - ["admin:cookbook_unit_change", [["admin/cookbook/unit/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_unit_changelist", [["admin/cookbook/unit/", []]]], - ["admin:cookbook_unit_delete", [["admin/cookbook/unit/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_unit_history", [["admin/cookbook/unit/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_userfile_add", [["admin/cookbook/userfile/add/", []]]], - ["admin:cookbook_userfile_change", [["admin/cookbook/userfile/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_userfile_changelist", [["admin/cookbook/userfile/", []]]], - ["admin:cookbook_userfile_delete", [["admin/cookbook/userfile/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_userfile_history", [["admin/cookbook/userfile/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_userpreference_add", [["admin/cookbook/userpreference/add/", []]]], - ["admin:cookbook_userpreference_change", [["admin/cookbook/userpreference/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_userpreference_changelist", [["admin/cookbook/userpreference/", []]]], - ["admin:cookbook_userpreference_delete", [["admin/cookbook/userpreference/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_userpreference_history", [["admin/cookbook/userpreference/%(object_id)s/history/", ["object_id"]]]], - ["admin:cookbook_viewlog_add", [["admin/cookbook/viewlog/add/", []]]], - ["admin:cookbook_viewlog_change", [["admin/cookbook/viewlog/%(object_id)s/change/", ["object_id"]]]], - ["admin:cookbook_viewlog_changelist", [["admin/cookbook/viewlog/", []]]], - ["admin:cookbook_viewlog_delete", [["admin/cookbook/viewlog/%(object_id)s/delete/", ["object_id"]]]], - ["admin:cookbook_viewlog_history", [["admin/cookbook/viewlog/%(object_id)s/history/", ["object_id"]]]], - ["admin:index", [["admin/", []]]], - [ - "admin:javascript-catalog", - [ - ["admin/cookbook/food/jsi18n/", []], - ["admin/cookbook/keyword/jsi18n/", []], - ], - ], - ["admin:jsi18n", [["admin/jsi18n/", []]]], - ["admin:login", [["admin/login/", []]]], - ["admin:logout", [["admin/logout/", []]]], - ["admin:password_change", [["admin/password_change/", []]]], - ["admin:password_change_done", [["admin/password_change/done/", []]]], - ["admin:sites_site_add", [["admin/sites/site/add/", []]]], - ["admin:sites_site_change", [["admin/sites/site/%(object_id)s/change/", ["object_id"]]]], - ["admin:sites_site_changelist", [["admin/sites/site/", []]]], - ["admin:sites_site_delete", [["admin/sites/site/%(object_id)s/delete/", ["object_id"]]]], - ["admin:sites_site_history", [["admin/sites/site/%(object_id)s/history/", ["object_id"]]]], - ["admin:socialaccount_socialaccount_add", [["admin/socialaccount/socialaccount/add/", []]]], - ["admin:socialaccount_socialaccount_change", [["admin/socialaccount/socialaccount/%(object_id)s/change/", ["object_id"]]]], - ["admin:socialaccount_socialaccount_changelist", [["admin/socialaccount/socialaccount/", []]]], - ["admin:socialaccount_socialaccount_delete", [["admin/socialaccount/socialaccount/%(object_id)s/delete/", ["object_id"]]]], - ["admin:socialaccount_socialaccount_history", [["admin/socialaccount/socialaccount/%(object_id)s/history/", ["object_id"]]]], - ["admin:socialaccount_socialapp_add", [["admin/socialaccount/socialapp/add/", []]]], - ["admin:socialaccount_socialapp_change", [["admin/socialaccount/socialapp/%(object_id)s/change/", ["object_id"]]]], - ["admin:socialaccount_socialapp_changelist", [["admin/socialaccount/socialapp/", []]]], - ["admin:socialaccount_socialapp_delete", [["admin/socialaccount/socialapp/%(object_id)s/delete/", ["object_id"]]]], - ["admin:socialaccount_socialapp_history", [["admin/socialaccount/socialapp/%(object_id)s/history/", ["object_id"]]]], - ["admin:socialaccount_socialtoken_add", [["admin/socialaccount/socialtoken/add/", []]]], - ["admin:socialaccount_socialtoken_change", [["admin/socialaccount/socialtoken/%(object_id)s/change/", ["object_id"]]]], - ["admin:socialaccount_socialtoken_changelist", [["admin/socialaccount/socialtoken/", []]]], - ["admin:socialaccount_socialtoken_delete", [["admin/socialaccount/socialtoken/%(object_id)s/delete/", ["object_id"]]]], - ["admin:socialaccount_socialtoken_history", [["admin/socialaccount/socialtoken/%(object_id)s/history/", ["object_id"]]]], - ["admin:view_on_site", [["admin/r/%(content_type_id)s/%(object_id)s/", ["content_type_id", "object_id"]]]], - [ - "api:api-root", - [ - ["api/.%(format)s", ["format"]], - ["api/", []], - ], - ], - [ - "api:automation-detail", - [ - ["api/automation/%(pk)s.%(format)s", ["pk", "format"]], - ["api/automation/%(pk)s/", ["pk"]], - ], - ], - [ - "api:automation-list", - [ - ["api/automation.%(format)s", ["format"]], - ["api/automation/", []], - ], - ], - [ - "api:bookmarkletimport-detail", - [ - ["api/bookmarklet-import/%(pk)s.%(format)s", ["pk", "format"]], - ["api/bookmarklet-import/%(pk)s/", ["pk"]], - ], - ], - [ - "api:bookmarkletimport-list", - [ - ["api/bookmarklet-import.%(format)s", ["format"]], - ["api/bookmarklet-import/", []], - ], - ], - [ - "api:cooklog-detail", - [ - ["api/cook-log/%(pk)s.%(format)s", ["pk", "format"]], - ["api/cook-log/%(pk)s/", ["pk"]], - ], - ], - [ - "api:cooklog-list", - [ - ["api/cook-log.%(format)s", ["format"]], - ["api/cook-log/", []], - ], - ], - [ - "api:food-detail", - [ - ["api/food/%(pk)s.%(format)s", ["pk", "format"]], - ["api/food/%(pk)s/", ["pk"]], - ], - ], - [ - "api:food-list", - [ - ["api/food.%(format)s", ["format"]], - ["api/food/", []], - ], - ], - [ - "api:food-merge", - [ - ["api/food/%(pk)s/merge/%(target)s.%(format)s", ["pk", "target", "format"]], - ["api/food/%(pk)s/merge/%(target)s/", ["pk", "target"]], - ], - ], - [ - "api:food-move", - [ - ["api/food/%(pk)s/move/%(parent)s.%(format)s", ["pk", "parent", "format"]], - ["api/food/%(pk)s/move/%(parent)s/", ["pk", "parent"]], - ], - ], - [ - "api:food-shopping", - [ - ["api/food/%(pk)s/shopping.%(format)s", ["pk", "format"]], - ["api/food/%(pk)s/shopping/", ["pk"]], - ], - ], - [ - "api:foodinheritfield-detail", - [ - ["api/food-inherit-field/%(pk)s.%(format)s", ["pk", "format"]], - ["api/food-inherit-field/%(pk)s/", ["pk"]], - ], - ], - [ - "api:foodinheritfield-list", - [ - ["api/food-inherit-field.%(format)s", ["format"]], - ["api/food-inherit-field/", []], - ], - ], - [ - "api:importlog-detail", - [ - ["api/import-log/%(pk)s.%(format)s", ["pk", "format"]], - ["api/import-log/%(pk)s/", ["pk"]], - ], - ], - [ - "api:importlog-list", - [ - ["api/import-log.%(format)s", ["format"]], - ["api/import-log/", []], - ], - ], - [ - "api:ingredient-detail", - [ - ["api/ingredient/%(pk)s.%(format)s", ["pk", "format"]], - ["api/ingredient/%(pk)s/", ["pk"]], - ], - ], - [ - "api:ingredient-list", - [ - ["api/ingredient.%(format)s", ["format"]], - ["api/ingredient/", []], - ], - ], - [ - "api:keyword-detail", - [ - ["api/keyword/%(pk)s.%(format)s", ["pk", "format"]], - ["api/keyword/%(pk)s/", ["pk"]], - ], - ], - [ - "api:keyword-list", - [ - ["api/keyword.%(format)s", ["format"]], - ["api/keyword/", []], - ], - ], - [ - "api:keyword-merge", - [ - ["api/keyword/%(pk)s/merge/%(target)s.%(format)s", ["pk", "target", "format"]], - ["api/keyword/%(pk)s/merge/%(target)s/", ["pk", "target"]], - ], - ], - [ - "api:keyword-move", - [ - ["api/keyword/%(pk)s/move/%(parent)s.%(format)s", ["pk", "parent", "format"]], - ["api/keyword/%(pk)s/move/%(parent)s/", ["pk", "parent"]], - ], - ], - [ - "api:mealplan-detail", - [ - ["api/meal-plan/%(pk)s.%(format)s", ["pk", "format"]], - ["api/meal-plan/%(pk)s/", ["pk"]], - ], - ], - [ - "api:mealplan-list", - [ - ["api/meal-plan.%(format)s", ["format"]], - ["api/meal-plan/", []], - ], - ], - [ - "api:mealtype-detail", - [ - ["api/meal-type/%(pk)s.%(format)s", ["pk", "format"]], - ["api/meal-type/%(pk)s/", ["pk"]], - ], - ], - [ - "api:mealtype-list", - [ - ["api/meal-type.%(format)s", ["format"]], - ["api/meal-type/", []], - ], - ], - [ - "api:recipe-detail", - [ - ["api/recipe/%(pk)s.%(format)s", ["pk", "format"]], - ["api/recipe/%(pk)s/", ["pk"]], - ], - ], - [ - "api:recipe-image", - [ - ["api/recipe/%(pk)s/image.%(format)s", ["pk", "format"]], - ["api/recipe/%(pk)s/image/", ["pk"]], - ], - ], - [ - "api:recipe-list", - [ - ["api/recipe.%(format)s", ["format"]], - ["api/recipe/", []], - ], - ], - [ - "api:recipe-related", - [ - ["api/recipe/%(pk)s/related.%(format)s", ["pk", "format"]], - ["api/recipe/%(pk)s/related/", ["pk"]], - ], - ], - [ - "api:recipe-shopping", - [ - ["api/recipe/%(pk)s/shopping.%(format)s", ["pk", "format"]], - ["api/recipe/%(pk)s/shopping/", ["pk"]], - ], - ], - [ - "api:recipebook-detail", - [ - ["api/recipe-book/%(pk)s.%(format)s", ["pk", "format"]], - ["api/recipe-book/%(pk)s/", ["pk"]], - ], - ], - [ - "api:recipebook-list", - [ - ["api/recipe-book.%(format)s", ["format"]], - ["api/recipe-book/", []], - ], - ], - [ - "api:recipebookentry-detail", - [ - ["api/recipe-book-entry/%(pk)s.%(format)s", ["pk", "format"]], - ["api/recipe-book-entry/%(pk)s/", ["pk"]], - ], - ], - [ - "api:recipebookentry-list", - [ - ["api/recipe-book-entry.%(format)s", ["format"]], - ["api/recipe-book-entry/", []], - ], - ], - [ - "api:shoppinglist-detail", - [ - ["api/shopping-list/%(pk)s.%(format)s", ["pk", "format"]], - ["api/shopping-list/%(pk)s/", ["pk"]], - ], - ], - [ - "api:shoppinglist-list", - [ - ["api/shopping-list.%(format)s", ["format"]], - ["api/shopping-list/", []], - ], - ], - [ - "api:shoppinglistentry-detail", - [ - ["api/shopping-list-entry/%(pk)s.%(format)s", ["pk", "format"]], - ["api/shopping-list-entry/%(pk)s/", ["pk"]], - ], - ], - [ - "api:shoppinglistentry-list", - [ - ["api/shopping-list-entry.%(format)s", ["format"]], - ["api/shopping-list-entry/", []], - ], - ], - [ - "api:shoppinglistrecipe-detail", - [ - ["api/shopping-list-recipe/%(pk)s.%(format)s", ["pk", "format"]], - ["api/shopping-list-recipe/%(pk)s/", ["pk"]], - ], - ], - [ - "api:shoppinglistrecipe-list", - [ - ["api/shopping-list-recipe.%(format)s", ["format"]], - ["api/shopping-list-recipe/", []], - ], - ], - [ - "api:step-detail", - [ - ["api/step/%(pk)s.%(format)s", ["pk", "format"]], - ["api/step/%(pk)s/", ["pk"]], - ], - ], - [ - "api:step-list", - [ - ["api/step.%(format)s", ["format"]], - ["api/step/", []], - ], - ], - [ - "api:storage-detail", - [ - ["api/storage/%(pk)s.%(format)s", ["pk", "format"]], - ["api/storage/%(pk)s/", ["pk"]], - ], - ], - [ - "api:storage-list", - [ - ["api/storage.%(format)s", ["format"]], - ["api/storage/", []], - ], - ], - [ - "api:supermarket-detail", - [ - ["api/supermarket/%(pk)s.%(format)s", ["pk", "format"]], - ["api/supermarket/%(pk)s/", ["pk"]], - ], - ], - [ - "api:supermarket-list", - [ - ["api/supermarket.%(format)s", ["format"]], - ["api/supermarket/", []], - ], - ], - [ - "api:supermarketcategory-detail", - [ - ["api/supermarket-category/%(pk)s.%(format)s", ["pk", "format"]], - ["api/supermarket-category/%(pk)s/", ["pk"]], - ], - ], - [ - "api:supermarketcategory-list", - [ - ["api/supermarket-category.%(format)s", ["format"]], - ["api/supermarket-category/", []], - ], - ], - [ - "api:supermarketcategoryrelation-detail", - [ - ["api/supermarket-category-relation/%(pk)s.%(format)s", ["pk", "format"]], - ["api/supermarket-category-relation/%(pk)s/", ["pk"]], - ], - ], - [ - "api:supermarketcategoryrelation-list", - [ - ["api/supermarket-category-relation.%(format)s", ["format"]], - ["api/supermarket-category-relation/", []], - ], - ], - [ - "api:sync-detail", - [ - ["api/sync/%(pk)s.%(format)s", ["pk", "format"]], - ["api/sync/%(pk)s/", ["pk"]], - ], - ], - [ - "api:sync-list", - [ - ["api/sync.%(format)s", ["format"]], - ["api/sync/", []], - ], - ], - [ - "api:synclog-detail", - [ - ["api/sync-log/%(pk)s.%(format)s", ["pk", "format"]], - ["api/sync-log/%(pk)s/", ["pk"]], - ], - ], - [ - "api:synclog-list", - [ - ["api/sync-log.%(format)s", ["format"]], - ["api/sync-log/", []], - ], - ], - [ - "api:unit-detail", - [ - ["api/unit/%(pk)s.%(format)s", ["pk", "format"]], - ["api/unit/%(pk)s/", ["pk"]], - ], - ], - [ - "api:unit-list", - [ - ["api/unit.%(format)s", ["format"]], - ["api/unit/", []], - ], - ], - [ - "api:unit-merge", - [ - ["api/unit/%(pk)s/merge/%(target)s.%(format)s", ["pk", "target", "format"]], - ["api/unit/%(pk)s/merge/%(target)s/", ["pk", "target"]], - ], - ], - [ - "api:userfile-detail", - [ - ["api/user-file/%(pk)s.%(format)s", ["pk", "format"]], - ["api/user-file/%(pk)s/", ["pk"]], - ], - ], - [ - "api:userfile-list", - [ - ["api/user-file.%(format)s", ["format"]], - ["api/user-file/", []], - ], - ], - [ - "api:username-detail", - [ - ["api/user-name/%(pk)s.%(format)s", ["pk", "format"]], - ["api/user-name/%(pk)s/", ["pk"]], - ], - ], - [ - "api:username-list", - [ - ["api/user-name.%(format)s", ["format"]], - ["api/user-name/", []], - ], - ], - [ - "api:userpreference-detail", - [ - ["api/user-preference/%(pk)s.%(format)s", ["pk", "format"]], - ["api/user-preference/%(pk)s/", ["pk"]], - ], - ], - [ - "api:userpreference-list", - [ - ["api/user-preference.%(format)s", ["format"]], - ["api/user-preference/", []], - ], - ], - [ - "api:viewlog-detail", - [ - ["api/view-log/%(pk)s.%(format)s", ["pk", "format"]], - ["api/view-log/%(pk)s/", ["pk"]], - ], - ], - [ - "api:viewlog-list", - [ - ["api/view-log.%(format)s", ["format"]], - ["api/view-log/", []], - ], - ], - ["api_backup", [["api/backup/", []]]], - ["api_get_external_file_link", [["api/get_external_file_link/%(recipe_id)s/", ["recipe_id"]]]], - ["api_get_facets", [["api/get_facets/", []]]], - ["api_get_plan_ical", [["api/plan-ical/%(from_date)s/%(to_date)s/", ["from_date", "to_date"]]]], - ["api_get_recipe_file", [["api/get_recipe_file/%(recipe_id)s/", ["recipe_id"]]]], - ["api_ingredient_from_string", [["api/ingredient-from-string/", []]]], - ["api_log_cooking", [["api/log_cooking/%(recipe_id)s/", ["recipe_id"]]]], - ["api_recipe_from_source", [["api/recipe-from-source/", []]]], - ["api_share_link", [["api/share-link/%(pk)s", ["pk"]]]], - ["api_sync", [["api/sync_all/", []]]], - ["change_space_member", [["space/member/%(user_id)s/%(space_id)s/%(group)s", ["user_id", "space_id", "group"]]]], - ["dal_food", [["dal/food/", []]]], - ["dal_keyword", [["dal/keyword/", []]]], - ["dal_unit", [["dal/unit/", []]]], - ["data_batch_edit", [["data/batch/edit", []]]], - ["data_batch_import", [["data/batch/import", []]]], - ["data_import_url", [["data/import/url", []]]], - ["data_stats", [["data/statistics", []]]], - ["data_sync", [["data/sync", []]]], - ["data_sync_wait", [["data/sync/wait", []]]], - ["delete_comment", [["delete/comment/%(pk)s/", ["pk"]]]], - ["delete_invite_link", [["delete/invite-link/%(pk)s/", ["pk"]]]], - ["delete_meal_plan", [["delete/meal-plan/%(pk)s/", ["pk"]]]], - ["delete_recipe", [["delete/recipe/%(pk)s/", ["pk"]]]], - ["delete_recipe_book", [["delete/recipe-book/%(pk)s/", ["pk"]]]], - ["delete_recipe_book_entry", [["delete/recipe-book-entry/%(pk)s/", ["pk"]]]], - ["delete_recipe_import", [["delete/recipe-import/%(pk)s/", ["pk"]]]], - ["delete_recipe_source", [["delete/recipe-source/%(pk)s/", ["pk"]]]], - ["delete_storage", [["delete/storage/%(pk)s/", ["pk"]]]], - ["delete_sync", [["delete/sync/%(pk)s/", ["pk"]]]], - ["docs_api", [["docs/api/", []]]], - ["docs_markdown", [["docs/markdown/", []]]], - ["docs_search", [["docs/search/", []]]], - ["edit_comment", [["edit/comment/%(pk)s/", ["pk"]]]], - ["edit_convert_recipe", [["edit/recipe/convert/%(pk)s/", ["pk"]]]], - ["edit_external_recipe", [["edit/recipe/external/%(pk)s/", ["pk"]]]], - ["edit_internal_recipe", [["edit/recipe/internal/%(pk)s/", ["pk"]]]], - ["edit_meal_plan", [["edit/meal-plan/%(pk)s/", ["pk"]]]], - ["edit_recipe", [["edit/recipe/%(pk)s/", ["pk"]]]], - ["edit_storage", [["edit/storage/%(pk)s/", ["pk"]]]], - ["edit_sync", [["edit/sync/%(pk)s/", ["pk"]]]], - ["index", [["", []]]], - ["javascript-catalog", [["jsi18n/", []]]], - ["js_reverse", [["jsreverse.json", []]]], - ["list_automation", [["list/automation/", []]]], - ["list_food", [["list/food/", []]]], - ["list_invite_link", [["list/invite-link/", []]]], - ["list_keyword", [["list/keyword/", []]]], - ["list_recipe_import", [["list/recipe-import/", []]]], - ["list_shopping_list", [["list/shopping-list/", []]]], - ["list_step", [["list/step/", []]]], - ["list_storage", [["list/storage/", []]]], - ["list_supermarket", [["list/supermarket/", []]]], - ["list_supermarket_category", [["list/supermarket-category/", []]]], - ["list_sync_log", [["list/sync-log/", []]]], - ["list_unit", [["list/unit/", []]]], - ["list_user_file", [["list/user-file/", []]]], - ["new_invite_link", [["new/invite-link/", []]]], - ["new_meal_plan", [["new/meal-plan/", []]]], - ["new_recipe", [["new/recipe/", []]]], - ["new_recipe_import", [["new/recipe-import/%(import_id)s/", ["import_id"]]]], - ["new_share_link", [["new/share-link/%(pk)s/", ["pk"]]]], - ["new_storage", [["new/storage/", []]]], - ["openapi-schema", [["openapi/", []]]], - ["rest_framework:login", [["api-auth/login/", []]]], - ["rest_framework:logout", [["api-auth/logout/", []]]], - ["service_worker", [["service-worker.js", []]]], - ["set_language", [["i18n/setlang/", []]]], - ["socialaccount_connections", [["accounts/social/connections/", []]]], - ["socialaccount_login_cancelled", [["accounts/social/login/cancelled/", []]]], - ["socialaccount_login_error", [["accounts/social/login/error/", []]]], - ["socialaccount_signup", [["accounts/social/signup/", []]]], - ["telegram_hook", [["telegram/hook/%(token)s/", ["token"]]]], - ["telegram_remove", [["telegram/remove/%(pk)s", ["pk"]]]], - ["telegram_setup", [["telegram/setup/%(pk)s", ["pk"]]]], - ["view_books", [["books/", []]]], - ["view_export", [["export/", []]]], - ["view_history", [["history/", []]]], - ["view_import", [["import/", []]]], - ["view_import_response", [["import-response/%(pk)s/", ["pk"]]]], - ["view_invite", [["invite/%(token)s", ["token"]]]], - ["view_no_group", [["no-group", []]]], - ["view_no_perm", [["no-perm", []]]], - ["view_no_space", [["no-space", []]]], - ["view_offline", [["offline/", []]]], - ["view_plan", [["plan/", []]]], - ["view_plan_entry", [["plan/entry/%(pk)s", ["pk"]]]], - [ - "view_recipe", - [ - ["view/recipe/%(pk)s/%(share)s", ["pk", "share"]], - ["view/recipe/%(pk)s", ["pk"]], - ], - ], - ["view_report_share_abuse", [["abuse/%(token)s", ["token"]]]], - ["view_search", [["search/", []]]], - ["view_search_v2", [["search/v2/", []]]], - ["view_settings", [["settings/", []]]], - ["view_setup", [["setup/", []]]], - [ - "view_shopping", - [ - ["shopping/%(pk)s", ["pk"]], - ["shopping/", []], - ], - ], - ["view_shopping_latest", [["shopping/latest/", []]]], - ["view_shopping_new", [["shopping/new/", []]]], - ["view_signup", [["signup/%(token)s", ["token"]]]], - ["view_space", [["space/", []]]], - ["view_supermarket", [["supermarket/", []]]], - ["view_system", [["system/", []]]], - ["web_manifest", [["manifest.json", []]]], - ], - prefix: "/", - } - function factory(d) { - var url_patterns = d.urls - var url_prefix = d.prefix - var Urls = {} - var self_url_patterns = {} - var _get_url = function (url_pattern) { - return function () { - var _arguments, index, url, url_arg, url_args, _i, _len, _ref, _ref_list, match_ref, provided_keys, build_kwargs - _arguments = arguments - _ref_list = self_url_patterns[url_pattern] - if (arguments.length == 1 && typeof arguments[0] == "object") { - var provided_keys_list = Object.keys(arguments[0]) - provided_keys = {} - for (_i = 0; _i < provided_keys_list.length; _i++) provided_keys[provided_keys_list[_i]] = 1 - match_ref = function (ref) { - var _i - if (ref[1].length != provided_keys_list.length) return false - for (_i = 0; _i < ref[1].length && ref[1][_i] in provided_keys; _i++); - return _i == ref[1].length - } - build_kwargs = function (keys) { - return _arguments[0] - } - } else { - match_ref = function (ref) { - return ref[1].length == _arguments.length - } - build_kwargs = function (keys) { - var kwargs = {} - for (var i = 0; i < keys.length; i++) { - kwargs[keys[i]] = _arguments[i] - } - return kwargs - } - } - for (_i = 0; _i < _ref_list.length && !match_ref(_ref_list[_i]); _i++); - if (_i == _ref_list.length) return null - _ref = _ref_list[_i] - ;(url = _ref[0]), (url_args = build_kwargs(_ref[1])) - for (url_arg in url_args) { - var url_arg_value = url_args[url_arg] - if (url_arg_value === undefined || url_arg_value === null) { - url_arg_value = "" - } else { - url_arg_value = url_arg_value.toString() - } - url = url.replace("%(" + url_arg + ")s", url_arg_value) - } - return url_prefix + url - } - } - var name, pattern, url, _i, _len, _ref - for (_i = 0, _len = url_patterns.length; _i < _len; _i++) { - ;(_ref = url_patterns[_i]), (name = _ref[0]), (pattern = _ref[1]) - self_url_patterns[name] = pattern - url = _get_url(name) - Urls[ - name.replace(/[-_]+(.)/g, function (_m, p1) { - return p1.toUpperCase() - }) - ] = url - Urls[name.replace(/-/g, "_")] = url - Urls[name] = url - } - return Urls - } - return data ? factory(data) : factory -})() +this.Urls=(function(){"use strict";var data={"urls":[["account_change_password",[["accounts/password/change/",[]]]],["account_confirm_email",[["accounts/confirm-email/%(key)s/",["key"]]]],["account_email",[["accounts/email/",[]]]],["account_email_verification_sent",[["accounts/confirm-email/",[]]]],["account_inactive",[["accounts/inactive/",[]]]],["account_login",[["accounts/login/",[]]]],["account_logout",[["accounts/logout/",[]]]],["account_reset_password",[["accounts/password/reset/",[]]]],["account_reset_password_done",[["accounts/password/reset/done/",[]]]],["account_reset_password_from_key",[["accounts/password/reset/key/%(uidb36)s-%(key)s/",["uidb36","key"]]]],["account_reset_password_from_key_done",[["accounts/password/reset/key/done/",[]]]],["account_set_password",[["accounts/password/set/",[]]]],["account_signup",[["accounts/signup/",[]]]],["admin:account_emailaddress_add",[["admin/account/emailaddress/add/",[]]]],["admin:account_emailaddress_change",[["admin/account/emailaddress/%(object_id)s/change/",["object_id"]]]],["admin:account_emailaddress_changelist",[["admin/account/emailaddress/",[]]]],["admin:account_emailaddress_delete",[["admin/account/emailaddress/%(object_id)s/delete/",["object_id"]]]],["admin:account_emailaddress_history",[["admin/account/emailaddress/%(object_id)s/history/",["object_id"]]]],["admin:app_list",[["admin/%(app_label)s/",["app_label"]]]],["admin:auth_user_add",[["admin/auth/user/add/",[]]]],["admin:auth_user_change",[["admin/auth/user/%(object_id)s/change/",["object_id"]]]],["admin:auth_user_changelist",[["admin/auth/user/",[]]]],["admin:auth_user_delete",[["admin/auth/user/%(object_id)s/delete/",["object_id"]]]],["admin:auth_user_history",[["admin/auth/user/%(object_id)s/history/",["object_id"]]]],["admin:auth_user_password_change",[["admin/auth/user/%(id)s/password/",["id"]]]],["admin:authtoken_tokenproxy_add",[["admin/authtoken/tokenproxy/add/",[]]]],["admin:authtoken_tokenproxy_change",[["admin/authtoken/tokenproxy/%(object_id)s/change/",["object_id"]]]],["admin:authtoken_tokenproxy_changelist",[["admin/authtoken/tokenproxy/",[]]]],["admin:authtoken_tokenproxy_delete",[["admin/authtoken/tokenproxy/%(object_id)s/delete/",["object_id"]]]],["admin:authtoken_tokenproxy_history",[["admin/authtoken/tokenproxy/%(object_id)s/history/",["object_id"]]]],["admin:autocomplete",[["admin/autocomplete/",[]]]],["admin:cookbook_bookmarkletimport_add",[["admin/cookbook/bookmarkletimport/add/",[]]]],["admin:cookbook_bookmarkletimport_change",[["admin/cookbook/bookmarkletimport/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_bookmarkletimport_changelist",[["admin/cookbook/bookmarkletimport/",[]]]],["admin:cookbook_bookmarkletimport_delete",[["admin/cookbook/bookmarkletimport/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_bookmarkletimport_history",[["admin/cookbook/bookmarkletimport/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_comment_add",[["admin/cookbook/comment/add/",[]]]],["admin:cookbook_comment_change",[["admin/cookbook/comment/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_comment_changelist",[["admin/cookbook/comment/",[]]]],["admin:cookbook_comment_delete",[["admin/cookbook/comment/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_comment_history",[["admin/cookbook/comment/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_cooklog_add",[["admin/cookbook/cooklog/add/",[]]]],["admin:cookbook_cooklog_change",[["admin/cookbook/cooklog/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_cooklog_changelist",[["admin/cookbook/cooklog/",[]]]],["admin:cookbook_cooklog_delete",[["admin/cookbook/cooklog/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_cooklog_history",[["admin/cookbook/cooklog/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_food_add",[["admin/cookbook/food/add/",[]]]],["admin:cookbook_food_change",[["admin/cookbook/food/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_food_changelist",[["admin/cookbook/food/",[]]]],["admin:cookbook_food_delete",[["admin/cookbook/food/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_food_history",[["admin/cookbook/food/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_importlog_add",[["admin/cookbook/importlog/add/",[]]]],["admin:cookbook_importlog_change",[["admin/cookbook/importlog/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_importlog_changelist",[["admin/cookbook/importlog/",[]]]],["admin:cookbook_importlog_delete",[["admin/cookbook/importlog/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_importlog_history",[["admin/cookbook/importlog/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_ingredient_add",[["admin/cookbook/ingredient/add/",[]]]],["admin:cookbook_ingredient_change",[["admin/cookbook/ingredient/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_ingredient_changelist",[["admin/cookbook/ingredient/",[]]]],["admin:cookbook_ingredient_delete",[["admin/cookbook/ingredient/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_ingredient_history",[["admin/cookbook/ingredient/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_invitelink_add",[["admin/cookbook/invitelink/add/",[]]]],["admin:cookbook_invitelink_change",[["admin/cookbook/invitelink/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_invitelink_changelist",[["admin/cookbook/invitelink/",[]]]],["admin:cookbook_invitelink_delete",[["admin/cookbook/invitelink/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_invitelink_history",[["admin/cookbook/invitelink/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_keyword_add",[["admin/cookbook/keyword/add/",[]]]],["admin:cookbook_keyword_change",[["admin/cookbook/keyword/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_keyword_changelist",[["admin/cookbook/keyword/",[]]]],["admin:cookbook_keyword_delete",[["admin/cookbook/keyword/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_keyword_history",[["admin/cookbook/keyword/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_mealplan_add",[["admin/cookbook/mealplan/add/",[]]]],["admin:cookbook_mealplan_change",[["admin/cookbook/mealplan/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_mealplan_changelist",[["admin/cookbook/mealplan/",[]]]],["admin:cookbook_mealplan_delete",[["admin/cookbook/mealplan/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_mealplan_history",[["admin/cookbook/mealplan/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_mealtype_add",[["admin/cookbook/mealtype/add/",[]]]],["admin:cookbook_mealtype_change",[["admin/cookbook/mealtype/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_mealtype_changelist",[["admin/cookbook/mealtype/",[]]]],["admin:cookbook_mealtype_delete",[["admin/cookbook/mealtype/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_mealtype_history",[["admin/cookbook/mealtype/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_nutritioninformation_add",[["admin/cookbook/nutritioninformation/add/",[]]]],["admin:cookbook_nutritioninformation_change",[["admin/cookbook/nutritioninformation/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_nutritioninformation_changelist",[["admin/cookbook/nutritioninformation/",[]]]],["admin:cookbook_nutritioninformation_delete",[["admin/cookbook/nutritioninformation/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_nutritioninformation_history",[["admin/cookbook/nutritioninformation/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_recipe_add",[["admin/cookbook/recipe/add/",[]]]],["admin:cookbook_recipe_change",[["admin/cookbook/recipe/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_recipe_changelist",[["admin/cookbook/recipe/",[]]]],["admin:cookbook_recipe_delete",[["admin/cookbook/recipe/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_recipe_history",[["admin/cookbook/recipe/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_recipebook_add",[["admin/cookbook/recipebook/add/",[]]]],["admin:cookbook_recipebook_change",[["admin/cookbook/recipebook/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_recipebook_changelist",[["admin/cookbook/recipebook/",[]]]],["admin:cookbook_recipebook_delete",[["admin/cookbook/recipebook/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_recipebook_history",[["admin/cookbook/recipebook/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_recipebookentry_add",[["admin/cookbook/recipebookentry/add/",[]]]],["admin:cookbook_recipebookentry_change",[["admin/cookbook/recipebookentry/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_recipebookentry_changelist",[["admin/cookbook/recipebookentry/",[]]]],["admin:cookbook_recipebookentry_delete",[["admin/cookbook/recipebookentry/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_recipebookentry_history",[["admin/cookbook/recipebookentry/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_recipeimport_add",[["admin/cookbook/recipeimport/add/",[]]]],["admin:cookbook_recipeimport_change",[["admin/cookbook/recipeimport/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_recipeimport_changelist",[["admin/cookbook/recipeimport/",[]]]],["admin:cookbook_recipeimport_delete",[["admin/cookbook/recipeimport/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_recipeimport_history",[["admin/cookbook/recipeimport/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_searchpreference_add",[["admin/cookbook/searchpreference/add/",[]]]],["admin:cookbook_searchpreference_change",[["admin/cookbook/searchpreference/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_searchpreference_changelist",[["admin/cookbook/searchpreference/",[]]]],["admin:cookbook_searchpreference_delete",[["admin/cookbook/searchpreference/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_searchpreference_history",[["admin/cookbook/searchpreference/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_sharelink_add",[["admin/cookbook/sharelink/add/",[]]]],["admin:cookbook_sharelink_change",[["admin/cookbook/sharelink/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_sharelink_changelist",[["admin/cookbook/sharelink/",[]]]],["admin:cookbook_sharelink_delete",[["admin/cookbook/sharelink/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_sharelink_history",[["admin/cookbook/sharelink/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_shoppinglist_add",[["admin/cookbook/shoppinglist/add/",[]]]],["admin:cookbook_shoppinglist_change",[["admin/cookbook/shoppinglist/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_shoppinglist_changelist",[["admin/cookbook/shoppinglist/",[]]]],["admin:cookbook_shoppinglist_delete",[["admin/cookbook/shoppinglist/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_shoppinglist_history",[["admin/cookbook/shoppinglist/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_shoppinglistentry_add",[["admin/cookbook/shoppinglistentry/add/",[]]]],["admin:cookbook_shoppinglistentry_change",[["admin/cookbook/shoppinglistentry/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_shoppinglistentry_changelist",[["admin/cookbook/shoppinglistentry/",[]]]],["admin:cookbook_shoppinglistentry_delete",[["admin/cookbook/shoppinglistentry/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_shoppinglistentry_history",[["admin/cookbook/shoppinglistentry/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_shoppinglistrecipe_add",[["admin/cookbook/shoppinglistrecipe/add/",[]]]],["admin:cookbook_shoppinglistrecipe_change",[["admin/cookbook/shoppinglistrecipe/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_shoppinglistrecipe_changelist",[["admin/cookbook/shoppinglistrecipe/",[]]]],["admin:cookbook_shoppinglistrecipe_delete",[["admin/cookbook/shoppinglistrecipe/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_shoppinglistrecipe_history",[["admin/cookbook/shoppinglistrecipe/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_space_add",[["admin/cookbook/space/add/",[]]]],["admin:cookbook_space_change",[["admin/cookbook/space/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_space_changelist",[["admin/cookbook/space/",[]]]],["admin:cookbook_space_delete",[["admin/cookbook/space/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_space_history",[["admin/cookbook/space/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_step_add",[["admin/cookbook/step/add/",[]]]],["admin:cookbook_step_change",[["admin/cookbook/step/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_step_changelist",[["admin/cookbook/step/",[]]]],["admin:cookbook_step_delete",[["admin/cookbook/step/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_step_history",[["admin/cookbook/step/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_storage_add",[["admin/cookbook/storage/add/",[]]]],["admin:cookbook_storage_change",[["admin/cookbook/storage/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_storage_changelist",[["admin/cookbook/storage/",[]]]],["admin:cookbook_storage_delete",[["admin/cookbook/storage/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_storage_history",[["admin/cookbook/storage/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_supermarket_add",[["admin/cookbook/supermarket/add/",[]]]],["admin:cookbook_supermarket_change",[["admin/cookbook/supermarket/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_supermarket_changelist",[["admin/cookbook/supermarket/",[]]]],["admin:cookbook_supermarket_delete",[["admin/cookbook/supermarket/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_supermarket_history",[["admin/cookbook/supermarket/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_supermarketcategory_add",[["admin/cookbook/supermarketcategory/add/",[]]]],["admin:cookbook_supermarketcategory_change",[["admin/cookbook/supermarketcategory/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_supermarketcategory_changelist",[["admin/cookbook/supermarketcategory/",[]]]],["admin:cookbook_supermarketcategory_delete",[["admin/cookbook/supermarketcategory/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_supermarketcategory_history",[["admin/cookbook/supermarketcategory/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_sync_add",[["admin/cookbook/sync/add/",[]]]],["admin:cookbook_sync_change",[["admin/cookbook/sync/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_sync_changelist",[["admin/cookbook/sync/",[]]]],["admin:cookbook_sync_delete",[["admin/cookbook/sync/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_sync_history",[["admin/cookbook/sync/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_synclog_add",[["admin/cookbook/synclog/add/",[]]]],["admin:cookbook_synclog_change",[["admin/cookbook/synclog/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_synclog_changelist",[["admin/cookbook/synclog/",[]]]],["admin:cookbook_synclog_delete",[["admin/cookbook/synclog/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_synclog_history",[["admin/cookbook/synclog/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_telegrambot_add",[["admin/cookbook/telegrambot/add/",[]]]],["admin:cookbook_telegrambot_change",[["admin/cookbook/telegrambot/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_telegrambot_changelist",[["admin/cookbook/telegrambot/",[]]]],["admin:cookbook_telegrambot_delete",[["admin/cookbook/telegrambot/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_telegrambot_history",[["admin/cookbook/telegrambot/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_unit_add",[["admin/cookbook/unit/add/",[]]]],["admin:cookbook_unit_change",[["admin/cookbook/unit/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_unit_changelist",[["admin/cookbook/unit/",[]]]],["admin:cookbook_unit_delete",[["admin/cookbook/unit/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_unit_history",[["admin/cookbook/unit/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_userfile_add",[["admin/cookbook/userfile/add/",[]]]],["admin:cookbook_userfile_change",[["admin/cookbook/userfile/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_userfile_changelist",[["admin/cookbook/userfile/",[]]]],["admin:cookbook_userfile_delete",[["admin/cookbook/userfile/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_userfile_history",[["admin/cookbook/userfile/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_userpreference_add",[["admin/cookbook/userpreference/add/",[]]]],["admin:cookbook_userpreference_change",[["admin/cookbook/userpreference/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_userpreference_changelist",[["admin/cookbook/userpreference/",[]]]],["admin:cookbook_userpreference_delete",[["admin/cookbook/userpreference/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_userpreference_history",[["admin/cookbook/userpreference/%(object_id)s/history/",["object_id"]]]],["admin:cookbook_viewlog_add",[["admin/cookbook/viewlog/add/",[]]]],["admin:cookbook_viewlog_change",[["admin/cookbook/viewlog/%(object_id)s/change/",["object_id"]]]],["admin:cookbook_viewlog_changelist",[["admin/cookbook/viewlog/",[]]]],["admin:cookbook_viewlog_delete",[["admin/cookbook/viewlog/%(object_id)s/delete/",["object_id"]]]],["admin:cookbook_viewlog_history",[["admin/cookbook/viewlog/%(object_id)s/history/",["object_id"]]]],["admin:index",[["admin/",[]]]],["admin:javascript-catalog",[["admin/cookbook/food/jsi18n/",[]],["admin/cookbook/keyword/jsi18n/",[]]]],["admin:jsi18n",[["admin/jsi18n/",[]]]],["admin:login",[["admin/login/",[]]]],["admin:logout",[["admin/logout/",[]]]],["admin:password_change",[["admin/password_change/",[]]]],["admin:password_change_done",[["admin/password_change/done/",[]]]],["admin:sites_site_add",[["admin/sites/site/add/",[]]]],["admin:sites_site_change",[["admin/sites/site/%(object_id)s/change/",["object_id"]]]],["admin:sites_site_changelist",[["admin/sites/site/",[]]]],["admin:sites_site_delete",[["admin/sites/site/%(object_id)s/delete/",["object_id"]]]],["admin:sites_site_history",[["admin/sites/site/%(object_id)s/history/",["object_id"]]]],["admin:socialaccount_socialaccount_add",[["admin/socialaccount/socialaccount/add/",[]]]],["admin:socialaccount_socialaccount_change",[["admin/socialaccount/socialaccount/%(object_id)s/change/",["object_id"]]]],["admin:socialaccount_socialaccount_changelist",[["admin/socialaccount/socialaccount/",[]]]],["admin:socialaccount_socialaccount_delete",[["admin/socialaccount/socialaccount/%(object_id)s/delete/",["object_id"]]]],["admin:socialaccount_socialaccount_history",[["admin/socialaccount/socialaccount/%(object_id)s/history/",["object_id"]]]],["admin:socialaccount_socialapp_add",[["admin/socialaccount/socialapp/add/",[]]]],["admin:socialaccount_socialapp_change",[["admin/socialaccount/socialapp/%(object_id)s/change/",["object_id"]]]],["admin:socialaccount_socialapp_changelist",[["admin/socialaccount/socialapp/",[]]]],["admin:socialaccount_socialapp_delete",[["admin/socialaccount/socialapp/%(object_id)s/delete/",["object_id"]]]],["admin:socialaccount_socialapp_history",[["admin/socialaccount/socialapp/%(object_id)s/history/",["object_id"]]]],["admin:socialaccount_socialtoken_add",[["admin/socialaccount/socialtoken/add/",[]]]],["admin:socialaccount_socialtoken_change",[["admin/socialaccount/socialtoken/%(object_id)s/change/",["object_id"]]]],["admin:socialaccount_socialtoken_changelist",[["admin/socialaccount/socialtoken/",[]]]],["admin:socialaccount_socialtoken_delete",[["admin/socialaccount/socialtoken/%(object_id)s/delete/",["object_id"]]]],["admin:socialaccount_socialtoken_history",[["admin/socialaccount/socialtoken/%(object_id)s/history/",["object_id"]]]],["admin:view_on_site",[["admin/r/%(content_type_id)s/%(object_id)s/",["content_type_id","object_id"]]]],["api:api-root",[["api/.%(format)s",["format"]],["api/",[]]]],["api:automation-detail",[["api/automation/%(pk)s.%(format)s",["pk","format"]],["api/automation/%(pk)s/",["pk"]]]],["api:automation-list",[["api/automation.%(format)s",["format"]],["api/automation/",[]]]],["api:bookmarkletimport-detail",[["api/bookmarklet-import/%(pk)s.%(format)s",["pk","format"]],["api/bookmarklet-import/%(pk)s/",["pk"]]]],["api:bookmarkletimport-list",[["api/bookmarklet-import.%(format)s",["format"]],["api/bookmarklet-import/",[]]]],["api:cooklog-detail",[["api/cook-log/%(pk)s.%(format)s",["pk","format"]],["api/cook-log/%(pk)s/",["pk"]]]],["api:cooklog-list",[["api/cook-log.%(format)s",["format"]],["api/cook-log/",[]]]],["api:food-detail",[["api/food/%(pk)s.%(format)s",["pk","format"]],["api/food/%(pk)s/",["pk"]]]],["api:food-list",[["api/food.%(format)s",["format"]],["api/food/",[]]]],["api:food-merge",[["api/food/%(pk)s/merge/%(target)s.%(format)s",["pk","target","format"]],["api/food/%(pk)s/merge/%(target)s/",["pk","target"]]]],["api:food-move",[["api/food/%(pk)s/move/%(parent)s.%(format)s",["pk","parent","format"]],["api/food/%(pk)s/move/%(parent)s/",["pk","parent"]]]],["api:food-shopping",[["api/food/%(pk)s/shopping.%(format)s",["pk","format"]],["api/food/%(pk)s/shopping/",["pk"]]]],["api:foodinheritfield-detail",[["api/food-inherit-field/%(pk)s.%(format)s",["pk","format"]],["api/food-inherit-field/%(pk)s/",["pk"]]]],["api:foodinheritfield-list",[["api/food-inherit-field.%(format)s",["format"]],["api/food-inherit-field/",[]]]],["api:importlog-detail",[["api/import-log/%(pk)s.%(format)s",["pk","format"]],["api/import-log/%(pk)s/",["pk"]]]],["api:importlog-list",[["api/import-log.%(format)s",["format"]],["api/import-log/",[]]]],["api:ingredient-detail",[["api/ingredient/%(pk)s.%(format)s",["pk","format"]],["api/ingredient/%(pk)s/",["pk"]]]],["api:ingredient-list",[["api/ingredient.%(format)s",["format"]],["api/ingredient/",[]]]],["api:keyword-detail",[["api/keyword/%(pk)s.%(format)s",["pk","format"]],["api/keyword/%(pk)s/",["pk"]]]],["api:keyword-list",[["api/keyword.%(format)s",["format"]],["api/keyword/",[]]]],["api:keyword-merge",[["api/keyword/%(pk)s/merge/%(target)s.%(format)s",["pk","target","format"]],["api/keyword/%(pk)s/merge/%(target)s/",["pk","target"]]]],["api:keyword-move",[["api/keyword/%(pk)s/move/%(parent)s.%(format)s",["pk","parent","format"]],["api/keyword/%(pk)s/move/%(parent)s/",["pk","parent"]]]],["api:mealplan-detail",[["api/meal-plan/%(pk)s.%(format)s",["pk","format"]],["api/meal-plan/%(pk)s/",["pk"]]]],["api:mealplan-list",[["api/meal-plan.%(format)s",["format"]],["api/meal-plan/",[]]]],["api:mealtype-detail",[["api/meal-type/%(pk)s.%(format)s",["pk","format"]],["api/meal-type/%(pk)s/",["pk"]]]],["api:mealtype-list",[["api/meal-type.%(format)s",["format"]],["api/meal-type/",[]]]],["api:recipe-detail",[["api/recipe/%(pk)s.%(format)s",["pk","format"]],["api/recipe/%(pk)s/",["pk"]]]],["api:recipe-image",[["api/recipe/%(pk)s/image.%(format)s",["pk","format"]],["api/recipe/%(pk)s/image/",["pk"]]]],["api:recipe-list",[["api/recipe.%(format)s",["format"]],["api/recipe/",[]]]],["api:recipe-related",[["api/recipe/%(pk)s/related.%(format)s",["pk","format"]],["api/recipe/%(pk)s/related/",["pk"]]]],["api:recipe-shopping",[["api/recipe/%(pk)s/shopping.%(format)s",["pk","format"]],["api/recipe/%(pk)s/shopping/",["pk"]]]],["api:recipebook-detail",[["api/recipe-book/%(pk)s.%(format)s",["pk","format"]],["api/recipe-book/%(pk)s/",["pk"]]]],["api:recipebook-list",[["api/recipe-book.%(format)s",["format"]],["api/recipe-book/",[]]]],["api:recipebookentry-detail",[["api/recipe-book-entry/%(pk)s.%(format)s",["pk","format"]],["api/recipe-book-entry/%(pk)s/",["pk"]]]],["api:recipebookentry-list",[["api/recipe-book-entry.%(format)s",["format"]],["api/recipe-book-entry/",[]]]],["api:shoppinglist-detail",[["api/shopping-list/%(pk)s.%(format)s",["pk","format"]],["api/shopping-list/%(pk)s/",["pk"]]]],["api:shoppinglist-list",[["api/shopping-list.%(format)s",["format"]],["api/shopping-list/",[]]]],["api:shoppinglistentry-detail",[["api/shopping-list-entry/%(pk)s.%(format)s",["pk","format"]],["api/shopping-list-entry/%(pk)s/",["pk"]]]],["api:shoppinglistentry-list",[["api/shopping-list-entry.%(format)s",["format"]],["api/shopping-list-entry/",[]]]],["api:shoppinglistrecipe-detail",[["api/shopping-list-recipe/%(pk)s.%(format)s",["pk","format"]],["api/shopping-list-recipe/%(pk)s/",["pk"]]]],["api:shoppinglistrecipe-list",[["api/shopping-list-recipe.%(format)s",["format"]],["api/shopping-list-recipe/",[]]]],["api:step-detail",[["api/step/%(pk)s.%(format)s",["pk","format"]],["api/step/%(pk)s/",["pk"]]]],["api:step-list",[["api/step.%(format)s",["format"]],["api/step/",[]]]],["api:storage-detail",[["api/storage/%(pk)s.%(format)s",["pk","format"]],["api/storage/%(pk)s/",["pk"]]]],["api:storage-list",[["api/storage.%(format)s",["format"]],["api/storage/",[]]]],["api:supermarket-detail",[["api/supermarket/%(pk)s.%(format)s",["pk","format"]],["api/supermarket/%(pk)s/",["pk"]]]],["api:supermarket-list",[["api/supermarket.%(format)s",["format"]],["api/supermarket/",[]]]],["api:supermarketcategory-detail",[["api/supermarket-category/%(pk)s.%(format)s",["pk","format"]],["api/supermarket-category/%(pk)s/",["pk"]]]],["api:supermarketcategory-list",[["api/supermarket-category.%(format)s",["format"]],["api/supermarket-category/",[]]]],["api:supermarketcategoryrelation-detail",[["api/supermarket-category-relation/%(pk)s.%(format)s",["pk","format"]],["api/supermarket-category-relation/%(pk)s/",["pk"]]]],["api:supermarketcategoryrelation-list",[["api/supermarket-category-relation.%(format)s",["format"]],["api/supermarket-category-relation/",[]]]],["api:sync-detail",[["api/sync/%(pk)s.%(format)s",["pk","format"]],["api/sync/%(pk)s/",["pk"]]]],["api:sync-list",[["api/sync.%(format)s",["format"]],["api/sync/",[]]]],["api:synclog-detail",[["api/sync-log/%(pk)s.%(format)s",["pk","format"]],["api/sync-log/%(pk)s/",["pk"]]]],["api:synclog-list",[["api/sync-log.%(format)s",["format"]],["api/sync-log/",[]]]],["api:unit-detail",[["api/unit/%(pk)s.%(format)s",["pk","format"]],["api/unit/%(pk)s/",["pk"]]]],["api:unit-list",[["api/unit.%(format)s",["format"]],["api/unit/",[]]]],["api:unit-merge",[["api/unit/%(pk)s/merge/%(target)s.%(format)s",["pk","target","format"]],["api/unit/%(pk)s/merge/%(target)s/",["pk","target"]]]],["api:userfile-detail",[["api/user-file/%(pk)s.%(format)s",["pk","format"]],["api/user-file/%(pk)s/",["pk"]]]],["api:userfile-list",[["api/user-file.%(format)s",["format"]],["api/user-file/",[]]]],["api:username-detail",[["api/user-name/%(pk)s.%(format)s",["pk","format"]],["api/user-name/%(pk)s/",["pk"]]]],["api:username-list",[["api/user-name.%(format)s",["format"]],["api/user-name/",[]]]],["api:userpreference-detail",[["api/user-preference/%(pk)s.%(format)s",["pk","format"]],["api/user-preference/%(pk)s/",["pk"]]]],["api:userpreference-list",[["api/user-preference.%(format)s",["format"]],["api/user-preference/",[]]]],["api:viewlog-detail",[["api/view-log/%(pk)s.%(format)s",["pk","format"]],["api/view-log/%(pk)s/",["pk"]]]],["api:viewlog-list",[["api/view-log.%(format)s",["format"]],["api/view-log/",[]]]],["api_backup",[["api/backup/",[]]]],["api_get_external_file_link",[["api/get_external_file_link/%(recipe_id)s/",["recipe_id"]]]],["api_get_facets",[["api/get_facets/",[]]]],["api_get_plan_ical",[["api/plan-ical/%(from_date)s/%(to_date)s/",["from_date","to_date"]]]],["api_get_recipe_file",[["api/get_recipe_file/%(recipe_id)s/",["recipe_id"]]]],["api_ingredient_from_string",[["api/ingredient-from-string/",[]]]],["api_log_cooking",[["api/log_cooking/%(recipe_id)s/",["recipe_id"]]]],["api_recipe_from_source",[["api/recipe-from-source/",[]]]],["api_share_link",[["api/share-link/%(pk)s",["pk"]]]],["api_sync",[["api/sync_all/",[]]]],["change_space_member",[["space/member/%(user_id)s/%(space_id)s/%(group)s",["user_id","space_id","group"]]]],["dal_food",[["dal/food/",[]]]],["dal_keyword",[["dal/keyword/",[]]]],["dal_unit",[["dal/unit/",[]]]],["data_batch_edit",[["data/batch/edit",[]]]],["data_batch_import",[["data/batch/import",[]]]],["data_import_url",[["data/import/url",[]]]],["data_stats",[["data/statistics",[]]]],["data_sync",[["data/sync",[]]]],["data_sync_wait",[["data/sync/wait",[]]]],["delete_comment",[["delete/comment/%(pk)s/",["pk"]]]],["delete_invite_link",[["delete/invite-link/%(pk)s/",["pk"]]]],["delete_meal_plan",[["delete/meal-plan/%(pk)s/",["pk"]]]],["delete_recipe",[["delete/recipe/%(pk)s/",["pk"]]]],["delete_recipe_book",[["delete/recipe-book/%(pk)s/",["pk"]]]],["delete_recipe_book_entry",[["delete/recipe-book-entry/%(pk)s/",["pk"]]]],["delete_recipe_import",[["delete/recipe-import/%(pk)s/",["pk"]]]],["delete_recipe_source",[["delete/recipe-source/%(pk)s/",["pk"]]]],["delete_storage",[["delete/storage/%(pk)s/",["pk"]]]],["delete_sync",[["delete/sync/%(pk)s/",["pk"]]]],["docs_api",[["docs/api/",[]]]],["docs_markdown",[["docs/markdown/",[]]]],["docs_search",[["docs/search/",[]]]],["edit_comment",[["edit/comment/%(pk)s/",["pk"]]]],["edit_convert_recipe",[["edit/recipe/convert/%(pk)s/",["pk"]]]],["edit_external_recipe",[["edit/recipe/external/%(pk)s/",["pk"]]]],["edit_internal_recipe",[["edit/recipe/internal/%(pk)s/",["pk"]]]],["edit_meal_plan",[["edit/meal-plan/%(pk)s/",["pk"]]]],["edit_recipe",[["edit/recipe/%(pk)s/",["pk"]]]],["edit_storage",[["edit/storage/%(pk)s/",["pk"]]]],["edit_sync",[["edit/sync/%(pk)s/",["pk"]]]],["index",[["",[]]]],["javascript-catalog",[["jsi18n/",[]]]],["js_reverse",[["jsreverse.json",[]]]],["list_automation",[["list/automation/",[]]]],["list_food",[["list/food/",[]]]],["list_invite_link",[["list/invite-link/",[]]]],["list_keyword",[["list/keyword/",[]]]],["list_recipe_import",[["list/recipe-import/",[]]]],["list_shopping_list",[["list/shopping-list/",[]]]],["list_step",[["list/step/",[]]]],["list_storage",[["list/storage/",[]]]],["list_supermarket",[["list/supermarket/",[]]]],["list_supermarket_category",[["list/supermarket-category/",[]]]],["list_sync_log",[["list/sync-log/",[]]]],["list_unit",[["list/unit/",[]]]],["list_user_file",[["list/user-file/",[]]]],["new_invite_link",[["new/invite-link/",[]]]],["new_meal_plan",[["new/meal-plan/",[]]]],["new_recipe",[["new/recipe/",[]]]],["new_recipe_import",[["new/recipe-import/%(import_id)s/",["import_id"]]]],["new_share_link",[["new/share-link/%(pk)s/",["pk"]]]],["new_storage",[["new/storage/",[]]]],["openapi-schema",[["openapi/",[]]]],["rest_framework:login",[["api-auth/login/",[]]]],["rest_framework:logout",[["api-auth/logout/",[]]]],["service_worker",[["service-worker.js",[]]]],["set_language",[["i18n/setlang/",[]]]],["socialaccount_connections",[["accounts/social/connections/",[]]]],["socialaccount_login_cancelled",[["accounts/social/login/cancelled/",[]]]],["socialaccount_login_error",[["accounts/social/login/error/",[]]]],["socialaccount_signup",[["accounts/social/signup/",[]]]],["telegram_hook",[["telegram/hook/%(token)s/",["token"]]]],["telegram_remove",[["telegram/remove/%(pk)s",["pk"]]]],["telegram_setup",[["telegram/setup/%(pk)s",["pk"]]]],["view_books",[["books/",[]]]],["view_export",[["export/",[]]]],["view_history",[["history/",[]]]],["view_import",[["import/",[]]]],["view_import_response",[["import-response/%(pk)s/",["pk"]]]],["view_invite",[["invite/%(token)s",["token"]]]],["view_no_group",[["no-group",[]]]],["view_no_perm",[["no-perm",[]]]],["view_no_space",[["no-space",[]]]],["view_offline",[["offline/",[]]]],["view_plan",[["plan/",[]]]],["view_plan_entry",[["plan/entry/%(pk)s",["pk"]]]],["view_recipe",[["view/recipe/%(pk)s/%(share)s",["pk","share"]],["view/recipe/%(pk)s",["pk"]]]],["view_report_share_abuse",[["abuse/%(token)s",["token"]]]],["view_search",[["search/",[]]]],["view_search_v2",[["search/v2/",[]]]],["view_settings",[["settings/",[]]]],["view_setup",[["setup/",[]]]],["view_shopping",[["shopping/%(pk)s",["pk"]],["shopping/",[]]]],["view_shopping_latest",[["shopping/latest/",[]]]],["view_shopping_new",[["shopping/new/",[]]]],["view_signup",[["signup/%(token)s",["token"]]]],["view_space",[["space/",[]]]],["view_supermarket",[["supermarket/",[]]]],["view_system",[["system/",[]]]],["web_manifest",[["manifest.json",[]]]]],"prefix":"/"};function factory(d){var url_patterns=d.urls;var url_prefix=d.prefix;var Urls={};var self_url_patterns={};var _get_url=function(url_pattern){return function(){var _arguments,index,url,url_arg,url_args,_i,_len,_ref,_ref_list,match_ref,provided_keys,build_kwargs;_arguments=arguments;_ref_list=self_url_patterns[url_pattern];if(arguments.length==1&&typeof(arguments[0])=="object"){var provided_keys_list=Object.keys(arguments[0]);provided_keys={};for(_i=0;_i {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} - + {% endif %} diff --git a/cookbook/templates/shopping_list.html b/cookbook/templates/shopping_list.html index 837c0eac8..dcc4ba531 100644 --- a/cookbook/templates/shopping_list.html +++ b/cookbook/templates/shopping_list.html @@ -834,7 +834,7 @@ this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => { for (let s of response.data.steps) { for (let i of s.ingredients) { - if (!i.is_header && i.food !== null && i.food.ignore_shopping === false) { + if (!i.is_header && i.food !== null && i.food.food_onhand === false) { this.shopping_list.entries.push({ 'list_recipe': slr.id, 'food': i.food, diff --git a/cookbook/templates/url_import.html b/cookbook/templates/url_import.html index 4850cb820..4a42a594b 100644 --- a/cookbook/templates/url_import.html +++ b/cookbook/templates/url_import.html @@ -76,6 +76,7 @@ + diff --git a/cookbook/tests/api/test_api_food.py b/cookbook/tests/api/test_api_food.py index 2b1bd3a4c..cde725484 100644 --- a/cookbook/tests/api/test_api_food.py +++ b/cookbook/tests/api/test_api_food.py @@ -57,7 +57,19 @@ def obj_tree_1(request, space_1): except AttributeError: params = {} objs = [] + inherit = params.pop('inherit', False) objs.extend(FoodFactory.create_batch(3, space=space_1, **params)) + + # set all foods to inherit everything + if inherit: + inherit = Food.inheritable_fields + Through = Food.objects.filter(space=space_1).first().inherit_fields.through + for i in inherit: + Through.objects.bulk_create([ + Through(food_id=x, foodinheritfield_id=i.id) + for x in Food.objects.filter(space=space_1).values_list('id', flat=True) + ]) + objs[0].move(objs[1], node_location) objs[1].move(objs[2], node_location) return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object @@ -471,8 +483,8 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1): @pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [ ({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'), ({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'), - ({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'), - ({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'), + ({'food_onhand': True, 'inherit': True}, 'food_onhand', True, 'false'), + ({'food_onhand': True, 'inherit': False}, 'food_onhand', False, 'false'), ], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): with scope(space=obj_tree_1.space): @@ -496,47 +508,17 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): assert (getattr(child, field) == new_val) == inherit -@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [ - ({'has_category': True, 'inherit': True, }, 'supermarket_category', True, 'cat_1'), - ({'ignore_shopping': True, 'inherit': True, }, 'ignore_shopping', True, 'false'), -], indirect=['obj_tree_1']) -# This is more about the model than the API - should this be moved to a different test? -def test_ignoreinherit_field(request, obj_tree_1, field, inherit, new_val, u1_s1): - with scope(space=obj_tree_1.space): - parent = obj_tree_1.get_parent() - child = obj_tree_1.get_descendants()[0] - obj_tree_1.ignore_inherit.add(FoodInheritField.objects.get(field=field)) - new_val = request.getfixturevalue(new_val) - - # change parent to a new value - setattr(parent, field, new_val) - with scope(space=parent.space): - parent.save() # trigger post-save signal - # get the objects again because values are cached - obj_tree_1 = Food.objects.get(id=obj_tree_1.id) - # inheritance is blocked - should not get new value - assert getattr(obj_tree_1, field) != new_val - - setattr(obj_tree_1, field, new_val) - with scope(space=parent.space): - obj_tree_1.save() # trigger post-save signal - # get the objects again because values are cached - child = Food.objects.get(id=child.id) - # inherit with child should still work - assert getattr(child, field) == new_val - - @pytest.mark.parametrize("obj_tree_1", [ - ({'has_category': True, 'inherit': False, 'ignore_shopping': True}), + ({'has_category': True, 'inherit': False, 'food_onhand': True}), ], indirect=['obj_tree_1']) def test_reset_inherit(obj_tree_1, space_1): with scope(space=space_1): - space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True)) # set default inherit fields + space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields parent = obj_tree_1.get_parent() child = obj_tree_1.get_descendants()[0] - obj_tree_1.ignore_shopping = False - assert parent.ignore_shopping == child.ignore_shopping - assert parent.ignore_shopping != obj_tree_1.ignore_shopping + obj_tree_1.food_onhand = False + assert parent.food_onhand == child.food_onhand + assert parent.food_onhand != obj_tree_1.food_onhand assert parent.supermarket_category != child.supermarket_category assert parent.supermarket_category != obj_tree_1.supermarket_category @@ -545,5 +527,5 @@ def test_reset_inherit(obj_tree_1, space_1): obj_tree_1 = Food.objects.get(id=obj_tree_1.id) parent = obj_tree_1.get_parent() child = obj_tree_1.get_descendants()[0] - assert parent.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping + assert parent.food_onhand == obj_tree_1.food_onhand == child.food_onhand assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category diff --git a/cookbook/tests/api/test_api_shopping_recipe.py b/cookbook/tests/api/test_api_shopping_recipe.py index 37ac53ba6..d564bf2ab 100644 --- a/cookbook/tests/api/test_api_shopping_recipe.py +++ b/cookbook/tests/api/test_api_shopping_recipe.py @@ -3,6 +3,8 @@ from datetime import timedelta import factory import pytest +# work around for bug described here https://stackoverflow.com/a/70312265/15762829 +from django.conf import settings from django.contrib import auth from django.forms import model_to_dict from django.urls import reverse @@ -14,6 +16,11 @@ from cookbook.models import Food, Ingredient, ShoppingListEntry, Step from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory, StepFactory, UserFactory) +if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', + 'django.db.backends.postgresql']: + from django.db.backends.postgresql.features import DatabaseFeatures + DatabaseFeatures.can_defer_constraint_checks = False + SHOPPING_LIST_URL = 'api:shoppinglistentry-list' SHOPPING_RECIPE_URL = 'api:recipe-shopping' @@ -43,7 +50,7 @@ def recipe(request, space_1, u1_s1): # steps__food_recipe_count = params.get('steps__food_recipe_count', {}) params['created_by'] = params.get('created_by', auth.get_user(u1_s1)) params['space'] = space_1 - return RecipeFactory.create(**params) + return RecipeFactory(**params) # return RecipeFactory.create( # steps__recipe_count=steps__recipe_count, @@ -178,27 +185,24 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u @pytest.mark.parametrize("user2, sle_count", [ - ({'mealplan_autoadd_shopping': False}, (0, 17)), - ({'mealplan_autoinclude_related': False}, (8, 8)), - ({'mealplan_autoexclude_onhand': False}, (19, 19)), - ({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (9, 9)), + ({'mealplan_autoadd_shopping': False}, (0, 18)), + ({'mealplan_autoinclude_related': False}, (9, 9)), + ({'mealplan_autoexclude_onhand': False}, (20, 20)), + ({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (10, 10)), ], indirect=['user2']) @pytest.mark.parametrize("use_mealplan", [(False), (True), ]) @pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe']) def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2): with scopes_disabled(): user = auth.get_user(user2) - # setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe), 1 food ignore shopping + # setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe) ingredients = Ingredient.objects.filter(step__recipe=recipe) food = Food.objects.get(id=ingredients[2].food.id) - food.on_hand = True + food.food_onhand = True food.save() food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food food = Food.objects.get(id=food.id) - food.on_hand = True - food.save() - food = Food.objects.get(id=ingredients[4].food.id) - food.ignore_shopping = True + food.food_onhand = True food.save() if use_mealplan: diff --git a/cookbook/tests/api/test_api_userpreference.py b/cookbook/tests/api/test_api_userpreference.py index b168d7075..c31e473fa 100644 --- a/cookbook/tests/api/test_api_userpreference.py +++ b/cookbook/tests/api/test_api_userpreference.py @@ -112,30 +112,29 @@ def test_preference_delete(u1_s1, u2_s1): def test_default_inherit_fields(u1_s1, u1_s2, space_1, space_2): - food_inherit_fields = Food.inherit_fields.all() - - r = u1_s1.get( - reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), - ) + food_inherit_fields = Food.inheritable_fields + assert len([x.field for x in food_inherit_fields]) > 0 # by default space food will not inherit any fields, so all of them will be ignored assert space_1.food_inherit.all().count() == 0 - assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0 - - # inherit all possible fields - with scope(space=space_1): - space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True)) r = u1_s1.get( reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), ) + assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == 0 - assert space_1.food_inherit.all().count() == Food.inherit_fields.all().count() > 0 - # now by default, food is not ignoring inheritance on any field - assert len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) == 0 + # inherit all possible fields + with scope(space=space_1): + space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) - # other spaces and users in those spaced not effected + assert space_1.food_inherit.all().count() == Food.inheritable_fields.count() > 0 + # now by default, food is inheriting all of the possible fields + r = u1_s1.get( + reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), + ) + assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == space_1.food_inherit.all().count() + + # other spaces and users in those spaces not effected r = u1_s2.get( reverse(DETAIL_URL, args={auth.get_user(u1_s2).id}), ) - assert space_2.food_inherit.all().count() == 0 - assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0 + assert space_2.food_inherit.all().count() == 0 == len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 41951f293..b5006752d 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -402,7 +402,8 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): # exclude fields not yet implemented - return Food.inherit_fields + self.queryset = Food.inheritable_fields + return super().get_queryset() class FoodViewSet(viewsets.ModelViewSet, TreeMixin): diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 0a8e83e16..ebbef836e 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -11,6 +11,7 @@ from django.utils.translation import gettext as _ from cookbook.forms import ExportForm, ImportForm, ImportExportBase from cookbook.helper.permission_helper import group_required from cookbook.integration.cookbookapp import CookBookApp +from cookbook.integration.copymethat import CopyMeThat from cookbook.integration.pepperplate import Pepperplate from cookbook.integration.cheftap import ChefTap from cookbook.integration.chowdown import Chowdown @@ -65,6 +66,8 @@ def get_integration(request, export_type): return Plantoeat(request, export_type) if export_type == ImportExportBase.COOKBOOKAPP: return CookBookApp(request, export_type) + if export_type == ImportExportBase.COPYMETHAT: + return CopyMeThat(request, export_type) @group_required('user') diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 09949be2f..382ba2ab1 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -201,7 +201,10 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView): def form_valid(self, form): obj = form.save(commit=False) obj.created_by = self.request.user - obj.space = self.request.space + + # verify given space is actually owned by the user creating the link + if obj.space.created_by != self.request.user: + obj.space = self.request.space obj.save() if obj.email: try: diff --git a/cookbook/views/views.py b/cookbook/views/views.py index ef79e072c..b875bc389 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -561,7 +561,7 @@ def space(request): space_form = SpacePreferenceForm(instance=request.space) - space_form.base_fields['food_inherit'].queryset = Food.inherit_fields + space_form.base_fields['food_inherit'].queryset = Food.inheritable_fields if request.method == "POST" and 'space_form' in request.POST: form = SpacePreferenceForm(request.POST, prefix='space') if form.is_valid(): diff --git a/docs/features/import_export.md b/docs/features/import_export.md index d53c30c04..9db829ecb 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -37,6 +37,7 @@ Overview of the capabilities of the different integrations. | OpenEats | ✔️ | ❌ | ⌚ | | Plantoeat | ✔️ | ❌ | ✔ | | CookBookApp | ✔️ | ⌚ | ✔️ | +| CopyMeThat | ✔️ | ❌ | ✔️ | ✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented @@ -218,3 +219,7 @@ Plan to eat allows you to export a text file containing all your recipes. Simply ## CookBookApp CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes. + +## CopyMeThat + +CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes. \ No newline at end of file diff --git a/docs/install/docker.md b/docs/install/docker.md index 78130a07e..60fe1f709 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -1,6 +1,6 @@ !!! success "Recommended Installation" - Setting up this application using Docker is recommended. This does not mean that other options are bad, just that - support is much easier for this setup. +Setting up this application using Docker is recommended. This does not mean that other options are bad, just that +support is much easier for this setup. It is possible to install this application using many Docker configurations. @@ -34,27 +34,27 @@ file in the GitHub repository to verify if additional environment variables are ### Versions -There are different versions (tags) released on docker hub. +There are different versions (tags) released on docker hub. -- **latest** Default image. The one you should use if you don't know that you need anything else. -- **beta** Partially stable version that gets updated every now and then. Expect to have some problems. -- **develop** If you want the most bleeding edge version with potentially many breaking changes feel free to use this version (I don't recommend it!). -- **X.Y.Z** each released version has its own image. If you need to revert to an old version or want to make sure you stay on one specific use these tags. +- **latest** Default image. The one you should use if you don't know that you need anything else. +- **beta** Partially stable version that gets updated every now and then. Expect to have some problems. +- **develop** If you want the most bleeding edge version with potentially many breaking changes feel free to use this version (I don't recommend it!). +- **X.Y.Z** each released version has its own image. If you need to revert to an old version or want to make sure you stay on one specific use these tags. !!! danger "No Downgrading" - There is currently no way to migrate back to an older version as there is no mechanism to downgrade the database. - You could probably do it but I cannot help you with that. Choose wisely if you want to use the unstable images. - That said **beta** should usually be working if you like frequent updates and new stuff. +There is currently no way to migrate back to an older version as there is no mechanism to downgrade the database. +You could probably do it but I cannot help you with that. Choose wisely if you want to use the unstable images. +That said **beta** should usually be working if you like frequent updates and new stuff. ## Docker Compose The main, and also recommended, installation option is to install this application using Docker Compose. 1. Choose your `docker-compose.yml` from the examples below. -2. Download the `.env` configuration file with `wget`, then **edit it accordingly**. - ```shell - wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env - ``` +2. Download the `.env` configuration file with `wget`, then **edit it accordingly** (you NEED to set `SECRET_KEY` and `POSTGRES_PASSWORD`). + ```shell + wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env + ``` 3. Start your container using `docker-compose up -d`. ### Plain @@ -65,29 +65,30 @@ This configuration exposes the application through an nginx web server on port 8 wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml ``` -~~~yaml -{% include "./docker/plain/docker-compose.yml" %} -~~~ +```yaml +{ % include "./docker/plain/docker-compose.yml" % } +``` ### Reverse Proxy Most deployments will likely use a reverse proxy. #### Traefik + If you use traefik, this configuration is the one for you. !!! info - Traefik can be a little confusing to setup. - Please refer to [their excellent documentation](https://doc.traefik.io/traefik/). If that does not help, - [this little example](traefik.md) might be for you. +Traefik can be a little confusing to setup. +Please refer to [their excellent documentation](https://doc.traefik.io/traefik/). If that does not help, +[this little example](traefik.md) might be for you. ```shell wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/traefik-nginx/docker-compose.yml ``` -~~~yaml -{% include "./docker/traefik-nginx/docker-compose.yml" %} -~~~ +```yaml +{ % include "./docker/traefik-nginx/docker-compose.yml" % } +``` #### nginx-proxy @@ -97,6 +98,7 @@ in combination with [jrcs's letsencrypt companion](https://hub.docker.com/r/jrcs Please refer to the appropriate documentation on how to setup the reverse proxy and networks. Remember to add the appropriate environment variables to `.env` file: + ``` VIRTUAL_HOST= LETSENCRYPT_HOST= @@ -107,27 +109,49 @@ LETSENCRYPT_EMAIL= wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/nginx-proxy/docker-compose.yml ``` -~~~yaml -{% include "./docker/nginx-proxy/docker-compose.yml" %} -~~~ +```yaml +{ % include "./docker/nginx-proxy/docker-compose.yml" % } +``` #### Nginx Swag by LinuxServer -[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io -It also contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance +[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io. + +It contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance. If you're running Swag on the default port, you'll just need to change the container name to yours. -If your running Swag on a custom port, some headers must be changed. To do this, +If your running Swag on a custom port, some headers must be changed: -- Create a copy of `proxy.conf` -- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to - - `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;` -- Update `recipes.subdomain.conf` to use the new file -- Restart the linuxserver/swag container and Recipes will work +- Create a copy of `proxy.conf` +- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to + - `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;` +- Update `recipes.subdomain.conf` to use the new file +- Restart the linuxserver/swag container and Recipes will work correctly More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627). +In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory. + +Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup. + +#### Nginx Swag by LinuxServer + +[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io + +It also contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance + +If you're running Swag on the default port, you'll just need to change the container name to yours. + +If your running Swag on a custom port, some headers must be changed. To do this, + +- Create a copy of `proxy.conf` +- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to + - `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;` +- Update `recipes.subdomain.conf` to use the new file +- Restart the linuxserver/swag container and Recipes will work + +More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627). In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory. @@ -136,6 +160,7 @@ Please refer to the [appropriate documentation](https://github.com/linuxserver/d ## Additional Information ### Nginx vs Gunicorn + All examples use an additional `nginx` container to serve mediafiles and act as the forward facing webserver. This is **technically not required** but **very much recommended**. @@ -144,14 +169,14 @@ the WSGi server that handles the Python execution, explicitly state that it is n You will also likely not see any decrease in performance or a lot of space used as nginx is a very light container. !!! info - Even if you run behind a reverse proxy as described above, using an additional nginx container is the recommended option. +Even if you run behind a reverse proxy as described above, using an additional nginx container is the recommended option. If you run a small private deployment and don't care about performance, security and whatever else feel free to run without a ngix container. !!! warning - When running without nginx make sure to enable `GUNICORN_MEDIA` in the `.env`. Without it, media files will be uploaded - but not shown on the page. +When running without nginx make sure to enable `GUNICORN_MEDIA` in the `.env`. Without it, media files will be uploaded +but not shown on the page. For additional information please refer to the [0.9.0 Release](https://github.com/vabene1111/recipes/releases?after=0.9.0) and [Issue 201](https://github.com/vabene1111/recipes/issues/201) where these topics have been discussed. diff --git a/recipes/settings.py b/recipes/settings.py index 80390509a..0e79da2e2 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -56,6 +56,7 @@ CORS_ORIGIN_ALLOW_ALL = True LOGIN_REDIRECT_URL = "index" LOGOUT_REDIRECT_URL = "index" +ACCOUNT_LOGOUT_REDIRECT_URL = "index" SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_COOKIE_AGE = 365 * 60 * 24 * 60 diff --git a/vue/src/apps/ModelListView/ModelListView.vue b/vue/src/apps/ModelListView/ModelListView.vue index 92420b013..d889267c7 100644 --- a/vue/src/apps/ModelListView/ModelListView.vue +++ b/vue/src/apps/ModelListView/ModelListView.vue @@ -19,8 +19,10 @@ {{ this.this_model.name }} - + + + @@ -112,6 +114,9 @@ export default { // TODO this is not necessarily bad but maybe there are better options to do this return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`) }, + apiName() { + return this.this_model?.apiName + }, }, mounted() { // value is passed from lists.py @@ -291,11 +296,6 @@ export default { this.refreshCard({ ...food }, this.items_right) }) }, - addOnhand: function (item) { - item.on_hand = true - this.saveThis(item) - }, - updateThis: function (item) { this.refreshThis(item.id) }, diff --git a/vue/src/apps/RecipeEditView/RecipeEditView.vue b/vue/src/apps/RecipeEditView/RecipeEditView.vue index 3b98745ee..f4810026a 100644 --- a/vue/src/apps/RecipeEditView/RecipeEditView.vue +++ b/vue/src/apps/RecipeEditView/RecipeEditView.vue @@ -49,13 +49,13 @@
- +
- +
- +
@@ -343,7 +343,7 @@
- @@ -623,9 +623,10 @@ export default { this.sortIngredients(s) } - if (this.recipe.waiting_time === ''){ this.recipe.waiting_time = 0} - if (this.recipe.working_time === ''){ this.recipe.working_time = 0} - if (this.recipe.servings === ''){ this.recipe.servings = 0} + if (this.recipe.waiting_time === '' || isNaN(this.recipe.waiting_time)){ this.recipe.waiting_time = 0} + if (this.recipe.working_time === ''|| isNaN(this.recipe.working_time)){ this.recipe.working_time = 0} + if (this.recipe.servings === ''|| isNaN(this.recipe.servings)){ this.recipe.servings = 0} + apiFactory.updateRecipe(this.recipe_id, this.recipe, {}).then((response) => { diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index 9b7571fc8..a76c7f051 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -306,7 +306,7 @@ export default { this.settings?.search_keywords?.length === 0 && this.settings?.search_foods?.length === 0 && this.settings?.search_books?.length === 0 && - this.settings?.pagination_page === 1 && + // this.settings?.pagination_page === 1 && !this.random_search && this.settings?.search_ratings === undefined ) { diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index 6cbc09762..98ee617eb 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -200,6 +200,9 @@ export default { ingredient_factor: function () { return this.servings / this.recipe.servings }, + title() { + return this.recipe?.steps?.map((x) => x?.ingredients).flat() + }, }, data() { return { @@ -212,9 +215,11 @@ export default { share_uid: window.SHARE_UID, } }, + mounted() { this.loadRecipe(window.RECIPE_ID) this.$i18n.locale = window.CUSTOM_LOCALE + console.log(this.recipe) }, methods: { loadRecipe: function (recipe_id) { diff --git a/vue/src/apps/ShoppingListView/ShoppingListView.vue b/vue/src/apps/ShoppingListView/ShoppingListView.vue index 277a872dd..37aa5e2df 100644 --- a/vue/src/apps/ShoppingListView/ShoppingListView.vue +++ b/vue/src/apps/ShoppingListView/ShoppingListView.vue @@ -30,22 +30,24 @@
-
-
+ + + -
-
+ + -
-
+ + -
-
+ + -
-
+ + +
@@ -491,15 +493,6 @@ - - {{ $t("IgnoreThis", { food: foodName(contextData) }) }} - - { console.log(err) @@ -906,13 +899,6 @@ export default { getThis: function (id) { return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id }) }, - ignoreThis: function (item) { - let food = { - id: item?.[0]?.food.id ?? item.food.id, - ignore_shopping: true, - } - this.updateFood(food, "ignore_shopping") - }, mergeShoppingList: function (data) { this.items.map((x) => data.map((y) => { @@ -939,10 +925,10 @@ export default { let api = new ApiApiFactory() let food = { id: item?.[0]?.food.id ?? item?.food?.id, - on_hand: true, + food_onhand: true, } - this.updateFood(food) + this.updateFood(food, "food_onhand") .then((result) => { let entries = this.items.filter((x) => x.food.id == food.id).map((x) => x.id) this.items = this.items.filter((x) => x.food.id !== food.id) @@ -1005,16 +991,18 @@ export default { // when checking a sub item don't refresh the screen until all entries complete but change class to cross out let promises = [] update.entries.forEach((x) => { - promises.push(this.saveThis({ id: x, checked: update.checked }, false)) - let item = this.items.filter((entry) => entry.id == x)[0] - - Vue.set(item, "checked", update.checked) + const id = x?.id ?? x + let completed_at = undefined if (update.checked) { - Vue.set(item, "completed_at", new Date().toISOString()) - } else { - Vue.set(item, "completed_at", undefined) + completed_at = new Date().toISOString() } + promises.push(this.saveThis({ id: id, checked: update.checked }, false)) + + let item = this.items.filter((entry) => entry.id == id)[0] + Vue.set(item, "checked", update.checked) + Vue.set(item, "completed_at", completed_at) }) + Promise.all(promises).catch((err) => { console.log(err, err.response) StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE) @@ -1024,8 +1012,8 @@ export default { let api = new ApiApiFactory() let ignore_category if (field) { - ignore_category = food.ignore_inherit - .map((x) => food.ignore_inherit.fields) + ignore_category = food.inherit_fields + .map((x) => food.inherit_fields.fields) .flat() .includes(field) } else { @@ -1035,7 +1023,7 @@ export default { return api .partialUpdateFood(food.id, food) .then((result) => { - if (food.inherit && food.supermarket_category && !ignore_category && food.parent) { + if (food.supermarket_category && !ignore_category && food.parent) { makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning") } else { StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE) diff --git a/vue/src/components/Badges.vue b/vue/src/components/Badges.vue index f35999480..3e353d7ed 100644 --- a/vue/src/components/Badges.vue +++ b/vue/src/components/Badges.vue @@ -1,52 +1,44 @@ \ No newline at end of file + diff --git a/vue/src/components/Badges/OnHand.vue b/vue/src/components/Badges/OnHand.vue index 9f851af14..7e48294ed 100644 --- a/vue/src/components/Badges/OnHand.vue +++ b/vue/src/components/Badges/OnHand.vue @@ -1,7 +1,7 @@