Merge remote-tracking branch 'upstream/feature/vue3' into feature/vue3

This commit is contained in:
smilerz
2024-04-23 09:47:09 -05:00
186 changed files with 17219 additions and 1937 deletions

View File

@@ -185,7 +185,7 @@ class StepAdmin(admin.ModelAdmin):
@admin.display(description="Name")
def recipe_and_name(obj):
if not obj.recipe_set.exists():
return f"Orphaned Step{'':s if not obj.name else f': {obj.name}'}"
return f"Orphaned Step{'' if not obj.name else f': {obj.name}'}"
return f"{obj.recipe_set.first().name}: {obj.name}" if obj.name else obj.recipe_set.first().name
@@ -376,10 +376,17 @@ class ShareLinkAdmin(admin.ModelAdmin):
admin.site.register(ShareLink, ShareLinkAdmin)
@admin.action(description='Delete all properties with type')
def delete_properties_with_type(modeladmin, request, queryset):
for pt in queryset:
Property.objects.filter(property_type=pt).delete()
class PropertyTypeAdmin(admin.ModelAdmin):
search_fields = ('space',)
search_fields = ('name',)
list_display = ('id', 'space', 'name', 'fdc_id')
actions = [delete_properties_with_type]
admin.site.register(PropertyType, PropertyTypeAdmin)

View File

@@ -1,26 +1,35 @@
import logging
from logging import Logger
from typing import Dict, Tuple
from urllib.parse import urljoin
from homeassistant_api import Client, HomeassistantAPIError, Domain
from aiohttp import ClientError, request
from cookbook.connectors.connector import Connector
from cookbook.models import ShoppingListEntry, ConnectorConfig, Space
class HomeAssistant(Connector):
_domains_cache: dict[str, Domain]
_config: ConnectorConfig
_logger: Logger
_client: Client
def __init__(self, config: ConnectorConfig):
if not config.token or not config.url or not config.todo_entity:
raise ValueError("config for HomeAssistantConnector in incomplete")
self._domains_cache = dict()
if config.url[-1] != "/":
config.url += "/"
self._config = config
self._logger = logging.getLogger("connector.HomeAssistant")
self._client = Client(self._config.url, self._config.token, async_cache_session=False, use_async=True)
async def homeassistant_api_call(self, method: str, path: str, data: Dict) -> str:
headers = {
"Authorization": f"Bearer {self._config.token}",
"Content-Type": "application/json"
}
async with request(method, urljoin(self._config.url, path), headers=headers, json=data) as response:
response.raise_for_status()
return await response.json()
async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
if not self._config.on_shopping_list_entry_created_enabled:
@@ -28,15 +37,17 @@ class HomeAssistant(Connector):
item, description = _format_shopping_list_entry(shopping_list_entry)
todo_domain = self._domains_cache.get('todo')
try:
if todo_domain is None:
todo_domain = await self._client.async_get_domain('todo')
self._domains_cache['todo'] = todo_domain
logging.debug(f"adding {item=} to {self._config.name}")
logging.debug(f"pushing {item} to {self._config.name}")
await todo_domain.add_item(entity_id=self._config.todo_entity, item=item)
except HomeassistantAPIError as err:
data = {
"entity_id": self._config.todo_entity,
"item": item,
"description": description,
}
try:
await self.homeassistant_api_call("POST", "services/todo/add_item", data)
except ClientError as err:
self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}")
async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
@@ -48,24 +59,31 @@ class HomeAssistant(Connector):
if not self._config.on_shopping_list_entry_deleted_enabled:
return
item, description = _format_shopping_list_entry(shopping_list_entry)
if not hasattr(shopping_list_entry._state.fields_cache, "food"):
# Sometimes the food foreign key is not loaded, and we cant load it from an async process
self._logger.debug("required property was not present in ShoppingListEntry")
return
item, _ = _format_shopping_list_entry(shopping_list_entry)
logging.debug(f"removing {item=} from {self._config.name}")
data = {
"entity_id": self._config.todo_entity,
"item": item,
}
todo_domain = self._domains_cache.get('todo')
try:
if todo_domain is None:
todo_domain = await self._client.async_get_domain('todo')
self._domains_cache['todo'] = todo_domain
logging.debug(f"deleting {item} from {self._config.name}")
await todo_domain.remove_item(entity_id=self._config.todo_entity, item=item)
except HomeassistantAPIError as err:
self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}")
await self.homeassistant_api_call("POST", "services/todo/remove_item", data)
except ClientError as err:
# This error will always trigger if the item is not present/found
self._logger.debug(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}")
async def close(self) -> None:
await self._client.async_cache_session.close()
pass
def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry):
def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry) -> Tuple[str, str]:
item = shopping_list_entry.food.name
if shopping_list_entry.amount > 0:
item += f" ({shopping_list_entry.amount:.2f}".rstrip('0').rstrip('.')
@@ -76,10 +94,10 @@ def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry):
else:
item += ")"
description = "Imported by TandoorRecipes"
description = "From TandoorRecipes"
if shopping_list_entry.created_by.first_name and len(shopping_list_entry.created_by.first_name) > 0:
description += f", created by {shopping_list_entry.created_by.first_name}"
description += f", by {shopping_list_entry.created_by.first_name}"
else:
description += f", created by {shopping_list_entry.created_by.username}"
description += f", by {shopping_list_entry.created_by.username}"
return item, description

View File

@@ -0,0 +1,39 @@
# custom processing for schema
# reason: DRF writable nested needs ID's to decide if a nested object should be created or updated
# the API schema/client make ID's read only by default and strips them entirely in request objects (with COMPONENT_SPLIT_REQUEST enabled)
# change request objects (schema ends with Request) and response objects (just model name) to the following:
# response objects: id field is required but read only
# request objects: id field is optional but writable/included
# WARNING: COMPONENT_SPLIT_REQUEST must be enabled, if not schemas might be wrong
def custom_postprocessing_hook(result, generator, request, public):
for c in result['components']['schemas'].keys():
# handle schemas used by the client to do requests on the server
if c.strip().endswith('Request'):
# check if request schema has a corresponding response schema to avoid changing request schemas for models that end with the word Request
response_schema = None
if c.strip().replace('Request', '') in result['components']['schemas'].keys():
response_schema = c.strip().replace('Request', '')
elif c.strip().startswith('Patched') and c.strip().replace('Request', '').replace('Patched', '', 1) in result['components']['schemas'].keys():
response_schema = c.strip().replace('Request', '').replace('Patched', '', 1)
# if response schema exist update request schema to include writable, optional id
if response_schema and 'id' in result['components']['schemas'][response_schema]['properties']:
if 'id' not in result['components']['schemas'][c]['properties']:
result['components']['schemas'][c]['properties']['id'] = {'readOnly': False, 'type': 'integer'}
# this is probably never the case but make sure ID is not required anyway
if 'required' in result['components']['schemas'][c] and 'id' in result['components']['schemas'][c]['required']:
result['components']['schemas'][c]['required'].remove('id')
# handle all schemas returned by the server to the client
else:
if 'properties' in result['components']['schemas'][c] and 'id' in result['components']['schemas'][c]['properties']:
# make ID field not read only so it's not stripped from the request on the client
result['components']['schemas'][c]['properties']['id']['readOnly'] = True
# make ID field required because if an object has an id it should also always be returned
if 'required' not in result['components']['schemas'][c]:
result['components']['schemas'][c]['required'] = ['id']
else:
result['components']['schemas'][c]['required'].append('id')
return result

View File

@@ -15,12 +15,9 @@ from cookbook.models import Automation, Keyword, PropertyType
def get_from_scraper(scrape, request):
# converting the scrape_me object to the existing json format based on ld+json
# converting the scrape_html object to the existing json format based on ld+json
recipe_json = {
'steps': [],
'internal': True
}
recipe_json = {'steps': [], 'internal': True}
keywords = []
# assign source URL
@@ -157,11 +154,18 @@ def get_from_scraper(scrape, request):
# assign steps
try:
for i in parse_instructions(scrape.instructions()):
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients, })
recipe_json['steps'].append({
'instruction': i,
'ingredients': [],
'show_ingredients_table': request.user.userpreference.show_step_ingredients,
})
except Exception:
pass
if len(recipe_json['steps']) == 0:
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
recipe_json['steps'].append({
'instruction': '',
'ingredients': [],
})
recipe_json['description'] = recipe_json['description'][:512]
if len(recipe_json['description']) > 256: # split at 256 as long descriptions don't look good on recipe cards
@@ -182,20 +186,20 @@ def get_from_scraper(scrape, request):
'original_text': x
}
if unit:
ingredient['unit'] = {'name': unit, }
ingredient['unit'] = {
'name': unit,
}
recipe_json['steps'][0]['ingredients'].append(ingredient)
except Exception:
recipe_json['steps'][0]['ingredients'].append(
{
'amount': 0,
'unit': None,
'food': {
'name': x,
},
'note': '',
'original_text': x
}
)
recipe_json['steps'][0]['ingredients'].append({
'amount': 0,
'unit': None,
'food': {
'name': x,
},
'note': '',
'original_text': x
})
except Exception:
pass
@@ -248,14 +252,16 @@ def get_from_youtube_scraper(url, request):
'working_time': 0,
'waiting_time': 0,
'image': "",
'keywords': [{'name': kw.name, 'label': kw.name, 'id': kw.pk}],
'keywords': [{
'name': kw.name,
'label': kw.name,
'id': kw.pk
}],
'source_url': url,
'steps': [
{
'ingredients': [],
'instruction': ''
}
]
'steps': [{
'ingredients': [],
'instruction': ''
}]
}
try:
@@ -452,10 +458,7 @@ def normalize_string(string):
def iso_duration_to_minutes(string):
match = re.match(
r'P((?P<years>\d+)Y)?((?P<months>\d+)M)?((?P<weeks>\d+)W)?((?P<days>\d+)D)?T((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+)S)?',
string
).groupdict()
match = re.match(r'P((?P<years>\d+)Y)?((?P<months>\d+)M)?((?P<weeks>\d+)W)?((?P<days>\d+)D)?T((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+)S)?', string).groupdict()
return int(match['days'] or 0) * 24 * 60 + int(match['hours'] or 0) * 60 + int(match['minutes'] or 0)

View File

@@ -30,7 +30,7 @@ def text_scraper(text, url=None):
html=None,
url=None,
):
self.wild_mode = False
self.supported_only = False
self.meta_http_equiv = False
self.soup = BeautifulSoup(html, "html.parser")
self.url = url

View File

@@ -14,8 +14,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-21 14:39+0100\n"
"PO-Revision-Date: 2023-09-25 09:59+0000\n"
"Last-Translator: Matias Laporte <laportematias+weblate@gmail.com>\n"
"PO-Revision-Date: 2024-03-27 19:02+0000\n"
"Last-Translator: Axel Breiterman <axelbreiterman@gmail.com>\n"
"Language-Team: Spanish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/es/>\n"
"Language: es\n"
@@ -23,7 +23,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.15\n"
"X-Generator: Weblate 5.4.2\n"
#: .\cookbook\forms.py:45
msgid ""
@@ -97,14 +97,16 @@ msgid ""
"<a href=\"https://www.home-assistant.io/docs/authentication/#your-account-"
"profile\">Long Lived Access Token</a> for your HomeAssistant instance"
msgstr ""
"<a href=\"https://www.home-assistant.io/docs/authentication/#your-account-"
"profile\">Token de larga duración</a>para tu instancia de HomeAssistant"
#: .\cookbook\forms.py:193
msgid "Something like http://homeassistant.local:8123/api"
msgstr ""
msgstr "Algo similar a http://homeassistant.local:8123/api"
#: .\cookbook\forms.py:205
msgid "http://homeassistant.local:8123/api for example"
msgstr ""
msgstr "por ejemplo http://homeassistant.local:8123/api for example"
#: .\cookbook\forms.py:222 .\cookbook\views\edit.py:117
msgid "Storage"
@@ -279,7 +281,7 @@ msgstr "Ha alcanzado el número máximo de recetas para su espacio."
#: .\cookbook\helper\permission_helper.py:414
msgid "You have more users than allowed in your space."
msgstr ""
msgstr "Tenés mas usuarios que los permitidos en tu espacio"
#: .\cookbook\helper\recipe_url_import.py:304
#, fuzzy
@@ -309,7 +311,7 @@ msgstr "fermentar"
#: .\cookbook\helper\recipe_url_import.py:310
msgid "sous-vide"
msgstr ""
msgstr "sous-vide"
#: .\cookbook\helper\shopping_helper.py:150
msgid "You must supply a servings size"
@@ -318,7 +320,7 @@ msgstr "Debe proporcionar un tamaño de porción"
#: .\cookbook\helper\template_helper.py:95
#: .\cookbook\helper\template_helper.py:97
msgid "Could not parse template code."
msgstr ""
msgstr "No se pudo parsear el código de la planitlla."
#: .\cookbook\integration\copymethat.py:44
#: .\cookbook\integration\melarecipes.py:37
@@ -342,6 +344,8 @@ msgid ""
"An unexpected error occurred during the import. Please make sure you have "
"uploaded a valid file."
msgstr ""
"Ocurrió un error inesperado al importar. Por favor asegurate de haber subido "
"un archivo válido."
#: .\cookbook\integration\integration.py:217
msgid "The following recipes were ignored because they already existed:"
@@ -457,7 +461,7 @@ msgstr "Calorías"
#: .\cookbook\migrations\0190_auto_20230525_1506.py:20
msgid "kcal"
msgstr ""
msgstr "kcal"
#: .\cookbook\models.py:325
msgid ""

View File

@@ -11,8 +11,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-21 14:39+0100\n"
"PO-Revision-Date: 2024-03-03 23:19+0000\n"
"Last-Translator: M Ugur <mugurd@gmail.com>\n"
"PO-Revision-Date: 2024-04-01 22:04+0000\n"
"Last-Translator: atom karinca <atomkarinca@tutanota.com>\n"
"Language-Team: Turkish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/tr/>\n"
"Language: tr\n"
@@ -20,7 +20,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Weblate 4.15\n"
"X-Generator: Weblate 5.4.2\n"
#: .\cookbook\forms.py:45
msgid ""
@@ -63,42 +63,48 @@ msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
"Varolan tariflerden benzer isimli olanlar mükerrerliği engellemek için "
"gözardı edilecektir. Tümünü içeri aktarmak için bu kutucuğu işaretleyin."
#: .\cookbook\forms.py:143
msgid "Add your comment: "
msgstr ""
msgstr "Yorum ekleyin: "
#: .\cookbook\forms.py:151
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
msgstr "Dropbox için boş bırakın ve Nextcloud için uygulama şifresini girin."
#: .\cookbook\forms.py:154
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
msgstr "Nextcloud için boş bırakın ve Dropbox için API anahtarını girin."
#: .\cookbook\forms.py:160
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
msgstr ""
"Dropbox için boş bırakın ve Nextcloud için yalnızca ana URL'yi "
"girin(<code>/remote.php/webdav/</code> otomatik olarak eklenir)"
#: .\cookbook\forms.py:188
msgid ""
"<a href=\"https://www.home-assistant.io/docs/authentication/#your-account-"
"profile\">Long Lived Access Token</a> for your HomeAssistant instance"
msgstr ""
"HomeAssistant uygulamanız için <a href=\"https://www.home-assistant.io/docs/"
"authentication/#your-account-profile\">Uzun Süreli Erişim Anahtarı</a>"
#: .\cookbook\forms.py:193
msgid "Something like http://homeassistant.local:8123/api"
msgstr ""
msgstr "Örneğin http://homeassistant.local:8123/api"
#: .\cookbook\forms.py:205
msgid "http://homeassistant.local:8123/api for example"
msgstr ""
msgstr "http://homeassistant.local:8123/api örneğin"
#: .\cookbook\forms.py:222 .\cookbook\views\edit.py:117
msgid "Storage"
msgstr ""
msgstr "Depolama"
#: .\cookbook\forms.py:222
msgid "Active"
@@ -106,51 +112,60 @@ msgstr "Aktif"
#: .\cookbook\forms.py:226
msgid "Search String"
msgstr ""
msgstr "Arama Sorgusu"
#: .\cookbook\forms.py:246
msgid "File ID"
msgstr ""
msgstr "Dosya ID"
#: .\cookbook\forms.py:262
msgid "Maximum number of users for this space reached."
msgstr ""
msgstr "Bu alan için maksimum kullanıcı sayısına ulaşıldı."
#: .\cookbook\forms.py:268
msgid "Email address already taken!"
msgstr ""
msgstr "Email adresi zaten alınmış!"
#: .\cookbook\forms.py:275
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
msgstr ""
"Email adresi zorunlu değildir fakat verilmesi halinde davet linki "
"kullanıcıya gönderilecektir."
#: .\cookbook\forms.py:287
msgid "Name already taken."
msgstr ""
msgstr "İsim zaten alınmış."
#: .\cookbook\forms.py:298
msgid "Accept Terms and Privacy"
msgstr ""
msgstr "Koşulları ve Gizliliği Onayla"
#: .\cookbook\forms.py:332
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
msgstr ""
"Trigram benzerlik eşleşmesi kullanılması halinde aramanın ne kadar bulanık "
"olduğunu belirler (ör. düşük değerler daha fazla yazım hatasını gözardı "
"eder)."
#: .\cookbook\forms.py:340
msgid ""
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
"full description of choices."
msgstr ""
"Arama tipi metodunu seçin. Seçeneklerin tam açıklamasını görmek için <a "
"href=\"/docs/search/\">buraya</a> tıklayın."
#: .\cookbook\forms.py:341
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr ""
"Tarifleri düzenlerken ve içeri aktarırken birimler, anahtar kelimeler ve "
"malzemelerde bulanık eşleştirme kullan."
#: .\cookbook\forms.py:342
msgid ""

View File

@@ -975,7 +975,12 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
read_only_fields = ['id', 'name', 'description', 'image', 'keywords', 'working_time',
# TODO having these readonly fields makes "RecipeOverview.ts" (API Client) not generate the RecipeOverviewToJSON second else block which leads to errors when using the api
# TODO find a solution (maybe trough a custom schema) to have these fields readonly (to save performance) and generate a proper client (two serializers would probably do the trick)
# read_only_fields = ['id', 'name', 'description', 'image', 'keywords', 'working_time',
# 'waiting_time', 'created_by', 'created_at', 'updated_at',
# 'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent']
read_only_fields = ['image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent']

View File

@@ -4,10 +4,10 @@ import pytest
from django.contrib import auth
from django.test import RequestFactory
from django_scopes import scope
from recipe_scrapers import scrape_html
from cookbook.helper.automation_helper import AutomationEngine
from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.models import Automation
DATA_DIR = "cookbook/tests/other/test_data/"
@@ -73,12 +73,14 @@ def test_unit_automation(u1_s1, arg):
assert (automation.apply_unit_automation(arg[0]) == target_name) is True
@pytest.mark.parametrize("arg", [
[[1, 'egg', 'white'], '', [1, '', 'egg', 'white']],
[[1, 'Egg', 'white'], '', [1, '', 'Egg', 'white']],
[[1, 'êgg', 'white'], '', [1, 'êgg', 'white']],
[[1, 'egg', 'white'], 'whole', [1, 'whole', 'egg', 'white']],
])
@pytest.mark.parametrize(
"arg", [
[[1, 'egg', 'white'], '', [1, '', 'egg', 'white']],
[[1, 'Egg', 'white'], '', [1, '', 'Egg', 'white']],
[[1, 'êgg', 'white'], '', [1, 'êgg', 'white']],
[[1, 'egg', 'white'], 'whole', [1, 'whole', 'egg', 'white']],
]
)
def test_never_unit_automation(u1_s1, arg):
user = auth.get_user(u1_s1)
space = user.userspace_set.first().space
@@ -97,13 +99,15 @@ def test_never_unit_automation(u1_s1, arg):
['.*allrecipes.*', True],
['.*google.*', False],
])
@pytest.mark.parametrize("arg", [
[Automation.DESCRIPTION_REPLACE],
[Automation.INSTRUCTION_REPLACE],
[Automation.NAME_REPLACE],
[Automation.FOOD_REPLACE],
[Automation.UNIT_REPLACE],
])
@pytest.mark.parametrize(
"arg", [
[Automation.DESCRIPTION_REPLACE],
[Automation.INSTRUCTION_REPLACE],
[Automation.NAME_REPLACE],
[Automation.FOOD_REPLACE],
[Automation.UNIT_REPLACE],
]
)
def test_regex_automation(u1_s1, arg, source):
user = auth.get_user(u1_s1)
space = user.userspace_set.first().space
@@ -124,11 +128,13 @@ def test_regex_automation(u1_s1, arg, source):
assert (automation.apply_regex_replace_automation(fail, arg[0]) == target) == False
@pytest.mark.parametrize("arg", [
['second first', 'first second'],
['longer string second first longer string', 'longer string first second longer string'],
['second fails first', 'second fails first'],
])
@pytest.mark.parametrize(
"arg", [
['second first', 'first second'],
['longer string second first longer string', 'longer string first second longer string'],
['second fails first', 'second fails first'],
]
)
def test_transpose_automation(u1_s1, arg):
user = auth.get_user(u1_s1)
space = user.userspace_set.first().space
@@ -156,10 +162,11 @@ def test_url_import_regex_replace(u1_s1):
if 'cookbook' in os.getcwd():
test_file = os.path.join(os.getcwd(), 'other', 'test_data', recipe)
# TODO this catch doesn't really work depending on from where you start the test, must check for duplicate path sections
else:
test_file = os.path.join(os.getcwd(), 'cookbook', 'tests', 'other', 'test_data', recipe)
with open(test_file, 'r', encoding='UTF-8') as d:
scrape = text_scraper(text=d.read(), url="https://www.allrecipes.com")
scrape = scrape_html(html=d.read(), org_url="https://testrecipe.test", supported_only=False)
with scope(space=space):
for t in types:
Automation.objects.get_or_create(name=t, type=t, param_1='.*', param_2=find_text, param_3='', created_by=user, space=space)

View File

@@ -14,6 +14,7 @@ from zipfile import ZipFile
import requests
import validators
from PIL import UnidentifiedImageError
from django.contrib import messages
from django.contrib.auth.models import Group, User
from django.contrib.postgres.search import TrigramSimilarity
@@ -34,8 +35,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view, OpenApiExample, inline_serializer
from icalendar import Calendar, Event
from oauth2_provider.models import AccessToken
from PIL import UnidentifiedImageError
from recipe_scrapers import scrape_me
from recipe_scrapers import scrape_html
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
from requests.exceptions import MissingSchema
from rest_framework import decorators, status, viewsets
@@ -59,9 +59,9 @@ from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.open_data_importer import OpenDataImporter
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, CustomIsShared, CustomIsSpaceOwner, CustomIsUser, CustomIsGuest, CustomRecipePermission,
CustomTokenHasReadWriteScope, CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF, above_space_limit, group_required,
has_group_permission, is_space_owner, switch_user_active_space
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, CustomIsShared, CustomIsSpaceOwner, CustomIsUser, CustomIsGuest,
CustomRecipePermission, CustomTokenHasReadWriteScope, CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF, above_space_limit,
group_required, has_group_permission, is_space_owner, switch_user_active_space
)
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
@@ -188,8 +188,8 @@ class FuzzyFilterMixin(viewsets.ModelViewSet, ExtendedRecipeMixin):
if query is not None and query not in ["''", '']:
if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
if (
self.request.user.is_authenticated
and any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)])
self.request.user.is_authenticated
and any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)])
):
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
else:
@@ -639,8 +639,8 @@ class FoodViewSet(TreeMixin):
return JsonResponse(
{
'msg':
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. \
Configure your key in Tandoor using environment FDC_API_KEY variable.'
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. \
Configure your key in Tandoor using environment FDC_API_KEY variable.'
},
status=429,
json_dumps_params={'indent': 4})
@@ -685,13 +685,13 @@ class FoodViewSet(TreeMixin):
space=self.request.space,
))
properties = Property.objects.bulk_create(food_property_list, unique_fields=('space', 'property_type', ))
properties = Property.objects.bulk_create(food_property_list, unique_fields=('space', 'property_type',))
property_food_relation_list = []
for p in properties:
property_food_relation_list.append(Food.properties.through(food_id=food.id, property_id=p.pk))
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id', ))
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
return self.retrieve(request, pk)
except Exception:
@@ -1187,14 +1187,13 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
@extend_schema_view(list=extend_schema(parameters=[
OpenApiParameter(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), type=int, many=True),
OpenApiParameter(
name='checked',
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> \
- ''recent'' includes unchecked items and recently completed items.'),
type=str
),
OpenApiParameter(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), type=int),
OpenApiParameter(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), type=int),
OpenApiParameter(
name='checked',
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> \
- ''recent'' includes unchecked items and recently completed items.')
),
OpenApiParameter(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), type=int),
]))
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
queryset = ShoppingListEntry.objects
@@ -1501,8 +1500,8 @@ class RecipeUrlImportView(APIView):
else:
try:
if validators.url(url, public=True):
scrape = scrape_me(url_path=url, wild_mode=True)
html = requests.get(url).content
scrape = scrape_html(org_url=url, html=html, supported_only=False)
else:
return Response({'error': True, 'msg': _('Invalid Url')}, status=status.HTTP_400_BAD_REQUEST)
except NoSchemaFoundInWildMode: