This commit is contained in:
vabene1111 2023-01-03 23:12:00 +01:00
parent 56252a707a
commit 6176eeb024
13 changed files with 1209 additions and 985 deletions

View File

@ -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

View File

@ -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,12 @@ def get_from_scraper(scrape, request):
except Exception: except Exception:
pass pass
if recipe_json['source_url']:
automations = Automation.objects.filter(type=Automation.DESCRIPTION_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]):
recipe_json['description'] = re.sub(a.param_2, a.param_3, recipe_json['description'], count=1)
return recipe_json return recipe_json

View File

@ -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),
),
]

View File

@ -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)

View File

@ -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',)

View File

@ -72,3 +72,13 @@ 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():
# 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')
#
# with open(test_file, 'r', encoding='UTF-8') as d:
# pass

View File

@ -0,0 +1,39 @@
!!! 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
asd
## Description Replace
This automation is a bit more complicated than the alis rules.
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/)
# 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`

View File

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.2.1"
}
}

View File

@ -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 },

View 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>

File diff suppressed because it is too large Load Diff

View File

@ -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}

View File

@ -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
},
} }