Merge remote-tracking branch 'origin/feature/import-automation' into develop
This commit is contained in:
commit
b4bcf5c032
@ -28,7 +28,7 @@ class IngredientParser:
|
|||||||
self.food_aliases = c
|
self.food_aliases = c
|
||||||
caches['default'].touch(FOOD_CACHE_KEY, 30)
|
caches['default'].touch(FOOD_CACHE_KEY, 30)
|
||||||
else:
|
else:
|
||||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').all():
|
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||||
self.food_aliases[a.param_1] = a.param_2
|
self.food_aliases[a.param_1] = a.param_2
|
||||||
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ class IngredientParser:
|
|||||||
self.unit_aliases = c
|
self.unit_aliases = c
|
||||||
caches['default'].touch(UNIT_CACHE_KEY, 30)
|
caches['default'].touch(UNIT_CACHE_KEY, 30)
|
||||||
else:
|
else:
|
||||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').all():
|
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||||
self.unit_aliases[a.param_1] = a.param_2
|
self.unit_aliases[a.param_1] = a.param_2
|
||||||
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
||||||
else:
|
else:
|
||||||
@ -59,7 +59,7 @@ class IngredientParser:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return food
|
return food
|
||||||
else:
|
else:
|
||||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).first():
|
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).order_by('order').first():
|
||||||
return automation.param_2
|
return automation.param_2
|
||||||
return food
|
return food
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class IngredientParser:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return unit
|
return unit
|
||||||
else:
|
else:
|
||||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).first():
|
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).order_by('order').first():
|
||||||
return automation.param_2
|
return automation.param_2
|
||||||
return unit
|
return unit
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@ from recipe_scrapers._utils import get_host_name, get_minutes
|
|||||||
|
|
||||||
from cookbook.helper import recipe_url_import as helper
|
from cookbook.helper import recipe_url_import as helper
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
from cookbook.models import Keyword
|
from cookbook.models import Keyword, Automation
|
||||||
|
|
||||||
|
|
||||||
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ def get_from_scraper(scrape, request):
|
|||||||
try:
|
try:
|
||||||
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
recipe_json['source_url'] = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
|
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
|
||||||
@ -139,10 +140,18 @@ def get_from_scraper(scrape, request):
|
|||||||
if len(recipe_json['steps']) == 0:
|
if len(recipe_json['steps']) == 0:
|
||||||
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
|
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
|
||||||
|
|
||||||
if len(parse_description(description)) > 256: # split at 256 as long descriptions dont look good on recipe cards
|
parsed_description = parse_description(description)
|
||||||
recipe_json['steps'][0]['instruction'] = f'*{parse_description(description)}* \n\n' + recipe_json['steps'][0]['instruction']
|
# TODO notify user about limit if reached
|
||||||
|
# limits exist to limit the attack surface for dos style attacks
|
||||||
|
automations = Automation.objects.filter(type=Automation.DESCRIPTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').all().order_by('order')[:512]
|
||||||
|
for a in automations:
|
||||||
|
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
|
||||||
|
parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1)
|
||||||
|
|
||||||
|
if len(parsed_description) > 256: # split at 256 as long descriptions don't look good on recipe cards
|
||||||
|
recipe_json['steps'][0]['instruction'] = f'*{parsed_description}* \n\n' + recipe_json['steps'][0]['instruction']
|
||||||
else:
|
else:
|
||||||
recipe_json['description'] = parse_description(description)[:512]
|
recipe_json['description'] = parsed_description[:512]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for x in scrape.ingredients():
|
for x in scrape.ingredients():
|
||||||
@ -175,6 +184,13 @@ def get_from_scraper(scrape, request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if recipe_json['source_url']:
|
||||||
|
automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512]
|
||||||
|
for a in automations:
|
||||||
|
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
|
||||||
|
for s in recipe_json['steps']:
|
||||||
|
s['instruction'] = re.sub(a.param_2, a.param_3, s['instruction'])
|
||||||
|
|
||||||
return recipe_json
|
return recipe_json
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.1.4 on 2023-01-03 21:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0185_food_plural_name_ingredient_always_use_plural_food_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='automation',
|
||||||
|
name='order',
|
||||||
|
field=models.IntegerField(default=1000),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='automation',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace')], max_length=128),
|
||||||
|
),
|
||||||
|
]
|
@ -367,7 +367,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True,blank=True, related_name='user_image')
|
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='user_image')
|
||||||
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
||||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||||
default_unit = models.CharField(max_length=32, default='g')
|
default_unit = models.CharField(max_length=32, default='g')
|
||||||
@ -1223,9 +1223,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
|||||||
FOOD_ALIAS = 'FOOD_ALIAS'
|
FOOD_ALIAS = 'FOOD_ALIAS'
|
||||||
UNIT_ALIAS = 'UNIT_ALIAS'
|
UNIT_ALIAS = 'UNIT_ALIAS'
|
||||||
KEYWORD_ALIAS = 'KEYWORD_ALIAS'
|
KEYWORD_ALIAS = 'KEYWORD_ALIAS'
|
||||||
|
DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE'
|
||||||
|
INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE'
|
||||||
|
|
||||||
type = models.CharField(max_length=128,
|
type = models.CharField(max_length=128,
|
||||||
choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),))
|
choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),
|
||||||
|
(DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),))
|
||||||
name = models.CharField(max_length=128, default='')
|
name = models.CharField(max_length=128, default='')
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
@ -1233,6 +1236,8 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
|||||||
param_2 = models.CharField(max_length=128, blank=True, null=True)
|
param_2 = models.CharField(max_length=128, blank=True, null=True)
|
||||||
param_3 = models.CharField(max_length=128, blank=True, null=True)
|
param_3 = models.CharField(max_length=128, blank=True, null=True)
|
||||||
|
|
||||||
|
order = models.IntegerField(default=1000)
|
||||||
|
|
||||||
disabled = models.BooleanField(default=False)
|
disabled = models.BooleanField(default=False)
|
||||||
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
@ -876,11 +876,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
|||||||
value = value.quantize(
|
value = value.quantize(
|
||||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||||
return (
|
return (
|
||||||
obj.name
|
obj.name
|
||||||
or getattr(obj.mealplan, 'title', None)
|
or getattr(obj.mealplan, 'title', None)
|
||||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||||
or obj.recipe.name
|
or obj.recipe.name
|
||||||
) + f' ({value:.2g})'
|
) + f' ({value:.2g})'
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
# TODO remove once old shopping list
|
# TODO remove once old shopping list
|
||||||
@ -1067,7 +1067,7 @@ class AutomationSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Automation
|
model = Automation
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'type', 'name', 'description', 'param_1', 'param_2', 'param_3', 'disabled', 'created_by',)
|
'id', 'type', 'name', 'description', 'param_1', 'param_2', 'param_3', 'order', 'disabled', 'created_by',)
|
||||||
read_only_fields = ('created_by',)
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ def page_help(page_name):
|
|||||||
'view_shopping': 'https://docs.tandoor.dev/features/shopping/',
|
'view_shopping': 'https://docs.tandoor.dev/features/shopping/',
|
||||||
'view_import': 'https://docs.tandoor.dev/features/import_export/',
|
'view_import': 'https://docs.tandoor.dev/features/import_export/',
|
||||||
'view_export': 'https://docs.tandoor.dev/features/import_export/',
|
'view_export': 'https://docs.tandoor.dev/features/import_export/',
|
||||||
|
'list_automation': 'https://docs.tandoor.dev/features/automation/',
|
||||||
}
|
}
|
||||||
|
|
||||||
link = help_pages.get(page_name, '')
|
link = help_pages.get(page_name, '')
|
||||||
|
49
cookbook/tests/other/test_automations.py
Normal file
49
cookbook/tests/other/test_automations.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.forms import ImportExportBase
|
||||||
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
|
from cookbook.models import ExportLog, Automation
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from cookbook.tests.conftest import validate_recipe
|
||||||
|
|
||||||
|
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
||||||
|
|
||||||
|
|
||||||
|
def test_description_replace_automation(u1_s1, space_1):
|
||||||
|
if 'cookbook' in os.getcwd():
|
||||||
|
test_file = os.path.join(os.getcwd(), 'other', 'test_data', 'chefkoch2.html')
|
||||||
|
else:
|
||||||
|
test_file = os.path.join(os.getcwd(), 'cookbook', 'tests', 'other', 'test_data', 'chefkoch2.html')
|
||||||
|
|
||||||
|
# original description
|
||||||
|
# Brokkoli - Bratlinge. Über 91 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
Automation.objects.create(
|
||||||
|
name='test1',
|
||||||
|
created_by=auth.get_user(u1_s1),
|
||||||
|
space=space_1,
|
||||||
|
param_1='.*',
|
||||||
|
param_2='.*',
|
||||||
|
param_3='',
|
||||||
|
order=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(test_file, 'r', encoding='UTF-8') as d:
|
||||||
|
response = u1_s1.post(
|
||||||
|
reverse(IMPORT_SOURCE_URL),
|
||||||
|
{
|
||||||
|
'data': d.read(),
|
||||||
|
'url': 'https://www.chefkoch.de/rezepte/804871184310070/Brokkoli-Bratlinge.html',
|
||||||
|
},
|
||||||
|
content_type='application/json')
|
||||||
|
recipe = json.loads(response.content)['recipe_json']
|
||||||
|
assert recipe['description'] == ''
|
@ -2,13 +2,16 @@ import json
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.tests.conftest import validate_recipe
|
from cookbook.tests.conftest import validate_recipe
|
||||||
|
|
||||||
from ._recipes import (ALLRECIPES, AMERICAS_TEST_KITCHEN, CHEF_KOCH, CHEF_KOCH2, COOKPAD,
|
from ._recipes import (ALLRECIPES, AMERICAS_TEST_KITCHEN, CHEF_KOCH, CHEF_KOCH2, COOKPAD,
|
||||||
COOKS_COUNTRY, DELISH, FOOD_NETWORK, GIALLOZAFFERANO, JOURNAL_DES_FEMMES,
|
COOKS_COUNTRY, DELISH, FOOD_NETWORK, GIALLOZAFFERANO, JOURNAL_DES_FEMMES,
|
||||||
MADAME_DESSERT, MARMITON, TASTE_OF_HOME, THE_SPRUCE_EATS, TUDOGOSTOSO)
|
MADAME_DESSERT, MARMITON, TASTE_OF_HOME, THE_SPRUCE_EATS, TUDOGOSTOSO)
|
||||||
|
from ...models import Automation
|
||||||
|
|
||||||
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
||||||
DATA_DIR = "cookbook/tests/other/test_data/"
|
DATA_DIR = "cookbook/tests/other/test_data/"
|
||||||
@ -72,3 +75,35 @@ def test_recipe_import(arg, u1_s1):
|
|||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
recipe = json.loads(response.content)['recipe_json']
|
recipe = json.loads(response.content)['recipe_json']
|
||||||
validate_recipe(arg, recipe)
|
validate_recipe(arg, recipe)
|
||||||
|
|
||||||
|
|
||||||
|
def test_description_replace_automation(u1_s1, space_1):
|
||||||
|
if 'cookbook' in os.getcwd():
|
||||||
|
test_file = os.path.join(os.getcwd(), 'other', 'test_data', 'chefkoch2.html')
|
||||||
|
else:
|
||||||
|
test_file = os.path.join(os.getcwd(), 'cookbook', 'tests', 'other', 'test_data', 'chefkoch2.html')
|
||||||
|
|
||||||
|
# original description
|
||||||
|
# Brokkoli - Bratlinge. Über 91 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
Automation.objects.create(
|
||||||
|
name='test1',
|
||||||
|
created_by=auth.get_user(u1_s1),
|
||||||
|
space=space_1,
|
||||||
|
param_1='.*',
|
||||||
|
param_2='.*',
|
||||||
|
param_3='',
|
||||||
|
order=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(test_file, 'r', encoding='UTF-8', errors='ignore') as d:
|
||||||
|
response = u1_s1.post(
|
||||||
|
reverse(IMPORT_SOURCE_URL),
|
||||||
|
{
|
||||||
|
'data': d.read(),
|
||||||
|
'url': 'https://www.chefkoch.de/rezepte/804871184310070/Brokkoli-Bratlinge.html',
|
||||||
|
},
|
||||||
|
content_type='application/json')
|
||||||
|
recipe = json.loads(response.content)['recipe_json']
|
||||||
|
assert recipe['description'] == ''
|
||||||
|
64
docs/features/automation.md
Normal file
64
docs/features/automation.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
!!! warning
|
||||||
|
Automations are currently in a beta stage. They work pretty stable but if I encounter any
|
||||||
|
issues while working on them, I might change how they work breaking existing automations.
|
||||||
|
I will try to avoid this and am pretty confident it won't happen.
|
||||||
|
|
||||||
|
|
||||||
|
Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that
|
||||||
|
would otherwise have to be done manually. Currently, the following automations are supported.
|
||||||
|
|
||||||
|
## Unit, Food, Keyword Alias
|
||||||
|
Foods, Units and Keywords can have automations that automatically replace them with another object
|
||||||
|
to allow aliasing them.
|
||||||
|
|
||||||
|
This helps to add consistency to the naming of objects, for example to always use the singular form
|
||||||
|
for the main name if a plural form is configured.
|
||||||
|
|
||||||
|
These automations are best created by dragging and dropping Foods, Units or Keywords in their respective
|
||||||
|
views and creating the automation there.
|
||||||
|
|
||||||
|
You can also create them manually by setting the following
|
||||||
|
- **Parameter 1**: name of food/unit/keyword to match
|
||||||
|
- **Parameter 2**: name of food/unit/keyword to replace matched food with
|
||||||
|
|
||||||
|
These rules are processed whenever you are importing recipes from websites or other apps
|
||||||
|
and when using the simple ingredient input (shopping, recipe editor, ...).
|
||||||
|
|
||||||
|
## Description Replace
|
||||||
|
This automation is a bit more complicated than the alis rules. It is run when importing a recipe
|
||||||
|
from a website.
|
||||||
|
|
||||||
|
It uses Regular Expressions (RegEx) to determine if a description should be altered, what exactly to remove
|
||||||
|
and what to replace it with.
|
||||||
|
|
||||||
|
- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`)
|
||||||
|
- **Parameter 2**: pattern of what to replace (e.g. `.*`)
|
||||||
|
- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced.
|
||||||
|
|
||||||
|
To replace the description the python [re.sub](https://docs.python.org/2/library/re.html#re.sub) function is used
|
||||||
|
like this `re.sub(<parameter 2>, <parameter 2>, <descriotion>, count=1)`
|
||||||
|
|
||||||
|
To test out your patterns and learn about RegEx you can use [regexr.com](https://regexr.com/)
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
In order to prevent denial of service attacks on the RegEx engine the number of replace automations
|
||||||
|
and the length of the inputs that are processed are limited. Those limits should never be reached
|
||||||
|
during normal usage.
|
||||||
|
|
||||||
|
## Instruction Replace
|
||||||
|
This works just like the Description Replace automation but runs against all instruction texts
|
||||||
|
in all steps of a recipe during import.
|
||||||
|
|
||||||
|
Also instead of just replacing a single occurrence of the matched pattern it will replace all.
|
||||||
|
|
||||||
|
# Order
|
||||||
|
If the Automation type allows for more than one rule to be executed (for example description replace)
|
||||||
|
the rules are processed in ascending order (ordered by the *order* property of the automation).
|
||||||
|
The default order is always 1000 to make it easier to add automations before and after other automations.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
1. Rule ABC (order 1000) replaces `everything` with `abc`
|
||||||
|
2. Rule DEF (order 2000) replaces `everything` with `def`
|
||||||
|
3. Rule XYZ (order 500) replaces `everything` with `xyz`
|
||||||
|
|
||||||
|
After processing rules XYZ, then ABC and then DEF the description will have the value `def`
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||||
|
"spaces": 2,
|
||||||
|
"generator-cli": {
|
||||||
|
"version": "6.2.1"
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||||
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
|
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
|
||||||
<date-input v-if="visibleCondition(f, 'date')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" :subtitle="f.subtitle" />
|
<date-input v-if="visibleCondition(f, 'date')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" :subtitle="f.subtitle" />
|
||||||
|
<number-input v-if="visibleCondition(f, 'number')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" />
|
||||||
</div>
|
</div>
|
||||||
<template v-slot:modal-footer>
|
<template v-slot:modal-footer>
|
||||||
<div class="row w-100">
|
<div class="row w-100">
|
||||||
@ -49,10 +50,11 @@ import ChoiceInput from "@/components/Modals/ChoiceInput"
|
|||||||
import FileInput from "@/components/Modals/FileInput"
|
import FileInput from "@/components/Modals/FileInput"
|
||||||
import SmallText from "@/components/Modals/SmallText"
|
import SmallText from "@/components/Modals/SmallText"
|
||||||
import HelpBadge from "@/components/Badges/Help"
|
import HelpBadge from "@/components/Badges/Help"
|
||||||
|
import NumberInput from "@/components/Modals/NumberInput.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "GenericModalForm",
|
name: "GenericModalForm",
|
||||||
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput },
|
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput, NumberInput },
|
||||||
mixins: [ApiMixin, ToastMixin],
|
mixins: [ApiMixin, ToastMixin],
|
||||||
props: {
|
props: {
|
||||||
model: { required: true, type: Object },
|
model: { required: true, type: Object },
|
||||||
|
37
vue/src/components/Modals/NumberInput.vue
Normal file
37
vue/src/components/Modals/NumberInput.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-form-group v-bind:label="label" class="mb-3">
|
||||||
|
<b-form-input v-model="new_value" type="number" :placeholder="placeholder"></b-form-input>
|
||||||
|
<em v-if="help" class="small text-muted">{{ help }}</em>
|
||||||
|
<small v-if="subtitle" class="text-muted">{{ subtitle }}</small>
|
||||||
|
</b-form-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "TextInput",
|
||||||
|
props: {
|
||||||
|
field: { type: String, default: "You Forgot To Set Field Name" },
|
||||||
|
label: { type: String, default: "Text Field" },
|
||||||
|
value: { type: String, default: "" },
|
||||||
|
placeholder: { type: Number, default: 0 },
|
||||||
|
help: { type: String, default: undefined },
|
||||||
|
subtitle: { type: String, default: undefined },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
new_value: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.new_value = this.value
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
new_value: function () {
|
||||||
|
this.$root.$emit("change", this.field, this.new_value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
@ -68,6 +68,8 @@
|
|||||||
"Enable_Amount": "Enable Amount",
|
"Enable_Amount": "Enable Amount",
|
||||||
"Disable_Amount": "Disable Amount",
|
"Disable_Amount": "Disable Amount",
|
||||||
"Ingredient Editor": "Ingredient Editor",
|
"Ingredient Editor": "Ingredient Editor",
|
||||||
|
"Description_Replace": "Description Replace",
|
||||||
|
"Instruction_Replace": "Instruction Replace",
|
||||||
"Auto_Sort": "Auto Sort",
|
"Auto_Sort": "Auto Sort",
|
||||||
"Auto_Sort_Help": "Move all ingredients to the best fitting step.",
|
"Auto_Sort_Help": "Move all ingredients to the best fitting step.",
|
||||||
"Private_Recipe": "Private Recipe",
|
"Private_Recipe": "Private Recipe",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -137,6 +137,12 @@ export interface Automation {
|
|||||||
* @memberof Automation
|
* @memberof Automation
|
||||||
*/
|
*/
|
||||||
param_3?: string | null;
|
param_3?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof Automation
|
||||||
|
*/
|
||||||
|
order?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -158,7 +164,9 @@ export interface Automation {
|
|||||||
export enum AutomationTypeEnum {
|
export enum AutomationTypeEnum {
|
||||||
FoodAlias = 'FOOD_ALIAS',
|
FoodAlias = 'FOOD_ALIAS',
|
||||||
UnitAlias = 'UNIT_ALIAS',
|
UnitAlias = 'UNIT_ALIAS',
|
||||||
KeywordAlias = 'KEYWORD_ALIAS'
|
KeywordAlias = 'KEYWORD_ALIAS',
|
||||||
|
DescriptionReplace = 'DESCRIPTION_REPLACE',
|
||||||
|
InstructionReplace = 'INSTRUCTION_REPLACE'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -431,6 +439,12 @@ export interface Food {
|
|||||||
* @memberof Food
|
* @memberof Food
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Food
|
||||||
|
*/
|
||||||
|
plural_name?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -655,6 +669,12 @@ export interface FoodSubstitute {
|
|||||||
* @memberof FoodSubstitute
|
* @memberof FoodSubstitute
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof FoodSubstitute
|
||||||
|
*/
|
||||||
|
plural_name?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -848,10 +868,10 @@ export interface Ingredient {
|
|||||||
food: IngredientFood | null;
|
food: IngredientFood | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {FoodSupermarketCategory}
|
* @type {IngredientUnit}
|
||||||
* @memberof Ingredient
|
* @memberof Ingredient
|
||||||
*/
|
*/
|
||||||
unit: FoodSupermarketCategory | null;
|
unit: IngredientUnit | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -894,6 +914,18 @@ export interface Ingredient {
|
|||||||
* @memberof Ingredient
|
* @memberof Ingredient
|
||||||
*/
|
*/
|
||||||
used_in_recipes?: string;
|
used_in_recipes?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof Ingredient
|
||||||
|
*/
|
||||||
|
always_use_plural_unit?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof Ingredient
|
||||||
|
*/
|
||||||
|
always_use_plural_food?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -913,6 +945,12 @@ export interface IngredientFood {
|
|||||||
* @memberof IngredientFood
|
* @memberof IngredientFood
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof IngredientFood
|
||||||
|
*/
|
||||||
|
plural_name?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -1004,6 +1042,37 @@ export interface IngredientFood {
|
|||||||
*/
|
*/
|
||||||
child_inherit_fields?: Array<FoodInheritFields> | null;
|
child_inherit_fields?: Array<FoodInheritFields> | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface IngredientUnit
|
||||||
|
*/
|
||||||
|
export interface IngredientUnit {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof IngredientUnit
|
||||||
|
*/
|
||||||
|
id?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof IngredientUnit
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof IngredientUnit
|
||||||
|
*/
|
||||||
|
plural_name?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof IngredientUnit
|
||||||
|
*/
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
@ -1746,13 +1815,13 @@ export interface MealPlanRecipe {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof MealPlanRecipe
|
* @memberof MealPlanRecipe
|
||||||
*/
|
*/
|
||||||
rating?: string;
|
rating?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof MealPlanRecipe
|
* @memberof MealPlanRecipe
|
||||||
*/
|
*/
|
||||||
last_cooked?: string;
|
last_cooked?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -1953,13 +2022,13 @@ export interface Recipe {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof Recipe
|
* @memberof Recipe
|
||||||
*/
|
*/
|
||||||
rating?: string;
|
rating?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof Recipe
|
* @memberof Recipe
|
||||||
*/
|
*/
|
||||||
last_cooked?: string;
|
last_cooked?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -2166,10 +2235,10 @@ export interface RecipeIngredients {
|
|||||||
food: IngredientFood | null;
|
food: IngredientFood | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {FoodSupermarketCategory}
|
* @type {IngredientUnit}
|
||||||
* @memberof RecipeIngredients
|
* @memberof RecipeIngredients
|
||||||
*/
|
*/
|
||||||
unit: FoodSupermarketCategory | null;
|
unit: IngredientUnit | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -2212,6 +2281,18 @@ export interface RecipeIngredients {
|
|||||||
* @memberof RecipeIngredients
|
* @memberof RecipeIngredients
|
||||||
*/
|
*/
|
||||||
used_in_recipes?: string;
|
used_in_recipes?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof RecipeIngredients
|
||||||
|
*/
|
||||||
|
always_use_plural_unit?: boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof RecipeIngredients
|
||||||
|
*/
|
||||||
|
always_use_plural_food?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -2412,13 +2493,13 @@ export interface RecipeOverview {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof RecipeOverview
|
* @memberof RecipeOverview
|
||||||
*/
|
*/
|
||||||
rating?: string;
|
rating?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof RecipeOverview
|
* @memberof RecipeOverview
|
||||||
*/
|
*/
|
||||||
last_cooked?: string;
|
last_cooked?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -2703,10 +2784,10 @@ export interface ShoppingListEntries {
|
|||||||
food: IngredientFood | null;
|
food: IngredientFood | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {FoodSupermarketCategory}
|
* @type {IngredientUnit}
|
||||||
* @memberof ShoppingListEntries
|
* @memberof ShoppingListEntries
|
||||||
*/
|
*/
|
||||||
unit?: FoodSupermarketCategory | null;
|
unit?: IngredientUnit | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
@ -2794,10 +2875,10 @@ export interface ShoppingListEntry {
|
|||||||
food: IngredientFood | null;
|
food: IngredientFood | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {FoodSupermarketCategory}
|
* @type {IngredientUnit}
|
||||||
* @memberof ShoppingListEntry
|
* @memberof ShoppingListEntry
|
||||||
*/
|
*/
|
||||||
unit?: FoodSupermarketCategory | null;
|
unit?: IngredientUnit | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
@ -3191,41 +3272,16 @@ export interface Space {
|
|||||||
file_size_mb?: string;
|
file_size_mb?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SpaceImage}
|
* @type {RecipeFile}
|
||||||
* @memberof Space
|
* @memberof Space
|
||||||
*/
|
*/
|
||||||
image?: SpaceImage;
|
image?: RecipeFile | null;
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface SpaceImage
|
|
||||||
*/
|
|
||||||
export interface SpaceImage {
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {boolean}
|
||||||
* @memberof SpaceImage
|
* @memberof Space
|
||||||
*/
|
*/
|
||||||
id?: number;
|
use_plural?: boolean;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SpaceImage
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SpaceImage
|
|
||||||
*/
|
|
||||||
file_download?: string;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SpaceImage
|
|
||||||
*/
|
|
||||||
preview?: string;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -3563,6 +3619,12 @@ export interface Unit {
|
|||||||
* @memberof Unit
|
* @memberof Unit
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Unit
|
||||||
|
*/
|
||||||
|
plural_name?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -717,4 +717,10 @@ export const formFunctions = {
|
|||||||
form.fields.filter((x) => x.field === "inherit_fields")[0].value = getUserPreference("food_inherit_default")
|
form.fields.filter((x) => x.field === "inherit_fields")[0].value = getUserPreference("food_inherit_default")
|
||||||
return form
|
return form
|
||||||
},
|
},
|
||||||
|
AutomationOrderDefault: function (form) {
|
||||||
|
if (form.fields.filter((x) => x.field === "order")[0].value === undefined) {
|
||||||
|
form.fields.filter((x) => x.field === "order")[0].value = 1000
|
||||||
|
}
|
||||||
|
return form
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user