mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-05 06:08:46 -05:00
Merge remote-tracking branch 'origin/feature/import-automation' into develop
This commit is contained in:
@@ -28,7 +28,7 @@ class IngredientParser:
|
||||
self.food_aliases = c
|
||||
caches['default'].touch(FOOD_CACHE_KEY, 30)
|
||||
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
|
||||
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
||||
|
||||
@@ -37,7 +37,7 @@ class IngredientParser:
|
||||
self.unit_aliases = c
|
||||
caches['default'].touch(UNIT_CACHE_KEY, 30)
|
||||
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
|
||||
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
||||
else:
|
||||
@@ -59,7 +59,7 @@ class IngredientParser:
|
||||
except KeyError:
|
||||
return food
|
||||
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 food
|
||||
|
||||
@@ -78,7 +78,7 @@ class IngredientParser:
|
||||
except KeyError:
|
||||
return unit
|
||||
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 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.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
|
||||
|
||||
@@ -121,7 +122,7 @@ def get_from_scraper(scrape, request):
|
||||
try:
|
||||
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
||||
except Exception:
|
||||
pass
|
||||
recipe_json['source_url'] = ''
|
||||
|
||||
try:
|
||||
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:
|
||||
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
|
||||
|
||||
if len(parse_description(description)) > 256: # split at 256 as long descriptions dont look good on recipe cards
|
||||
recipe_json['steps'][0]['instruction'] = f'*{parse_description(description)}* \n\n' + recipe_json['steps'][0]['instruction']
|
||||
parsed_description = parse_description(description)
|
||||
# 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:
|
||||
recipe_json['description'] = parse_description(description)[:512]
|
||||
recipe_json['description'] = parsed_description[:512]
|
||||
|
||||
try:
|
||||
for x in scrape.ingredients():
|
||||
@@ -175,6 +184,13 @@ def get_from_scraper(scrape, request):
|
||||
except Exception:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||
default_unit = models.CharField(max_length=32, default='g')
|
||||
@@ -1223,9 +1223,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
||||
FOOD_ALIAS = 'FOOD_ALIAS'
|
||||
UNIT_ALIAS = 'UNIT_ALIAS'
|
||||
KEYWORD_ALIAS = 'KEYWORD_ALIAS'
|
||||
DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE'
|
||||
INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE'
|
||||
|
||||
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='')
|
||||
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_3 = models.CharField(max_length=128, blank=True, null=True)
|
||||
|
||||
order = models.IntegerField(default=1000)
|
||||
|
||||
disabled = models.BooleanField(default=False)
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -876,11 +876,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
value = value.quantize(
|
||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
return (
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# TODO remove once old shopping list
|
||||
@@ -1067,7 +1067,7 @@ class AutomationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Automation
|
||||
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',)
|
||||
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ def page_help(page_name):
|
||||
'view_shopping': 'https://docs.tandoor.dev/features/shopping/',
|
||||
'view_import': '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, '')
|
||||
|
||||
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 pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.tests.conftest import validate_recipe
|
||||
|
||||
from ._recipes import (ALLRECIPES, AMERICAS_TEST_KITCHEN, CHEF_KOCH, CHEF_KOCH2, COOKPAD,
|
||||
COOKS_COUNTRY, DELISH, FOOD_NETWORK, GIALLOZAFFERANO, JOURNAL_DES_FEMMES,
|
||||
MADAME_DESSERT, MARMITON, TASTE_OF_HOME, THE_SPRUCE_EATS, TUDOGOSTOSO)
|
||||
from ...models import Automation
|
||||
|
||||
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
||||
DATA_DIR = "cookbook/tests/other/test_data/"
|
||||
@@ -72,3 +75,35 @@ def test_recipe_import(arg, u1_s1):
|
||||
content_type='application/json')
|
||||
recipe = json.loads(response.content)['recipe_json']
|
||||
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'] == ''
|
||||
|
||||
Reference in New Issue
Block a user