mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-08 15:48:27 -05:00
Merge branch 'feature/importer_to_vue' into develop
# Conflicts: # vue/src/apps/RecipeView/RecipeView.vue
This commit is contained in:
@@ -155,11 +155,13 @@ class ImportExportBase(forms.Form):
|
|||||||
RECIPESAGE = 'RECIPESAGE'
|
RECIPESAGE = 'RECIPESAGE'
|
||||||
DOMESTICA = 'DOMESTICA'
|
DOMESTICA = 'DOMESTICA'
|
||||||
MEALMASTER = 'MEALMASTER'
|
MEALMASTER = 'MEALMASTER'
|
||||||
|
MELARECIPES = 'MELARECIPES'
|
||||||
REZKONV = 'REZKONV'
|
REZKONV = 'REZKONV'
|
||||||
OPENEATS = 'OPENEATS'
|
OPENEATS = 'OPENEATS'
|
||||||
PLANTOEAT = 'PLANTOEAT'
|
PLANTOEAT = 'PLANTOEAT'
|
||||||
COOKBOOKAPP = 'COOKBOOKAPP'
|
COOKBOOKAPP = 'COOKBOOKAPP'
|
||||||
COPYMETHAT = 'COPYMETHAT'
|
COPYMETHAT = 'COPYMETHAT'
|
||||||
|
COOKMATE = 'COOKMATE'
|
||||||
PDF = 'PDF'
|
PDF = 'PDF'
|
||||||
|
|
||||||
type = forms.ChoiceField(choices=(
|
type = forms.ChoiceField(choices=(
|
||||||
@@ -167,7 +169,8 @@ class ImportExportBase(forms.Form):
|
|||||||
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'),
|
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'),
|
||||||
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
||||||
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'),
|
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
|
||||||
|
(COOKMATE, 'Cookmate')
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,11 +203,14 @@ class IngredientParser:
|
|||||||
def parse(self, x):
|
def parse(self, x):
|
||||||
# initialize default values
|
# initialize default values
|
||||||
amount = 0
|
amount = 0
|
||||||
unit = ''
|
unit = None
|
||||||
ingredient = ''
|
ingredient = ''
|
||||||
note = ''
|
note = ''
|
||||||
unit_note = ''
|
unit_note = ''
|
||||||
|
|
||||||
|
if len(x) == 0:
|
||||||
|
raise ValueError('string to parse cannot be empty')
|
||||||
|
|
||||||
# if the string contains parenthesis early on remove it and place it at the end
|
# if the string contains parenthesis early on remove it and place it at the end
|
||||||
# because its likely some kind of note
|
# because its likely some kind of note
|
||||||
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
|
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
|
||||||
@@ -271,4 +274,9 @@ class IngredientParser:
|
|||||||
|
|
||||||
if unit_note not in note:
|
if unit_note not in note:
|
||||||
note += ' ' + unit_note
|
note += ' ' + unit_note
|
||||||
return amount, self.apply_unit_automation(unit.strip()), self.apply_food_automation(ingredient.strip()), note.strip()
|
try:
|
||||||
|
unit = self.apply_unit_automation(unit.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return amount, unit, self.apply_food_automation(ingredient.strip()), note.strip()
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
"""
|
|
||||||
Source: https://djangosnippets.org/snippets/1703/
|
|
||||||
"""
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
@@ -12,7 +9,7 @@ from django.utils.translation import gettext as _
|
|||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
|
|
||||||
from cookbook.models import ShareLink
|
from cookbook.models import ShareLink, Recipe, UserPreference
|
||||||
|
|
||||||
|
|
||||||
def get_allowed_groups(groups_required):
|
def get_allowed_groups(groups_required):
|
||||||
@@ -262,3 +259,38 @@ class CustomIsShare(permissions.BasePermission):
|
|||||||
if share:
|
if share:
|
||||||
return share_link_valid(obj, share)
|
return share_link_valid(obj, share)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def above_space_limit(space): # TODO add file storage limit
|
||||||
|
"""
|
||||||
|
Test if the space has reached any limit (e.g. max recipes, users, ..)
|
||||||
|
:param space: Space to test for limits
|
||||||
|
:return: Tuple (True if above or equal any limit else false, message)
|
||||||
|
"""
|
||||||
|
r_limit, r_msg = above_space_recipe_limit(space)
|
||||||
|
u_limit, u_msg = above_space_user_limit(space)
|
||||||
|
return r_limit or u_limit, (r_msg + ' ' + u_msg).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def above_space_recipe_limit(space):
|
||||||
|
"""
|
||||||
|
Test if a space has reached its recipe limit
|
||||||
|
:param space: Space to test for limits
|
||||||
|
:return: Tuple (True if above or equal limit else false, message)
|
||||||
|
"""
|
||||||
|
limit = space.max_recipes != 0 and Recipe.objects.filter(space=space).count() >= space.max_recipes
|
||||||
|
if limit:
|
||||||
|
return True, _('You have reached the maximum number of recipes for your space.')
|
||||||
|
return False, ''
|
||||||
|
|
||||||
|
|
||||||
|
def above_space_user_limit(space):
|
||||||
|
"""
|
||||||
|
Test if a space has reached its user limit
|
||||||
|
:param space: Space to test for limits
|
||||||
|
:return: Tuple (True if above or equal limit else false, message)
|
||||||
|
"""
|
||||||
|
limit = space.max_users != 0 and UserPreference.objects.filter(space=space).count() > space.max_users
|
||||||
|
if limit:
|
||||||
|
return True, _('You have more users than allowed in your space.')
|
||||||
|
return False, ''
|
||||||
|
|||||||
@@ -58,18 +58,6 @@ def get_recipe_from_source(text, url, request):
|
|||||||
})
|
})
|
||||||
return kid_list
|
return kid_list
|
||||||
|
|
||||||
recipe_json = {
|
|
||||||
'name': '',
|
|
||||||
'url': '',
|
|
||||||
'description': '',
|
|
||||||
'image': '',
|
|
||||||
'keywords': [],
|
|
||||||
'recipeIngredient': [],
|
|
||||||
'recipeInstructions': '',
|
|
||||||
'servings': '',
|
|
||||||
'prepTime': '',
|
|
||||||
'cookTime': ''
|
|
||||||
}
|
|
||||||
recipe_tree = []
|
recipe_tree = []
|
||||||
parse_list = []
|
parse_list = []
|
||||||
html_data = []
|
html_data = []
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ from cookbook.helper.ingredient_parser import IngredientParser
|
|||||||
from cookbook.models import Keyword
|
from cookbook.models import Keyword
|
||||||
|
|
||||||
|
|
||||||
|
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||||
|
|
||||||
|
|
||||||
def get_from_scraper(scrape, request):
|
def get_from_scraper(scrape, request):
|
||||||
# converting the scrape_me object to the existing json format based on ld+json
|
# converting the scrape_me object to the existing json format based on ld+json
|
||||||
recipe_json = {}
|
recipe_json = {}
|
||||||
@@ -37,7 +40,7 @@ def get_from_scraper(scrape, request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
description = ''
|
description = ''
|
||||||
|
|
||||||
recipe_json['description'] = parse_description(description)
|
recipe_json['internal'] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
servings = scrape.yields() or None
|
servings = scrape.yields() or None
|
||||||
@@ -48,34 +51,31 @@ def get_from_scraper(scrape, request):
|
|||||||
servings = scrape.schema.data.get('recipeYield') or 1
|
servings = scrape.schema.data.get('recipeYield') or 1
|
||||||
except Exception:
|
except Exception:
|
||||||
servings = 1
|
servings = 1
|
||||||
if type(servings) != int:
|
|
||||||
try:
|
recipe_json['servings'] = parse_servings(servings)
|
||||||
servings = int(re.findall(r'\b\d+\b', servings)[0])
|
recipe_json['servings_text'] = parse_servings_text(servings)
|
||||||
except Exception:
|
|
||||||
servings = 1
|
|
||||||
recipe_json['servings'] = max(servings, 1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
recipe_json['prepTime'] = get_minutes(scrape.prep_time()) or 0
|
recipe_json['working_time'] = get_minutes(scrape.prep_time()) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
recipe_json['prepTime'] = get_minutes(scrape.schema.data.get("prepTime")) or 0
|
recipe_json['working_time'] = get_minutes(scrape.schema.data.get("prepTime")) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
recipe_json['prepTime'] = 0
|
recipe_json['working_time'] = 0
|
||||||
try:
|
try:
|
||||||
recipe_json['cookTime'] = get_minutes(scrape.cook_time()) or 0
|
recipe_json['waiting_time'] = get_minutes(scrape.cook_time()) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
recipe_json['cookTime'] = get_minutes(scrape.schema.data.get("cookTime")) or 0
|
recipe_json['waiting_time'] = get_minutes(scrape.schema.data.get("cookTime")) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
recipe_json['cookTime'] = 0
|
recipe_json['waiting_time'] = 0
|
||||||
|
|
||||||
if recipe_json['cookTime'] + recipe_json['prepTime'] == 0:
|
if recipe_json['working_time'] + recipe_json['waiting_time'] == 0:
|
||||||
try:
|
try:
|
||||||
recipe_json['prepTime'] = get_minutes(scrape.total_time()) or 0
|
recipe_json['working_time'] = get_minutes(scrape.total_time()) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
recipe_json['prepTime'] = get_minutes(scrape.schema.data.get("totalTime")) or 0
|
recipe_json['working_time'] = get_minutes(scrape.schema.data.get("totalTime")) or 0
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -113,6 +113,14 @@ def get_from_scraper(scrape, request):
|
|||||||
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
|
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if source_url := scrape.canonical_url():
|
||||||
|
recipe_json['source_url'] = source_url
|
||||||
|
try:
|
||||||
|
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
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)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -120,54 +128,49 @@ def get_from_scraper(scrape, request):
|
|||||||
|
|
||||||
ingredient_parser = IngredientParser(request, True)
|
ingredient_parser = IngredientParser(request, True)
|
||||||
|
|
||||||
ingredients = []
|
recipe_json['steps'] = []
|
||||||
|
|
||||||
|
for i in parse_instructions(scrape.instructions()):
|
||||||
|
recipe_json['steps'].append({'instruction': i, 'ingredients': [], })
|
||||||
|
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']
|
||||||
|
else:
|
||||||
|
recipe_json['description'] = parse_description(description)[:512]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for x in scrape.ingredients():
|
for x in scrape.ingredients():
|
||||||
try:
|
try:
|
||||||
amount, unit, ingredient, note = ingredient_parser.parse(x)
|
amount, unit, ingredient, note = ingredient_parser.parse(x)
|
||||||
ingredients.append(
|
ingredient = {
|
||||||
{
|
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'unit': {
|
'food': {
|
||||||
'text': unit,
|
'name': ingredient,
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
|
||||||
'ingredient': {
|
|
||||||
'text': ingredient,
|
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
},
|
||||||
|
'unit': None,
|
||||||
'note': note,
|
'note': note,
|
||||||
'original_text': x
|
'original_text': x
|
||||||
}
|
}
|
||||||
)
|
if unit:
|
||||||
|
ingredient['unit'] = {'name': unit, }
|
||||||
|
recipe_json['steps'][0]['ingredients'].append(ingredient)
|
||||||
except Exception:
|
except Exception:
|
||||||
ingredients.append(
|
recipe_json['steps'][0]['ingredients'].append(
|
||||||
{
|
{
|
||||||
'amount': 0,
|
'amount': 0,
|
||||||
'unit': {
|
'unit': None,
|
||||||
'text': '',
|
'food': {
|
||||||
'id': random.randrange(10000, 99999)
|
'name': x,
|
||||||
},
|
|
||||||
'ingredient': {
|
|
||||||
'text': x,
|
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
},
|
||||||
'note': '',
|
'note': '',
|
||||||
'original_text': x
|
'original_text': x
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
recipe_json['recipeIngredient'] = ingredients
|
|
||||||
except Exception:
|
except Exception:
|
||||||
recipe_json['recipeIngredient'] = ingredients
|
pass
|
||||||
|
|
||||||
try:
|
|
||||||
recipe_json['recipeInstructions'] = parse_instructions(scrape.instructions())
|
|
||||||
except Exception:
|
|
||||||
recipe_json['recipeInstructions'] = ""
|
|
||||||
|
|
||||||
if scrape.canonical_url():
|
|
||||||
recipe_json['url'] = scrape.canonical_url()
|
|
||||||
recipe_json['recipeInstructions'] += "\n\n" + _("Imported from") + ": " + scrape.canonical_url()
|
|
||||||
return recipe_json
|
return recipe_json
|
||||||
|
|
||||||
|
|
||||||
@@ -180,102 +183,46 @@ def parse_name(name):
|
|||||||
return normalize_string(name)
|
return normalize_string(name)
|
||||||
|
|
||||||
|
|
||||||
def parse_ingredients(ingredients):
|
|
||||||
# some pages have comma separated ingredients in a single array entry
|
|
||||||
try:
|
|
||||||
if type(ingredients[0]) == dict:
|
|
||||||
return ingredients
|
|
||||||
except (KeyError, IndexError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if (len(ingredients) == 1 and type(ingredients) == list):
|
|
||||||
ingredients = ingredients[0].split(',')
|
|
||||||
elif type(ingredients) == str:
|
|
||||||
ingredients = ingredients.split(',')
|
|
||||||
|
|
||||||
for x in ingredients:
|
|
||||||
if '\n' in x:
|
|
||||||
ingredients.remove(x)
|
|
||||||
for i in x.split('\n'):
|
|
||||||
ingredients.insert(0, i)
|
|
||||||
|
|
||||||
ingredient_list = []
|
|
||||||
|
|
||||||
for x in ingredients:
|
|
||||||
if x.replace(' ', '') != '':
|
|
||||||
x = x.replace('½', "0.5").replace('¼', "0.25").replace('¾', "0.75")
|
|
||||||
try:
|
|
||||||
amount, unit, ingredient, note = parse_single_ingredient(x)
|
|
||||||
if ingredient:
|
|
||||||
ingredient_list.append(
|
|
||||||
{
|
|
||||||
'amount': amount,
|
|
||||||
'unit': {
|
|
||||||
'text': unit,
|
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
|
||||||
'ingredient': {
|
|
||||||
'text': ingredient,
|
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
|
||||||
'note': note,
|
|
||||||
'original_text': x
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
ingredient_list.append(
|
|
||||||
{
|
|
||||||
'amount': 0,
|
|
||||||
'unit': {
|
|
||||||
'text': '',
|
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
|
||||||
'ingredient': {
|
|
||||||
'text': x,
|
|
||||||
'id': random.randrange(10000, 99999)
|
|
||||||
},
|
|
||||||
'note': '',
|
|
||||||
'original_text': x
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ingredients = ingredient_list
|
|
||||||
else:
|
|
||||||
ingredients = []
|
|
||||||
return ingredients
|
|
||||||
|
|
||||||
|
|
||||||
def parse_description(description):
|
def parse_description(description):
|
||||||
return normalize_string(description)
|
return normalize_string(description)
|
||||||
|
|
||||||
|
|
||||||
def parse_instructions(instructions):
|
def clean_instruction_string(instruction):
|
||||||
instruction_text = ''
|
normalized_string = normalize_string(instruction)
|
||||||
|
|
||||||
# flatten instructions if they are in a list
|
|
||||||
if type(instructions) == list:
|
|
||||||
for i in instructions:
|
|
||||||
if type(i) == str:
|
|
||||||
instruction_text += i
|
|
||||||
else:
|
|
||||||
if 'text' in i:
|
|
||||||
instruction_text += i['text'] + '\n\n'
|
|
||||||
elif 'itemListElement' in i:
|
|
||||||
for ile in i['itemListElement']:
|
|
||||||
if type(ile) == str:
|
|
||||||
instruction_text += ile + '\n\n'
|
|
||||||
elif 'text' in ile:
|
|
||||||
instruction_text += ile['text'] + '\n\n'
|
|
||||||
else:
|
|
||||||
instruction_text += str(i)
|
|
||||||
instructions = instruction_text
|
|
||||||
|
|
||||||
normalized_string = normalize_string(instructions)
|
|
||||||
normalized_string = normalized_string.replace('\n', ' \n')
|
normalized_string = normalized_string.replace('\n', ' \n')
|
||||||
normalized_string = normalized_string.replace(' \n \n', '\n\n')
|
normalized_string = normalized_string.replace(' \n \n', '\n\n')
|
||||||
return normalized_string
|
return normalized_string
|
||||||
|
|
||||||
|
|
||||||
|
def parse_instructions(instructions):
|
||||||
|
"""
|
||||||
|
Convert arbitrary instructions object from website import and turn it into a flat list of strings
|
||||||
|
:param instructions: any instructions object from import
|
||||||
|
:return: list of strings (from one to many elements depending on website)
|
||||||
|
"""
|
||||||
|
instruction_list = []
|
||||||
|
|
||||||
|
if type(instructions) == list:
|
||||||
|
for i in instructions:
|
||||||
|
if type(i) == str:
|
||||||
|
instruction_list.append(clean_instruction_string(i))
|
||||||
|
else:
|
||||||
|
if 'text' in i:
|
||||||
|
instruction_list.append(clean_instruction_string(i['text']))
|
||||||
|
elif 'itemListElement' in i:
|
||||||
|
for ile in i['itemListElement']:
|
||||||
|
if type(ile) == str:
|
||||||
|
instruction_list.append(clean_instruction_string(ile))
|
||||||
|
elif 'text' in ile:
|
||||||
|
instruction_list.append(clean_instruction_string(ile['text']))
|
||||||
|
else:
|
||||||
|
instruction_list.append(clean_instruction_string(str(i)))
|
||||||
|
else:
|
||||||
|
instruction_list.append(clean_instruction_string(instructions))
|
||||||
|
|
||||||
|
return instruction_list
|
||||||
|
|
||||||
|
|
||||||
def parse_image(image):
|
def parse_image(image):
|
||||||
# check if list of images is returned, take first if so
|
# check if list of images is returned, take first if so
|
||||||
if not image:
|
if not image:
|
||||||
@@ -310,40 +257,31 @@ def parse_servings(servings):
|
|||||||
return servings
|
return servings
|
||||||
|
|
||||||
|
|
||||||
def parse_cooktime(cooktime):
|
def parse_servings_text(servings):
|
||||||
if type(cooktime) not in [int, float]:
|
if type(servings) == str:
|
||||||
try:
|
try:
|
||||||
cooktime = float(re.search(r'\d+', cooktime).group())
|
servings = re.sub("\d+", '', servings).strip()
|
||||||
|
except Exception:
|
||||||
|
servings = ''
|
||||||
|
return servings
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time(recipe_time):
|
||||||
|
if type(recipe_time) not in [int, float]:
|
||||||
|
try:
|
||||||
|
recipe_time = float(re.search(r'\d+', recipe_time).group())
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
try:
|
try:
|
||||||
cooktime = round(iso_parse_duration(cooktime).seconds / 60)
|
recipe_time = round(iso_parse_duration(recipe_time).seconds / 60)
|
||||||
except ISO8601Error:
|
except ISO8601Error:
|
||||||
try:
|
try:
|
||||||
if (type(cooktime) == list and len(cooktime) > 0):
|
if (type(recipe_time) == list and len(recipe_time) > 0):
|
||||||
cooktime = cooktime[0]
|
recipe_time = recipe_time[0]
|
||||||
cooktime = round(parse_duration(cooktime).seconds / 60)
|
recipe_time = round(parse_duration(recipe_time).seconds / 60)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
cooktime = 0
|
recipe_time = 0
|
||||||
|
|
||||||
return cooktime
|
return recipe_time
|
||||||
|
|
||||||
|
|
||||||
def parse_preptime(preptime):
|
|
||||||
if type(preptime) not in [int, float]:
|
|
||||||
try:
|
|
||||||
preptime = float(re.search(r'\d+', preptime).group())
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
preptime = round(iso_parse_duration(preptime).seconds / 60)
|
|
||||||
except ISO8601Error:
|
|
||||||
try:
|
|
||||||
if (type(preptime) == list and len(preptime) > 0):
|
|
||||||
preptime = preptime[0]
|
|
||||||
preptime = round(parse_duration(preptime).seconds / 60)
|
|
||||||
except AttributeError:
|
|
||||||
preptime = 0
|
|
||||||
|
|
||||||
return preptime
|
|
||||||
|
|
||||||
|
|
||||||
def parse_keywords(keyword_json, space):
|
def parse_keywords(keyword_json, space):
|
||||||
@@ -353,9 +291,9 @@ def parse_keywords(keyword_json, space):
|
|||||||
kw = normalize_string(kw)
|
kw = normalize_string(kw)
|
||||||
if len(kw) != 0:
|
if len(kw) != 0:
|
||||||
if k := Keyword.objects.filter(name=kw, space=space).first():
|
if k := Keyword.objects.filter(name=kw, space=space).first():
|
||||||
keywords.append({'id': str(k.id), 'text': str(k.name)})
|
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
|
||||||
else:
|
else:
|
||||||
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw})
|
keywords.append({'label': kw, 'name': kw})
|
||||||
|
|
||||||
return keywords
|
return keywords
|
||||||
|
|
||||||
|
|||||||
77
cookbook/integration/cookmate.py
Normal file
77
cookbook/integration/cookmate.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
|
from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text
|
||||||
|
from cookbook.integration.integration import Integration
|
||||||
|
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||||
|
|
||||||
|
|
||||||
|
class Cookmate(Integration):
|
||||||
|
|
||||||
|
def import_file_name_filter(self, zip_info_object):
|
||||||
|
return zip_info_object.filename.endswith('.xml')
|
||||||
|
|
||||||
|
def get_files_from_recipes(self, recipes, el, cookie):
|
||||||
|
raise NotImplementedError('Method not implemented in storage integration')
|
||||||
|
|
||||||
|
def get_recipe_from_file(self, file):
|
||||||
|
recipe_xml = file
|
||||||
|
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
name=recipe_xml.find('title').text.strip(),
|
||||||
|
created_by=self.request.user, internal=True, space=self.request.space)
|
||||||
|
|
||||||
|
if recipe_xml.find('preptime') is not None:
|
||||||
|
recipe.working_time = parse_time(recipe_xml.find('preptime').text.strip())
|
||||||
|
|
||||||
|
if recipe_xml.find('cooktime') is not None:
|
||||||
|
recipe.waiting_time = parse_time(recipe_xml.find('cooktime').text.strip())
|
||||||
|
|
||||||
|
if recipe_xml.find('quantity') is not None:
|
||||||
|
recipe.servings = parse_servings(recipe_xml.find('quantity').text.strip())
|
||||||
|
recipe.servings_text = parse_servings_text(recipe_xml.find('quantity').text.strip())
|
||||||
|
|
||||||
|
if recipe_xml.find('url') is not None:
|
||||||
|
recipe.source_url = recipe_xml.find('url').text.strip()
|
||||||
|
|
||||||
|
if recipe_xml.find('description') is not None: # description is a list of <li>'s with text
|
||||||
|
if len(recipe_xml.find('description')) > 0:
|
||||||
|
recipe.description = recipe_xml.find('description')[0].text[:512]
|
||||||
|
|
||||||
|
for step in recipe_xml.find('recipetext').getchildren():
|
||||||
|
step = Step.objects.create(
|
||||||
|
instruction=step.text.strip(), space=self.request.space,
|
||||||
|
)
|
||||||
|
recipe.steps.add(step)
|
||||||
|
|
||||||
|
ingredient_parser = IngredientParser(self.request, True)
|
||||||
|
|
||||||
|
for ingredient in recipe_xml.find('ingredient').getchildren():
|
||||||
|
if ingredient.text.strip() != '':
|
||||||
|
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
|
||||||
|
f = ingredient_parser.get_food(food)
|
||||||
|
u = ingredient_parser.get_unit(unit)
|
||||||
|
recipe.steps.first().ingredients.add(Ingredient.objects.create(
|
||||||
|
food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space,
|
||||||
|
))
|
||||||
|
|
||||||
|
if recipe_xml.find('imageurl') is not None:
|
||||||
|
try:
|
||||||
|
response = requests.get(recipe_xml.find('imageurl').text.strip())
|
||||||
|
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||||
|
except Exception as e:
|
||||||
|
print('failed to import image ', str(e))
|
||||||
|
|
||||||
|
recipe.save()
|
||||||
|
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
def get_file_from_recipe(self, recipe):
|
||||||
|
raise NotImplementedError('Method not implemented in storage integration')
|
||||||
@@ -32,7 +32,14 @@ class CopyMeThat(Integration):
|
|||||||
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
|
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
|
||||||
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
|
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
|
||||||
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
|
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
|
||||||
recipe.save()
|
recipe.description = (file.find("div ", {"id": "description"}).text.strip())[:512]
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if len(file.find("span", {"id": "starred"}).text.strip()) > 0:
|
||||||
|
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=_('Favorite'))[0])
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import traceback
|
|||||||
import uuid
|
import uuid
|
||||||
from io import BytesIO, StringIO
|
from io import BytesIO, StringIO
|
||||||
from zipfile import BadZipFile, ZipFile
|
from zipfile import BadZipFile, ZipFile
|
||||||
|
|
||||||
|
import lxml
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ from django.http import HttpResponse
|
|||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_scopes import scope
|
from django_scopes import scope
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
from cookbook.forms import ImportExportBase
|
from cookbook.forms import ImportExportBase
|
||||||
from cookbook.helper.image_processing import get_filetype, handle_image
|
from cookbook.helper.image_processing import get_filetype, handle_image
|
||||||
@@ -144,7 +147,7 @@ class Integration:
|
|||||||
il.imported_recipes += 1
|
il.imported_recipes += 1
|
||||||
il.save()
|
il.save()
|
||||||
import_zip.close()
|
import_zip.close()
|
||||||
elif '.zip' in f['name'] or '.paprikarecipes' in f['name']:
|
elif '.zip' in f['name'] or '.paprikarecipes' in f['name'] or '.mcb' in f['name']:
|
||||||
import_zip = ZipFile(f['file'])
|
import_zip = ZipFile(f['file'])
|
||||||
file_list = []
|
file_list = []
|
||||||
for z in import_zip.filelist:
|
for z in import_zip.filelist:
|
||||||
@@ -157,9 +160,16 @@ class Integration:
|
|||||||
file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
|
file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
|
||||||
il.total_recipes += len(file_list)
|
il.total_recipes += len(file_list)
|
||||||
|
|
||||||
|
if isinstance(self, cookbook.integration.cookmate.Cookmate):
|
||||||
|
new_file_list = []
|
||||||
|
for file in file_list:
|
||||||
|
new_file_list += etree.parse(BytesIO(import_zip.read(file.filename))).getroot().getchildren()
|
||||||
|
il.total_recipes = len(new_file_list)
|
||||||
|
file_list = new_file_list
|
||||||
|
|
||||||
for z in file_list:
|
for z in file_list:
|
||||||
try:
|
try:
|
||||||
if isinstance(z, Tag):
|
if not hasattr(z, 'filename'):
|
||||||
recipe = self.get_recipe_from_file(z)
|
recipe = self.get_recipe_from_file(z)
|
||||||
else:
|
else:
|
||||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||||
@@ -172,7 +182,7 @@ class Integration:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
|
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
|
||||||
import_zip.close()
|
import_zip.close()
|
||||||
elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name']:
|
elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
|
||||||
data_list = self.split_recipe_file(f['file'])
|
data_list = self.split_recipe_file(f['file'])
|
||||||
il.total_recipes += len(data_list)
|
il.total_recipes += len(data_list)
|
||||||
for d in data_list:
|
for d in data_list:
|
||||||
|
|||||||
83
cookbook/integration/melarecipes.py
Normal file
83
cookbook/integration/melarecipes.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from gettext import gettext as _
|
||||||
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
|
from cookbook.helper.recipe_url_import import parse_servings, parse_time
|
||||||
|
from cookbook.integration.integration import Integration
|
||||||
|
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||||
|
|
||||||
|
|
||||||
|
class MelaRecipes(Integration):
|
||||||
|
|
||||||
|
def split_recipe_file(self, file):
|
||||||
|
return [json.loads(file.getvalue().decode("utf-8"))]
|
||||||
|
|
||||||
|
def get_files_from_recipes(self, recipes, el, cookie):
|
||||||
|
raise NotImplementedError('Method not implemented in storage integration')
|
||||||
|
|
||||||
|
def get_recipe_from_file(self, file):
|
||||||
|
recipe_json = file
|
||||||
|
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
name=recipe_json['title'].strip(),
|
||||||
|
created_by=self.request.user, internal=True, space=self.request.space)
|
||||||
|
|
||||||
|
if 'yield' in recipe_json:
|
||||||
|
recipe.servings = parse_servings(recipe_json['yield'])
|
||||||
|
|
||||||
|
if 'cookTime' in recipe_json:
|
||||||
|
recipe.waiting_time = parse_time(recipe_json['cookTime'])
|
||||||
|
|
||||||
|
if 'prepTime' in recipe_json:
|
||||||
|
recipe.working_time = parse_time(recipe_json['prepTime'])
|
||||||
|
|
||||||
|
if 'favorite' in recipe_json and recipe_json['favorite']:
|
||||||
|
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=_('Favorite'))[0])
|
||||||
|
|
||||||
|
if 'categories' in recipe_json:
|
||||||
|
try:
|
||||||
|
for x in recipe_json['categories']:
|
||||||
|
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
instruction = ''
|
||||||
|
if 'text' in recipe_json:
|
||||||
|
instruction += f'*{recipe_json["text"].strip()}* \n'
|
||||||
|
|
||||||
|
if 'instructions' in recipe_json:
|
||||||
|
instruction += recipe_json["instructions"].strip() + ' \n'
|
||||||
|
|
||||||
|
if 'notes' in recipe_json:
|
||||||
|
instruction += recipe_json["notes"].strip() + ' \n'
|
||||||
|
|
||||||
|
if 'link' in recipe_json:
|
||||||
|
recipe.source_url = recipe_json['link']
|
||||||
|
|
||||||
|
step = Step.objects.create(
|
||||||
|
instruction=instruction, space=self.request.space,
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredient_parser = IngredientParser(self.request, True)
|
||||||
|
for ingredient in recipe_json['ingredients'].split('\n'):
|
||||||
|
if ingredient.strip() != '':
|
||||||
|
amount, unit, food, note = ingredient_parser.parse(ingredient)
|
||||||
|
f = ingredient_parser.get_food(food)
|
||||||
|
u = ingredient_parser.get_unit(unit)
|
||||||
|
step.ingredients.add(Ingredient.objects.create(
|
||||||
|
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
|
||||||
|
))
|
||||||
|
recipe.steps.add(step)
|
||||||
|
|
||||||
|
if recipe_json.get("images", None):
|
||||||
|
try:
|
||||||
|
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['images'][0])), filetype='.jpeg')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
def get_file_from_recipe(self, recipe):
|
||||||
|
raise NotImplementedError('Method not implemented in storage integration')
|
||||||
@@ -31,6 +31,9 @@ class NextcloudCookbook(Integration):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if 'url' in recipe_json:
|
||||||
|
recipe.source_url = recipe_json['url'].strip()
|
||||||
|
|
||||||
if 'recipeCategory' in recipe_json:
|
if 'recipeCategory' in recipe_json:
|
||||||
try:
|
try:
|
||||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=recipe_json['recipeCategory'])[0])
|
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=recipe_json['recipeCategory'])[0])
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from gettext import gettext as _
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
|
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text
|
||||||
from cookbook.integration.integration import Integration
|
from cookbook.integration.integration import Integration
|
||||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||||
|
|
||||||
@@ -26,10 +27,9 @@ class Paprika(Integration):
|
|||||||
recipe.description = '' if len(recipe_json['description'].strip()) > 500 else recipe_json['description'].strip()
|
recipe.description = '' if len(recipe_json['description'].strip()) > 500 else recipe_json['description'].strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if re.match(r'([0-9])+\s(.)*', recipe_json['servings']):
|
if 'servings' in recipe_json['servings']:
|
||||||
s = recipe_json['servings'].split(' ')
|
recipe.servings = parse_servings(recipe_json['servings'])
|
||||||
recipe.servings = s[0]
|
recipe.servings_text = parse_servings_text(recipe_json['servings'])
|
||||||
recipe.servings_text = s[1]
|
|
||||||
|
|
||||||
if len(recipe_json['cook_time'].strip()) > 0:
|
if len(recipe_json['cook_time'].strip()) > 0:
|
||||||
recipe.waiting_time = re.findall(r'\d+', recipe_json['cook_time'])[0]
|
recipe.waiting_time = re.findall(r'\d+', recipe_json['cook_time'])[0]
|
||||||
|
|||||||
18
cookbook/migrations/0173_recipe_source_url.py
Normal file
18
cookbook/migrations/0173_recipe_source_url.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-03-04 13:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0172_ingredient_original_text'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recipe',
|
||||||
|
name='source_url',
|
||||||
|
field=models.CharField(blank=True, default=None, max_length=1024, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -337,7 +337,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
||||||
shopping_recent_days = models.PositiveIntegerField(default=7)
|
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||||
csv_delim = models.CharField(max_length=2, default=",")
|
csv_delim = models.CharField(max_length=2, default=",")
|
||||||
csv_prefix = models.CharField(max_length=10, blank=True,)
|
csv_prefix = models.CharField(max_length=10, blank=True, )
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||||
@@ -533,7 +533,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
if food:
|
if food:
|
||||||
# if child inherit fields is preset children should be set to that, otherwise inherit this foods inherited fields
|
# if child inherit fields is preset children should be set to that, otherwise inherit this foods inherited fields
|
||||||
inherit = list((food.child_inherit_fields.all() or food.inherit_fields.all()).values('id', 'field'))
|
inherit = list((food.child_inherit_fields.all() or food.inherit_fields.all()).values('id', 'field'))
|
||||||
tree_filter = Q(path__startswith=food.path, space=space, depth=food.depth+1)
|
tree_filter = Q(path__startswith=food.path, space=space, depth=food.depth + 1)
|
||||||
else:
|
else:
|
||||||
inherit = list(space.food_inherit.all().values('id', 'field'))
|
inherit = list(space.food_inherit.all().values('id', 'field'))
|
||||||
tree_filter = Q(space=space)
|
tree_filter = Q(space=space)
|
||||||
@@ -593,6 +593,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
|||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
|
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
|
||||||
|
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
objects = ScopedManager(space='space')
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
@@ -663,9 +665,7 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
|
|||||||
servings = models.IntegerField(default=1)
|
servings = models.IntegerField(default=1)
|
||||||
servings_text = models.CharField(default='', blank=True, max_length=32)
|
servings_text = models.CharField(default='', blank=True, max_length=32)
|
||||||
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
|
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
|
||||||
storage = models.ForeignKey(
|
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
|
||||||
Storage, on_delete=models.PROTECT, blank=True, null=True
|
|
||||||
)
|
|
||||||
file_uid = models.CharField(max_length=256, default="", blank=True)
|
file_uid = models.CharField(max_length=256, default="", blank=True)
|
||||||
file_path = models.CharField(max_length=512, default="", blank=True)
|
file_path = models.CharField(max_length=512, default="", blank=True)
|
||||||
link = models.CharField(max_length=512, null=True, blank=True)
|
link = models.CharField(max_length=512, null=True, blank=True)
|
||||||
@@ -675,9 +675,9 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
|
|||||||
working_time = models.IntegerField(default=0)
|
working_time = models.IntegerField(default=0)
|
||||||
waiting_time = models.IntegerField(default=0)
|
waiting_time = models.IntegerField(default=0)
|
||||||
internal = models.BooleanField(default=False)
|
internal = models.BooleanField(default=False)
|
||||||
nutrition = models.ForeignKey(
|
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
|
||||||
NutritionInformation, blank=True, null=True, on_delete=models.CASCADE
|
|
||||||
)
|
source_url = models.CharField(max_length=1024, default=None, blank=True, null=True)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from rest_framework.exceptions import NotFound, ValidationError
|
|||||||
|
|
||||||
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
||||||
from cookbook.helper.HelperFunctions import str2bool
|
from cookbook.helper.HelperFunctions import str2bool
|
||||||
|
from cookbook.helper.permission_helper import above_space_limit
|
||||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
|
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
|
||||||
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, Keyword,
|
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, Keyword,
|
||||||
@@ -649,6 +650,12 @@ class RecipeSerializer(RecipeBaseSerializer):
|
|||||||
)
|
)
|
||||||
read_only_fields = ['image', 'created_by', 'created_at']
|
read_only_fields = ['image', 'created_by', 'created_at']
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
above_limit, msg = above_space_limit(self.context['request'].space)
|
||||||
|
if above_limit:
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
return super().validate(data)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
@@ -656,9 +663,12 @@ class RecipeSerializer(RecipeBaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class RecipeImageSerializer(WritableNestedModelSerializer):
|
class RecipeImageSerializer(WritableNestedModelSerializer):
|
||||||
|
image = serializers.ImageField(required=False, allow_null=True)
|
||||||
|
image_url = serializers.CharField(max_length=4096, required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
fields = ['image', ]
|
fields = ['image', 'image_url', ]
|
||||||
|
|
||||||
|
|
||||||
class RecipeImportSerializer(SpacedModelSerializer):
|
class RecipeImportSerializer(SpacedModelSerializer):
|
||||||
@@ -968,12 +978,19 @@ class AutomationSerializer(serializers.ModelSerializer):
|
|||||||
# CORS, REST and Scopes aren't currently working
|
# CORS, REST and Scopes aren't currently working
|
||||||
# Scopes are evaluating before REST has authenticated the user assigning a None space
|
# Scopes are evaluating before REST has authenticated the user assigning a None space
|
||||||
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
|
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
|
||||||
class BookmarkletImportSerializer(serializers.ModelSerializer):
|
class BookmarkletImportListSerializer(serializers.ModelSerializer):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
validated_data['space'] = self.context['request'].user.userpreference.space
|
validated_data['space'] = self.context['request'].user.userpreference.space
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BookmarkletImport
|
||||||
|
fields = ('id', 'url', 'created_by', 'created_at')
|
||||||
|
read_only_fields = ('created_by', 'space')
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkletImportSerializer(BookmarkletImportListSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BookmarkletImport
|
model = BookmarkletImport
|
||||||
fields = ('id', 'url', 'html', 'created_by', 'created_at')
|
fields = ('id', 'url', 'html', 'created_by', 'created_at')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,6 +68,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$('#id_login').focus()
|
$('#id_login').focus()
|
||||||
|
$('#id_remember').prop('checked', true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load crispy_forms_filters %}
|
{% load render_bundle from webpack_loader %}
|
||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% load custom_tags %}
|
||||||
|
|
||||||
|
{% block title %}Test{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content_fluid %}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<import-view></import-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block title %}{% trans 'Import Recipes' %}{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
{{ form.media }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block script %}
|
||||||
<h2>{% trans 'Import' %}</h2>
|
{% if debug %}
|
||||||
<div class="row">
|
<script src="{% url 'js_reverse' %}"></script>
|
||||||
<div class="col col-md-12">
|
{% else %}
|
||||||
<form action="." method="post" enctype="multipart/form-data">
|
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||||
{% csrf_token %}
|
{% endif %}
|
||||||
{{ form|crispy }}
|
|
||||||
<button class="btn btn-success" type="submit"><i class="fas fa-file-import"></i> {% trans 'Import' %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||||
|
window.API_TOKEN = '{{ api_token }}'
|
||||||
|
window.BOOKMARKLET_IMPORT_ID = {{ bookmarklet_import_id }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% render_bundle 'import_view' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -136,7 +136,7 @@ def bookmarklet(request):
|
|||||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||||
api_token = Token.objects.create(user=request.user)
|
api_token = Token.objects.create(user=request.user)
|
||||||
|
|
||||||
bookmark = "javascript: \
|
bookmark = "<a href='javascript: \
|
||||||
(function(){ \
|
(function(){ \
|
||||||
if(window.bookmarkletTandoor!==undefined){ \
|
if(window.bookmarkletTandoor!==undefined){ \
|
||||||
bookmarkletTandoor(); \
|
bookmarkletTandoor(); \
|
||||||
@@ -146,8 +146,8 @@ def bookmarklet(request):
|
|||||||
localStorage.setItem('token', '" + api_token.__str__() + "'); \
|
localStorage.setItem('token', '" + api_token.__str__() + "'); \
|
||||||
document.body.appendChild(document.createElement(\'script\')).src=\'" \
|
document.body.appendChild(document.createElement(\'script\')).src=\'" \
|
||||||
+ server + prefix + static('js/bookmarklet.js') + "? \
|
+ server + prefix + static('js/bookmarklet.js') + "? \
|
||||||
r=\'+Math.floor(Math.random()*999999999);}})();"
|
r=\'+Math.floor(Math.random()*999999999);}})();'>Test</a>"
|
||||||
return re.sub(r"[\n\t\s]*", "", bookmark)
|
return re.sub(r"[\n\t]*", "", bookmark)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import uuid
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
from annoying.decorators import ajax_request
|
from annoying.decorators import ajax_request
|
||||||
from annoying.functions import get_object_or_None
|
from annoying.functions import get_object_or_None
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -23,6 +24,7 @@ from django.utils.translation import gettext as _
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from icalendar import Calendar, Event
|
from icalendar import Calendar, Event
|
||||||
from recipe_scrapers import NoSchemaFoundInWildMode, WebsiteNotImplementedError, scrape_me
|
from recipe_scrapers import NoSchemaFoundInWildMode, WebsiteNotImplementedError, scrape_me
|
||||||
|
from requests.exceptions import MissingSchema
|
||||||
from rest_framework import decorators, status, viewsets
|
from rest_framework import decorators, status, viewsets
|
||||||
from rest_framework.exceptions import APIException, PermissionDenied
|
from rest_framework.exceptions import APIException, PermissionDenied
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
@@ -68,7 +70,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
|
|||||||
SupermarketCategorySerializer, SupermarketSerializer,
|
SupermarketCategorySerializer, SupermarketSerializer,
|
||||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||||
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
||||||
ViewLogSerializer, IngredientSimpleSerializer)
|
ViewLogSerializer, IngredientSimpleSerializer, BookmarkletImportListSerializer)
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -767,20 +769,33 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
serializer = self.serializer_class(obj, data=request.data, partial=True)
|
serializer = self.serializer_class(obj, data=request.data, partial=True)
|
||||||
|
|
||||||
if self.request.space.demo:
|
|
||||||
raise PermissionDenied(detail='Not available in demo', code=None)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
image = None
|
||||||
|
|
||||||
if serializer.validated_data == {}:
|
if 'image' in serializer.validated_data:
|
||||||
obj.image = None
|
image = obj.image
|
||||||
else:
|
elif 'image_url' in serializer.validated_data:
|
||||||
img, filetype = handle_image(request, obj.image)
|
try:
|
||||||
|
response = requests.get(serializer.validated_data['image_url'])
|
||||||
|
image = File(io.BytesIO(response.content))
|
||||||
|
print('test')
|
||||||
|
except UnidentifiedImageError as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
except MissingSchema as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
if image is not None:
|
||||||
|
img, filetype = handle_image(request, image)
|
||||||
obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}')
|
obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}')
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
return Response(serializer.errors, 400)
|
return Response(serializer.errors, 400)
|
||||||
|
|
||||||
# TODO: refactor API to use post/put/delete or leave as put and change VUE to use list_recipe after creating
|
# TODO: refactor API to use post/put/delete or leave as put and change VUE to use list_recipe after creating
|
||||||
@@ -959,6 +974,11 @@ class BookmarkletImportViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = BookmarkletImportSerializer
|
serializer_class = BookmarkletImportSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == 'list':
|
||||||
|
return BookmarkletImportListSerializer
|
||||||
|
return self.serializer_class
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.queryset.filter(space=self.request.space).all()
|
return self.queryset.filter(space=self.request.space).all()
|
||||||
|
|
||||||
@@ -1132,103 +1152,56 @@ def get_plan_ical(request, from_date, to_date):
|
|||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def recipe_from_source(request):
|
def recipe_from_source(request):
|
||||||
url = request.POST.get('url', None)
|
"""
|
||||||
data = request.POST.get('data', None)
|
function to retrieve a recipe from a given url or source string
|
||||||
mode = request.POST.get('mode', None)
|
:param request: standard request with additional post parameters
|
||||||
auto = request.POST.get('auto', 'true')
|
- url: url to use for importing recipe
|
||||||
|
- data: if no url is given recipe is imported from provided source data
|
||||||
|
- (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes
|
||||||
|
:return: JsonResponse containing the parsed json, original html,json and images
|
||||||
|
"""
|
||||||
|
request_payload = json.loads(request.body.decode('utf-8'))
|
||||||
|
url = request_payload.get('url', None)
|
||||||
|
data = request_payload.get('data', None)
|
||||||
|
bookmarklet = request_payload.get('bookmarklet', None)
|
||||||
|
|
||||||
HEADERS = {
|
if bookmarklet := BookmarkletImport.objects.filter(pk=bookmarklet).first():
|
||||||
"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7"
|
url = bookmarklet.url
|
||||||
}
|
data = bookmarklet.html
|
||||||
|
bookmarklet.delete()
|
||||||
|
|
||||||
if (not url and not data) or (mode == 'url' and not url) or (mode == 'source' and not data):
|
# headers to use for request to external sites
|
||||||
return JsonResponse(
|
external_request_headers = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7"}
|
||||||
{
|
|
||||||
|
if not url and not data:
|
||||||
|
return JsonResponse({
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Nothing to do.')
|
'msg': _('Nothing to do.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
if mode == 'url' and auto == 'true':
|
# in manual mode request complete page to return it later
|
||||||
|
if url:
|
||||||
try:
|
try:
|
||||||
scrape = scrape_me(url)
|
data = requests.get(url, headers=external_request_headers).content
|
||||||
except (WebsiteNotImplementedError, AttributeError):
|
|
||||||
try:
|
|
||||||
scrape = scrape_me(url, wild_mode=True)
|
|
||||||
except NoSchemaFoundInWildMode:
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
'error': True,
|
|
||||||
'msg': _('The requested site provided malformed data and cannot be read.') # noqa: E501
|
|
||||||
},
|
|
||||||
status=400)
|
|
||||||
except ConnectionError:
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
'error': True,
|
|
||||||
'msg': _('The requested page could not be found.')
|
|
||||||
},
|
|
||||||
status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
instructions = scrape.instructions()
|
|
||||||
except Exception:
|
|
||||||
instructions = ""
|
|
||||||
try:
|
|
||||||
ingredients = scrape.ingredients()
|
|
||||||
except Exception:
|
|
||||||
ingredients = []
|
|
||||||
if len(ingredients) + len(instructions) == 0:
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
'error': True,
|
|
||||||
'msg': _(
|
|
||||||
'The requested site does not provide any recognized data format to import the recipe from.')
|
|
||||||
# noqa: E501
|
|
||||||
},
|
|
||||||
status=400)
|
|
||||||
else:
|
|
||||||
return JsonResponse({"recipe_json": get_from_scraper(scrape, request)})
|
|
||||||
elif (mode == 'source') or (mode == 'url' and auto == 'false'):
|
|
||||||
if not data or data == 'undefined':
|
|
||||||
try:
|
|
||||||
data = requests.get(url, headers=HEADERS).content
|
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Connection Refused.')
|
'msg': _('Connection Refused.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
recipe_json, recipe_tree, recipe_html, recipe_images = get_recipe_from_source(data, url, request)
|
||||||
)
|
|
||||||
recipe_json, recipe_tree, recipe_html, images = get_recipe_from_source(data, url, request)
|
|
||||||
if len(recipe_tree) == 0 and len(recipe_json) == 0:
|
if len(recipe_tree) == 0 and len(recipe_json) == 0:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('No usable data could be found.')
|
'msg': _('No usable data could be found.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'recipe_tree': recipe_tree,
|
|
||||||
'recipe_json': recipe_json,
|
'recipe_json': recipe_json,
|
||||||
|
'recipe_tree': recipe_tree,
|
||||||
'recipe_html': recipe_html,
|
'recipe_html': recipe_html,
|
||||||
'images': images,
|
'recipe_images': list(dict.fromkeys(recipe_images)),
|
||||||
})
|
})
|
||||||
|
|
||||||
else:
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
'error': True,
|
|
||||||
'msg': _('I couldn\'t find anything to do.')
|
|
||||||
},
|
|
||||||
status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@group_required('admin')
|
@group_required('admin')
|
||||||
def get_backup(request):
|
def get_backup(request):
|
||||||
|
|||||||
@@ -1,41 +1,27 @@
|
|||||||
import json
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.http import HttpResponseRedirect
|
||||||
from django.core.files import File
|
|
||||||
from django.db.transaction import atomic
|
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.utils.translation import ngettext
|
from django.utils.translation import ngettext
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
from PIL import UnidentifiedImageError
|
from rest_framework.authtoken.models import Token
|
||||||
from requests.exceptions import MissingSchema
|
|
||||||
|
|
||||||
from cookbook.forms import BatchEditForm, SyncForm
|
from cookbook.forms import BatchEditForm, SyncForm
|
||||||
from cookbook.helper.image_processing import handle_image
|
from cookbook.helper.permission_helper import group_required, has_group_permission, above_space_limit
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
from cookbook.models import (Comment, Food, Keyword, Recipe, RecipeImport, Sync,
|
||||||
from cookbook.helper.permission_helper import group_required, has_group_permission
|
Unit, UserPreference, BookmarkletImport)
|
||||||
from cookbook.helper.recipe_url_import import parse_cooktime
|
|
||||||
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe, RecipeImport, Step, Sync,
|
|
||||||
Unit, UserPreference)
|
|
||||||
from cookbook.tables import SyncTable
|
from cookbook.tables import SyncTable
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def sync(request):
|
def sync(request):
|
||||||
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
|
limit, msg = above_space_limit(request.space)
|
||||||
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
|
if limit:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
messages.add_message(request, messages.WARNING, msg)
|
||||||
|
|
||||||
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
|
|
||||||
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
if request.space.demo or settings.HOSTED:
|
if request.space.demo or settings.HOSTED:
|
||||||
@@ -123,103 +109,21 @@ def batch_edit(request):
|
|||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
@atomic
|
|
||||||
def import_url(request):
|
def import_url(request):
|
||||||
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
|
limit, msg = above_space_limit(request.space)
|
||||||
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
|
if limit:
|
||||||
|
messages.add_message(request, messages.WARNING, msg)
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
|
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||||
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
|
api_token = Token.objects.create(user=request.user)
|
||||||
return HttpResponseRedirect(reverse('index'))
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
data = json.loads(request.body)
|
|
||||||
data['cookTime'] = parse_cooktime(data.get('cookTime', ''))
|
|
||||||
data['prepTime'] = parse_cooktime(data.get('prepTime', ''))
|
|
||||||
|
|
||||||
recipe = Recipe.objects.create(
|
|
||||||
name=data['name'],
|
|
||||||
description=data['description'],
|
|
||||||
waiting_time=data['cookTime'],
|
|
||||||
working_time=data['prepTime'],
|
|
||||||
servings=data['servings'],
|
|
||||||
internal=True,
|
|
||||||
created_by=request.user,
|
|
||||||
space=request.space,
|
|
||||||
)
|
|
||||||
|
|
||||||
step = Step.objects.create(
|
|
||||||
instruction=data['recipeInstructions'], space=request.space,
|
|
||||||
)
|
|
||||||
|
|
||||||
recipe.steps.add(step)
|
|
||||||
|
|
||||||
for kw in data['keywords']:
|
|
||||||
if data['all_keywords']: # do not remove this check :) https://github.com/vabene1111/recipes/issues/645
|
|
||||||
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
|
|
||||||
recipe.keywords.add(k)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
k = Keyword.objects.get(name=kw['text'], space=request.space)
|
|
||||||
recipe.keywords.add(k)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
ingredient_parser = IngredientParser(request, True)
|
|
||||||
for ing in data['recipeIngredient']:
|
|
||||||
original = ing.pop('original', None) or ing.pop('original_text', None)
|
|
||||||
ingredient = Ingredient(original_text=original, space=request.space, )
|
|
||||||
|
|
||||||
if food_text := ing['ingredient']['text'].strip():
|
|
||||||
ingredient.food = ingredient_parser.get_food(food_text)
|
|
||||||
|
|
||||||
if ing['unit']:
|
|
||||||
if unit_text := ing['unit']['text'].strip():
|
|
||||||
ingredient.unit = ingredient_parser.get_unit(unit_text)
|
|
||||||
|
|
||||||
# TODO properly handle no_amount recipes
|
|
||||||
if isinstance(ing['amount'], str):
|
|
||||||
try:
|
|
||||||
ingredient.amount = float(ing['amount'].replace(',', '.'))
|
|
||||||
except ValueError:
|
|
||||||
ingredient.no_amount = True
|
|
||||||
pass
|
|
||||||
elif isinstance(ing['amount'], float) \
|
|
||||||
or isinstance(ing['amount'], int):
|
|
||||||
ingredient.amount = ing['amount']
|
|
||||||
ingredient.note = ing['note'].strip() if 'note' in ing else ''
|
|
||||||
|
|
||||||
ingredient.save()
|
|
||||||
step.ingredients.add(ingredient)
|
|
||||||
|
|
||||||
if 'image' in data and data['image'] != '' and data['image'] is not None:
|
|
||||||
try:
|
|
||||||
response = requests.get(data['image'])
|
|
||||||
|
|
||||||
img, filetype = handle_image(request, File(BytesIO(response.content), name='image'))
|
|
||||||
recipe.image = File(
|
|
||||||
img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}'
|
|
||||||
)
|
|
||||||
recipe.save()
|
|
||||||
except UnidentifiedImageError as e:
|
|
||||||
print(e)
|
|
||||||
pass
|
|
||||||
except MissingSchema as e:
|
|
||||||
print(e)
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))
|
|
||||||
|
|
||||||
|
bookmarklet_import_id = -1
|
||||||
if 'id' in request.GET:
|
if 'id' in request.GET:
|
||||||
context = {'bookmarklet': request.GET.get('id', '')}
|
if bookmarklet_import := BookmarkletImport.objects.filter(id=request.GET['id']).first():
|
||||||
else:
|
bookmarklet_import_id = bookmarklet_import.pk
|
||||||
context = {}
|
|
||||||
|
|
||||||
return render(request, 'url_import.html', context)
|
return render(request, 'url_import.html', {'api_token': api_token, 'bookmarklet_import_id': bookmarklet_import_id})
|
||||||
|
|
||||||
|
|
||||||
class Object(object):
|
class Object(object):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.views.generic import UpdateView
|
|||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
from cookbook.forms import CommentForm, ExternalRecipeForm, MealPlanForm, StorageForm, SyncForm
|
from cookbook.forms import CommentForm, ExternalRecipeForm, MealPlanForm, StorageForm, SyncForm
|
||||||
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required
|
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required, above_space_limit
|
||||||
from cookbook.models import (Comment, MealPlan, MealType, Recipe, RecipeImport, Storage, Sync,
|
from cookbook.models import (Comment, MealPlan, MealType, Recipe, RecipeImport, Storage, Sync,
|
||||||
UserPreference)
|
UserPreference)
|
||||||
from cookbook.provider.dropbox import Dropbox
|
from cookbook.provider.dropbox import Dropbox
|
||||||
@@ -39,12 +39,9 @@ def convert_recipe(request, pk):
|
|||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def internal_recipe_update(request, pk):
|
def internal_recipe_update(request, pk):
|
||||||
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() > request.space.max_recipes: # TODO move to central helper function
|
limit, msg = above_space_limit(request.space)
|
||||||
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
|
if limit:
|
||||||
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
|
messages.add_message(request, messages.WARNING, msg)
|
||||||
|
|
||||||
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
|
|
||||||
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
|
|
||||||
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
|
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
|
||||||
|
|
||||||
recipe_instance = get_object_or_404(Recipe, pk=pk, space=request.space)
|
recipe_instance = get_object_or_404(Recipe, pk=pk, space=request.space)
|
||||||
|
|||||||
@@ -10,16 +10,18 @@ from django.urls import reverse
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from cookbook.forms import ExportForm, ImportExportBase, ImportForm
|
from cookbook.forms import ExportForm, ImportExportBase, ImportForm
|
||||||
from cookbook.helper.permission_helper import group_required
|
from cookbook.helper.permission_helper import group_required, above_space_limit
|
||||||
from cookbook.helper.recipe_search import RecipeSearch
|
from cookbook.helper.recipe_search import RecipeSearch
|
||||||
from cookbook.integration.cheftap import ChefTap
|
from cookbook.integration.cheftap import ChefTap
|
||||||
from cookbook.integration.chowdown import Chowdown
|
from cookbook.integration.chowdown import Chowdown
|
||||||
from cookbook.integration.cookbookapp import CookBookApp
|
from cookbook.integration.cookbookapp import CookBookApp
|
||||||
|
from cookbook.integration.cookmate import Cookmate
|
||||||
from cookbook.integration.copymethat import CopyMeThat
|
from cookbook.integration.copymethat import CopyMeThat
|
||||||
from cookbook.integration.default import Default
|
from cookbook.integration.default import Default
|
||||||
from cookbook.integration.domestica import Domestica
|
from cookbook.integration.domestica import Domestica
|
||||||
from cookbook.integration.mealie import Mealie
|
from cookbook.integration.mealie import Mealie
|
||||||
from cookbook.integration.mealmaster import MealMaster
|
from cookbook.integration.mealmaster import MealMaster
|
||||||
|
from cookbook.integration.melarecipes import MelaRecipes
|
||||||
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
|
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
|
||||||
from cookbook.integration.openeats import OpenEats
|
from cookbook.integration.openeats import OpenEats
|
||||||
from cookbook.integration.paprika import Paprika
|
from cookbook.integration.paprika import Paprika
|
||||||
@@ -74,16 +76,17 @@ def get_integration(request, export_type):
|
|||||||
return CopyMeThat(request, export_type)
|
return CopyMeThat(request, export_type)
|
||||||
if export_type == ImportExportBase.PDF:
|
if export_type == ImportExportBase.PDF:
|
||||||
return PDFexport(request, export_type)
|
return PDFexport(request, export_type)
|
||||||
|
if export_type == ImportExportBase.MELARECIPES:
|
||||||
|
return MelaRecipes(request, export_type)
|
||||||
|
if export_type == ImportExportBase.COOKMATE:
|
||||||
|
return Cookmate(request, export_type)
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def import_recipe(request):
|
def import_recipe(request):
|
||||||
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
|
limit, msg = above_space_limit(request.space)
|
||||||
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
|
if limit:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
messages.add_message(request, messages.WARNING, msg)
|
||||||
|
|
||||||
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
|
|
||||||
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -100,7 +103,7 @@ def import_recipe(request):
|
|||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
return JsonResponse({'import_id': [il.pk]})
|
return JsonResponse({'import_id': il.pk})
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -662,11 +662,10 @@ def test(request):
|
|||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
with scopes_disabled():
|
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||||
result = ShoppingList.objects.filter(
|
api_token = Token.objects.create(user=request.user)
|
||||||
Q(created_by=request.user) | Q(shared=request.user)).filter(
|
|
||||||
space=request.space).values().distinct()
|
return render(request, 'test.html', {'api_token': api_token})
|
||||||
return JsonResponse(list(result), safe=False, json_dumps_params={'indent': 2})
|
|
||||||
|
|
||||||
|
|
||||||
def test2(request):
|
def test2(request):
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ Overview of the capabilities of the different integrations.
|
|||||||
| Plantoeat | ✔️ | ❌ | ✔ |
|
| Plantoeat | ✔️ | ❌ | ✔ |
|
||||||
| CookBookApp | ✔️ | ⌚ | ✔️ |
|
| CookBookApp | ✔️ | ⌚ | ✔️ |
|
||||||
| CopyMeThat | ✔️ | ❌ | ✔️ |
|
| CopyMeThat | ✔️ | ❌ | ✔️ |
|
||||||
|
| Melarecipes | ✔️ | ⌚ | ✔️ |
|
||||||
|
| Cookmate | ✔️ | ⌚ | ✔️ |
|
||||||
| PDF (experimental) | ⌚️ | ✔️ | ✔️ |
|
| PDF (experimental) | ⌚️ | ✔️ | ✔️ |
|
||||||
|
|
||||||
✔️ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
|
✔️ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
|
||||||
@@ -225,6 +227,19 @@ CookBookApp can export .zip files containing .html files. Upload the entire ZIP
|
|||||||
|
|
||||||
CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes.
|
CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes.
|
||||||
|
|
||||||
|
## Cookmate
|
||||||
|
Cookmate allows you to export a `.mcb` file which you can simply upload to tandoor and import all your recipes.
|
||||||
|
|
||||||
|
## RecetteTek
|
||||||
|
RecetteTek exports are `.rtk` files which can simply be uploaded to tandoor to import all your recipes.
|
||||||
|
|
||||||
|
## Melarecipes
|
||||||
|
|
||||||
|
Melarecipes provides multiple export formats but only the `MelaRecipes` format can export the complete collection.
|
||||||
|
Perform this export and open the `.melarecipes` file using your favorite archive opening program (e.g 7zip).
|
||||||
|
Repeat this if the file contains another `.melarecipes` file until you get a list of one or many `.melarecipe` files.
|
||||||
|
Upload all `.melarecipe` files you want to import to tandoor and start the import.
|
||||||
|
|
||||||
## PDF
|
## PDF
|
||||||
|
|
||||||
The PDF Exporter is an experimental feature that uses the puppeteer browser renderer to render each recipe and export it to PDF.
|
The PDF Exporter is an experimental feature that uses the puppeteer browser renderer to render each recipe and export it to PDF.
|
||||||
|
|||||||
0
openapitools.json
Normal file
0
openapitools.json
Normal file
671
vue/src/apps/ImportView/ImportView.vue
Normal file
671
vue/src/apps/ImportView/ImportView.vue
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<h2>{{ $t('Import') }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<b-tabs content-class="mt-3" v-model="tab_index">
|
||||||
|
<!-- URL Tab -->
|
||||||
|
<b-tab v-bind:title="$t('Website')" id="id_tab_url" active>
|
||||||
|
|
||||||
|
<!-- URL -->
|
||||||
|
<b-card no-body>
|
||||||
|
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||||
|
<b-col cols="12" md="6" offset="0" offset-md="3">
|
||||||
|
<b-button block v-b-toggle.id_accordion_url variant="info">Website</b-button>
|
||||||
|
<!-- TODO localize -->
|
||||||
|
</b-col>
|
||||||
|
</b-card-header>
|
||||||
|
<b-collapse id="id_accordion_url" visible accordion="url_import_accordion"
|
||||||
|
role="tabpanel" v-model="collapse_visible.url">
|
||||||
|
<div class="row justify-content-center p-2">
|
||||||
|
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 justify-content-cente">
|
||||||
|
<b-checkbox v-model="import_multiple" switch><span
|
||||||
|
v-if="import_multiple">Multiple Recipes</span><span
|
||||||
|
v-if="!import_multiple">Single Recipe</span></b-checkbox>
|
||||||
|
<!-- TODO localize -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<b-input-group class="mt-2" :class="{ bounce: empty_input }"
|
||||||
|
v-if="!import_multiple">
|
||||||
|
<b-input
|
||||||
|
class="form-control form-control-lg form-control-borderless form-control-search"
|
||||||
|
v-model="website_url"
|
||||||
|
placeholder="Website URL" @paste="onURLPaste"></b-input>
|
||||||
|
<!-- TODO localize -->
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="primary"
|
||||||
|
@click="loadRecipe(website_url,false,undefined)"><i
|
||||||
|
class="fas fa-search fa-fw"></i>
|
||||||
|
</b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
<b-textarea rows="10" placeholder="Enter one URL per line"
|
||||||
|
v-model="website_url_list"
|
||||||
|
v-if="import_multiple"> <!-- TODO localize -->
|
||||||
|
</b-textarea>
|
||||||
|
|
||||||
|
<b-button class="float-right" v-if="import_multiple"
|
||||||
|
:disabled="website_url_list.length < 1"
|
||||||
|
@click="autoImport()">Import
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<div class="row mt-2"> <!-- TODO remove -->
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<div v-if="!import_multiple">
|
||||||
|
<a href="#" @click="clearRecentImports()">Clear recent
|
||||||
|
imports</a>
|
||||||
|
<ul>
|
||||||
|
<li v-for="x in recent_urls" v-bind:key="x">
|
||||||
|
<a href="#"
|
||||||
|
@click="loadRecipe(x, false, undefined)">{{
|
||||||
|
x
|
||||||
|
}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-collapse>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<b-card no-body v-if="loading">
|
||||||
|
<loading-spinner></loading-spinner>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<!-- OPTIONS -->
|
||||||
|
<b-card no-body v-if="recipe_json !== undefined">
|
||||||
|
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||||
|
<b-col cols="12" md="6" offset="0" offset-md="3">
|
||||||
|
<b-button block v-b-toggle.id_accordion_add_options variant="info"
|
||||||
|
:disabled="recipe_json === undefined">Options
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-card-header>
|
||||||
|
<b-collapse id="id_accordion_add_options" accordion="url_import_accordion"
|
||||||
|
role="tabpanel" v-model="collapse_visible.options">
|
||||||
|
<b-card-body v-if="recipe_json !== undefined"
|
||||||
|
class="p-1 pb-md-5 pr-md-5 pl-md-5 pt-md-2">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 offset-0 offset-md-2">
|
||||||
|
<h4 class="text-center flex-grow-1" v-b-tooltip.hover.bottom
|
||||||
|
:title="$t('Click_To_Edit')" v-if="!edit_name"
|
||||||
|
@click="edit_name = true">{{
|
||||||
|
recipe_json.name
|
||||||
|
}} <span class="text-primary"><i class="fa fa-edit"></i> </span>
|
||||||
|
</h4>
|
||||||
|
<b-input-group v-if="edit_name" class="mb-2">
|
||||||
|
<b-input
|
||||||
|
class="form-control form-control-borderless form-control-search"
|
||||||
|
v-model="recipe_json.name"></b-input>
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="primary" @click="edit_name = false"><i
|
||||||
|
class="fas fa-save fa-fw"></i>
|
||||||
|
</b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12 text-center">
|
||||||
|
<b-img rounded fluid :src="recipe_json.image"
|
||||||
|
style="max-height: 30vh"></b-img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-1">
|
||||||
|
<div class="col col-md-12 text-center">
|
||||||
|
<small class="text-muted">Click the image you want to import for this
|
||||||
|
recipe</small> <!-- TODO localize -->
|
||||||
|
<span v-if="recipe_images.length === 0">No additional images found in source.</span>
|
||||||
|
<div class="scrolling-wrapper-flexbox">
|
||||||
|
<div class="wrapper-card" v-for="i in recipe_images"
|
||||||
|
v-bind:key="i"
|
||||||
|
@click="recipe_json.image = i">
|
||||||
|
<b-img rounded thumbnail fluid :src="i"
|
||||||
|
style="max-height: 10vh"
|
||||||
|
></b-img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2 mt-md-4">
|
||||||
|
<div class="col-12 col-md-6 offset-0 offset-md-3">
|
||||||
|
<b-card no-body>
|
||||||
|
<b-card-title>
|
||||||
|
<div class="clearfix">
|
||||||
|
<span class="float-left h5">Keywords</span>
|
||||||
|
<b-button-group class="float-right">
|
||||||
|
<b-button class="float-right" variant="primary"
|
||||||
|
@click="$set(recipe_json, 'keywords', recipe_json.keywords.map(x => {x.show = true; return x}))">
|
||||||
|
<i
|
||||||
|
class="fa fa-check-double"></i></b-button>
|
||||||
|
<b-button class="float-right" variant="secondary"
|
||||||
|
@click="$set(recipe_json, 'keywords', recipe_json.keywords.map(x => {x.show = false; return x}))">
|
||||||
|
<i
|
||||||
|
class="fa fa-times"></i></b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</div>
|
||||||
|
</b-card-title>
|
||||||
|
<b-card-body class="m-0 p-0 p-md-5">
|
||||||
|
<b-list-group>
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="(k, index) in recipe_json.keywords"
|
||||||
|
v-bind:key="k.name" style="cursor: pointer"
|
||||||
|
v-hover v-bind:class="{ 'bg-success': k.show }"
|
||||||
|
@click="k.show = (!k.show);$set(recipe_json.keywords, index, k)">
|
||||||
|
<div class="clearfix">
|
||||||
|
<span class="float-left">{{
|
||||||
|
k.label
|
||||||
|
}} </span>
|
||||||
|
<b-checkbox class="float-right" v-model="k.show"
|
||||||
|
disabled></b-checkbox>
|
||||||
|
</div>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</b-card-body>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<import-view-step-editor :recipe="recipe_json"
|
||||||
|
@change="recipe_json = $event"></import-view-step-editor>
|
||||||
|
|
||||||
|
</b-card-body>
|
||||||
|
</b-collapse>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<!-- IMPORT -->
|
||||||
|
<b-card no-body
|
||||||
|
v-if="recipe_json !== undefined || (imported_recipes.length > 0 || failed_imports.length>0)">
|
||||||
|
<b-card-header header-tag="header" class="p-1" role="tab">
|
||||||
|
<b-col cols="12" md="6" offset="0" offset-md="3">
|
||||||
|
<b-button block v-b-toggle.id_accordion_import variant="info"
|
||||||
|
:disabled="recipe_json === undefined">Import
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-card-header>
|
||||||
|
<b-collapse id="id_accordion_import" visible accordion="url_import_accordion"
|
||||||
|
role="tabpanel" v-model="collapse_visible.import">
|
||||||
|
<b-card-body class="text-center">
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="12" md="6" xl="4" offset="0" offset-md="3" offset-xl="4"
|
||||||
|
v-if="!import_multiple">
|
||||||
|
|
||||||
|
<recipe-card :recipe="recipe_json" :detailed="false"
|
||||||
|
:show_context_menu="false"
|
||||||
|
></recipe-card>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-list-group>
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="r in imported_recipes"
|
||||||
|
v-bind:key="r.id"
|
||||||
|
v-hover>
|
||||||
|
<div class="clearfix">
|
||||||
|
<a class="float-left" target="_blank"
|
||||||
|
rel="noreferrer nofollow"
|
||||||
|
:href="resolveDjangoUrl('view_recipe',r.id)">{{
|
||||||
|
r.name
|
||||||
|
}}</a>
|
||||||
|
<span class="float-right">Imported</span>
|
||||||
|
<!-- TODO localize -->
|
||||||
|
</div>
|
||||||
|
</b-list-group-item>
|
||||||
|
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="u in failed_imports"
|
||||||
|
v-bind:key="u"
|
||||||
|
v-hover>
|
||||||
|
<div class="clearfix">
|
||||||
|
<a class="float-left" target="_blank"
|
||||||
|
rel="noreferrer nofollow" :href="u">{{ u }}</a>
|
||||||
|
<span class="float-right">Failed</span>
|
||||||
|
<!-- TODO localize -->
|
||||||
|
</div>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-card-body>
|
||||||
|
<b-card-footer class="text-center">
|
||||||
|
<b-button-group>
|
||||||
|
<b-button @click="importRecipe('view')" v-if="!import_multiple">Import &
|
||||||
|
View
|
||||||
|
</b-button> <!-- TODO localize -->
|
||||||
|
<b-button @click="importRecipe('edit')" variant="success"
|
||||||
|
v-if="!import_multiple">Import & Edit
|
||||||
|
</b-button>
|
||||||
|
<b-button @click="importRecipe('import')" v-if="!import_multiple">Import &
|
||||||
|
Restart
|
||||||
|
</b-button>
|
||||||
|
<b-button @click="location.reload()">Restart
|
||||||
|
</b-button>
|
||||||
|
</b-button-group>
|
||||||
|
</b-card-footer>
|
||||||
|
</b-collapse>
|
||||||
|
</b-card>
|
||||||
|
</b-tab>
|
||||||
|
<!-- App Tab -->
|
||||||
|
<b-tab v-bind:title="$t('App')">
|
||||||
|
|
||||||
|
<select class="form-control" v-model="recipe_app">
|
||||||
|
<option v-for="i in INTEGRATIONS" :value="i.id" v-bind:key="i.id">{{ i.name }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<b-form-checkbox v-model="import_duplicates" name="check-button" switch
|
||||||
|
style="margin-top: 1vh">
|
||||||
|
{{ $t('import_duplicates') }}
|
||||||
|
</b-form-checkbox>
|
||||||
|
|
||||||
|
<a href="recipe_app_info.help_url"
|
||||||
|
v-if="recipe_app_info !== undefined && recipe_app_info.help_url !== ''">{{
|
||||||
|
$t('Help')
|
||||||
|
}}</a>
|
||||||
|
|
||||||
|
<b-form-file
|
||||||
|
class="my-2"
|
||||||
|
multiple
|
||||||
|
v-model="recipe_files"
|
||||||
|
:placeholder="$t('Select_File')"
|
||||||
|
drop-placeholder="Drop recipe files here...">
|
||||||
|
</b-form-file>
|
||||||
|
<button @click="importAppRecipe()" class="btn btn-primary shadow-none" type="button"
|
||||||
|
id="id_btn_app"><i class="fas fa-file-archive"></i> {{ $t('Import') }}
|
||||||
|
</button>
|
||||||
|
</b-tab>
|
||||||
|
<!-- Source Tab -->
|
||||||
|
<b-tab v-bind:title="$t('Source')">
|
||||||
|
|
||||||
|
<div class="input-group mt-4">
|
||||||
|
<b-textarea class="form-control input-group-append" v-model="source_data" rows=10
|
||||||
|
:placeholder="$t('paste_json')" style="font-size: 12px">
|
||||||
|
</b-textarea>
|
||||||
|
</div>
|
||||||
|
<b-button @click="loadRecipe('',false, undefined)" variant="primary"><i
|
||||||
|
class="fas fa-code"></i>
|
||||||
|
{{ $t('Import') }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
</b-tab>
|
||||||
|
<!-- Bookmarklet Tab -->
|
||||||
|
<b-tab v-bind:title="$t('Bookmarklet')">
|
||||||
|
<!-- TODO localize -->
|
||||||
|
Some pages cannot be imported from their URL, the Bookmarklet can be used to import from
|
||||||
|
some of them anyway.<br/>
|
||||||
|
1. Drag the following button to your bookmarks bar <a class="btn btn-outline-info btn-sm"
|
||||||
|
:href="makeBookmarklet()">Import into
|
||||||
|
Tandoor</a> <br/>
|
||||||
|
|
||||||
|
2. Open the page you want to import from <br/>
|
||||||
|
3. Click on the bookmark to perform the import <br/>
|
||||||
|
|
||||||
|
</b-tab>
|
||||||
|
|
||||||
|
</b-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue'
|
||||||
|
import {BootstrapVue} from 'bootstrap-vue'
|
||||||
|
|
||||||
|
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||||
|
|
||||||
|
import {resolveDjangoStatic, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils";
|
||||||
|
import axios from "axios";
|
||||||
|
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||||
|
import {INTEGRATIONS} from "@/utils/integration";
|
||||||
|
import ImportViewStepEditor from "@/apps/ImportView/ImportViewStepEditor";
|
||||||
|
import RecipeCard from "@/components/RecipeCard";
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
|
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ImportView',
|
||||||
|
mixins: [
|
||||||
|
ResolveUrlMixin,
|
||||||
|
ToastMixin,
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
LoadingSpinner,
|
||||||
|
RecipeCard,
|
||||||
|
ImportViewStepEditor
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
recipe_app_info: function () {
|
||||||
|
return this.INTEGRATIONS.filter(x => x.id === this.recipe_app)[0]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab_index: 0,
|
||||||
|
collapse_visible: {
|
||||||
|
url: true,
|
||||||
|
options: false,
|
||||||
|
advanced_options: false,
|
||||||
|
import: false,
|
||||||
|
},
|
||||||
|
// URL import
|
||||||
|
LS_IMPORT_RECENT: 'import_recent_urls', //TODO use central helper to manage all local storage keys (and maybe even access)
|
||||||
|
website_url: '',
|
||||||
|
website_url_list: 'https://madamedessert.de/schokoladenpudding-rezept-mit-echter-schokolade/\nhttps://www.essen-und-trinken.de/rezepte/58294-rzpt-schokoladenpudding\n' +
|
||||||
|
'https://www.chefkoch.de/rezepte/1825781296124455/Schokoladenpudding-selbst-gemacht.html\ntest.com\nhttps://bla.com',
|
||||||
|
|
||||||
|
import_multiple: false,
|
||||||
|
recent_urls: [],
|
||||||
|
source_data: '',
|
||||||
|
recipe_json: undefined,
|
||||||
|
recipe_html: undefined,
|
||||||
|
recipe_tree: undefined,
|
||||||
|
recipe_images: [],
|
||||||
|
imported_recipes: [],
|
||||||
|
failed_imports: [],
|
||||||
|
// App Import
|
||||||
|
INTEGRATIONS: INTEGRATIONS,
|
||||||
|
recipe_app: undefined,
|
||||||
|
import_duplicates: false,
|
||||||
|
recipe_files: [],
|
||||||
|
loading: false,
|
||||||
|
empty_input: false,
|
||||||
|
edit_name: false,
|
||||||
|
// Bookmarklet
|
||||||
|
BOOKMARKLET_CODE: window.BOOKMARKLET_CODE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let local_storage_recent = JSON.parse(window.localStorage.getItem(this.LS_IMPORT_RECENT))
|
||||||
|
this.recent_urls = local_storage_recent !== null ? local_storage_recent : []
|
||||||
|
this.tab_index = 0 //TODO add ability to pass open tab via get parameter
|
||||||
|
|
||||||
|
if (window.BOOKMARKLET_IMPORT_ID !== -1) {
|
||||||
|
this.loadRecipe('', false, window.BOOKMARKLET_IMPORT_ID)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Import recipe based on the data configured by the client
|
||||||
|
* @param action: action to perform after import (options are: edit, view, import)
|
||||||
|
* @param data: if parameter is passed ignore global application state and import form data variable
|
||||||
|
* @param silent do not show any messages for imports
|
||||||
|
*/
|
||||||
|
importRecipe: function (action, data, silent) {
|
||||||
|
if (this.recipe_json !== undefined) {
|
||||||
|
this.$set(this.recipe_json, 'keywords', this.recipe_json.keywords.filter(k => k.show))
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiFactory = new ApiApiFactory()
|
||||||
|
let recipe_json = data !== undefined ? data : this.recipe_json
|
||||||
|
if (recipe_json !== undefined) {
|
||||||
|
apiFactory.createRecipe(recipe_json).then(response => { // save recipe
|
||||||
|
let recipe = response.data
|
||||||
|
apiFactory.imageRecipe(response.data.id, undefined, recipe_json.image).then(response => { // save recipe image
|
||||||
|
if (!silent) {
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
|
}
|
||||||
|
this.afterImportAction(action, recipe)
|
||||||
|
}).catch(e => {
|
||||||
|
if (!silent) {
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||||
|
}
|
||||||
|
this.afterImportAction(action, recipe)
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
if (recipe_json.source_url !== '') {
|
||||||
|
this.failed_imports.push(recipe_json.source_url)
|
||||||
|
}
|
||||||
|
if (!silent) {
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('cant import recipe without data')
|
||||||
|
if (!silent) {
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Action performed after URL import
|
||||||
|
* @param action: action to perform after import
|
||||||
|
* edit: edit imported recipe
|
||||||
|
* view: view imported recipe
|
||||||
|
* import: restart the importer
|
||||||
|
* nothing: do nothing
|
||||||
|
* @param recipe: recipe that was imported
|
||||||
|
*/
|
||||||
|
afterImportAction: function (action, recipe) {
|
||||||
|
switch (action) {
|
||||||
|
case 'edit':
|
||||||
|
window.location = resolveDjangoUrl('edit_recipe', recipe.id)
|
||||||
|
break;
|
||||||
|
case 'view':
|
||||||
|
window.location = resolveDjangoUrl('view_recipe', recipe.id)
|
||||||
|
break;
|
||||||
|
case 'import':
|
||||||
|
location.reload();
|
||||||
|
break;
|
||||||
|
case 'multi_import':
|
||||||
|
this.imported_recipes.push(recipe)
|
||||||
|
break;
|
||||||
|
case 'nothing':
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Requests the recipe to be loaded form the source (url/data) from the server
|
||||||
|
* Updates all variables to contain what they need to render either simple preview or manual mapping mode
|
||||||
|
* @param url url to import (optional, empty string for bookmarklet imports)
|
||||||
|
* @param silent do not open the options tab after loading the recipe
|
||||||
|
* @param bookmarklet id of bookmarklet import to load instead of url, default undefined
|
||||||
|
*/
|
||||||
|
loadRecipe: function (url, silent, bookmarklet) {
|
||||||
|
// keep list of recently imported urls
|
||||||
|
if (url !== '') {
|
||||||
|
if (this.recent_urls.length > 5) {
|
||||||
|
this.recent_urls.pop()
|
||||||
|
}
|
||||||
|
if (this.recent_urls.filter(x => x === url).length === 0) {
|
||||||
|
this.recent_urls.push(url)
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(this.LS_IMPORT_RECENT, JSON.stringify(this.recent_urls))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === '' && bookmarklet === undefined) {
|
||||||
|
this.empty_input = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.empty_input = false
|
||||||
|
}, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
this.loading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset all variables
|
||||||
|
this.recipe_html = undefined
|
||||||
|
this.recipe_json = undefined
|
||||||
|
this.recipe_tree = undefined
|
||||||
|
this.recipe_images = []
|
||||||
|
|
||||||
|
// load recipe
|
||||||
|
let payload = {
|
||||||
|
'url': url,
|
||||||
|
'data': this.source_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookmarklet !== undefined) {
|
||||||
|
payload['bookmarklet'] = bookmarklet
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post(resolveDjangoUrl('api_recipe_from_source'), payload,).then((response) => {
|
||||||
|
this.loading = false
|
||||||
|
this.recipe_json = response.data['recipe_json'];
|
||||||
|
|
||||||
|
this.recipe_json.keywords.map(x => {
|
||||||
|
if (x.id === undefined) {
|
||||||
|
x.show = false
|
||||||
|
} else {
|
||||||
|
x.show = true
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
})
|
||||||
|
|
||||||
|
this.recipe_tree = response.data['recipe_tree'];
|
||||||
|
this.recipe_html = response.data['recipe_html'];
|
||||||
|
this.recipe_images = response.data['recipe_images'] !== undefined ? response.data['recipe_images'] : [];
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
this.tab_index = 0 // open tab 0 with import wizard
|
||||||
|
this.collapse_visible.options = true // open options collapse
|
||||||
|
}
|
||||||
|
return this.recipe_json
|
||||||
|
}).catch((err) => {
|
||||||
|
if (url !== '') {
|
||||||
|
this.failed_imports.push(url)
|
||||||
|
}
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH, err.response.data.msg)
|
||||||
|
throw "Load Recipe Error"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Function to automatically import multiple urls
|
||||||
|
* Takes input from website_url_list
|
||||||
|
*/
|
||||||
|
autoImport: function () {
|
||||||
|
this.collapse_visible.import = true
|
||||||
|
this.website_url_list.split('\n').forEach(r => {
|
||||||
|
this.loadRecipe(r, true, undefined).then((recipe_json) => {
|
||||||
|
this.importRecipe('multi_import', recipe_json, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.website_url_list = ''
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Import recipes with uploaded files and app integration
|
||||||
|
*/
|
||||||
|
importAppRecipe: function () {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('type', this.recipe_app);
|
||||||
|
formData.append('duplicates', this.import_duplicates)
|
||||||
|
for (let i = 0; i < this.recipe_files.length; i++) {
|
||||||
|
formData.append('files', this.recipe_files[i]);
|
||||||
|
}
|
||||||
|
axios.post(resolveDjangoUrl('view_import'), formData, {headers: {'Content-Type': 'multipart/form-data'}}).then((response) => {
|
||||||
|
window.location.href = resolveDjangoUrl('view_import_response', response.data['import_id'])
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Handles pasting URLs
|
||||||
|
*/
|
||||||
|
onURLPaste: function (evt) {
|
||||||
|
this.website_url = evt.clipboardData.getData('text')
|
||||||
|
this.loadRecipe(false, undefined)
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
/**loadRecipe(false,undefined)
|
||||||
|
* Clear list of recently imported recipe urls
|
||||||
|
*/
|
||||||
|
clearRecentImports: function () {
|
||||||
|
window.localStorage.setItem(this.LS_IMPORT_RECENT, JSON.stringify([]))
|
||||||
|
this.recent_urls = []
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Create the code required for the bookmarklet
|
||||||
|
* @returns {string} javascript:// protocol code to be loaded into href attribute of link that can be bookmarked
|
||||||
|
*/
|
||||||
|
makeBookmarklet: function () {
|
||||||
|
return 'javascript:(function(){' +
|
||||||
|
'if(window.bookmarkletTandoor!==undefined){' +
|
||||||
|
'bookmarkletTandoor();' +
|
||||||
|
'} else {' +
|
||||||
|
`localStorage.setItem("importURL", "${localStorage.getItem('BASE_PATH')}${this.resolveDjangoUrl('api:bookmarkletimport-list')}");` +
|
||||||
|
`localStorage.setItem("redirectURL", "${localStorage.getItem('BASE_PATH')}${this.resolveDjangoUrl('data_import_url')}");` +
|
||||||
|
`localStorage.setItem("token", "${window.API_TOKEN}");` +
|
||||||
|
`document.body.appendChild(document.createElement("script")).src="${localStorage.getItem('BASE_PATH')}${resolveDjangoStatic('/js/bookmarklet.js')}?r="+Math.floor(Math.random()*999999999)}` +
|
||||||
|
`})()`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
hover: {
|
||||||
|
inserted: function (el) {
|
||||||
|
el.addEventListener("mouseenter", () => {
|
||||||
|
el.classList.add("shadow")
|
||||||
|
})
|
||||||
|
el.addEventListener("mouseleave", () => {
|
||||||
|
el.classList.remove("shadow")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.bounce {
|
||||||
|
animation: bounce 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
transform: translate3d(-1px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20%,
|
||||||
|
80% {
|
||||||
|
transform: translate3d(2px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70% {
|
||||||
|
transform: translate3d(-4px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
40%,
|
||||||
|
60% {
|
||||||
|
transform: translate3d(4px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-wrapper-flexbox {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper-card {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
151
vue/src/apps/ImportView/ImportViewStepEditor.vue
Normal file
151
vue/src/apps/ImportView/ImportViewStepEditor.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="recipe_json !== undefined" class="mt-2 mt-md-0">
|
||||||
|
<h5>Steps</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12 text-center">
|
||||||
|
<b-button @click="splitAllSteps('\n')" variant="secondary" v-b-tooltip.hover :title="$t('Split_All_Steps')"><i
|
||||||
|
class="fas fa-expand-arrows-alt"></i> {{ $t('All') }}
|
||||||
|
</b-button>
|
||||||
|
<b-button @click="mergeAllSteps()" variant="primary" class="ml-1" v-b-tooltip.hover :title="$t('Combine_All_Steps')"><i
|
||||||
|
class="fas fa-compress-arrows-alt"></i> {{ $t('All') }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-2" v-for="(s, index) in recipe_json.steps"
|
||||||
|
v-bind:key="index">
|
||||||
|
<div class="col col-md-4 d-none d-md-block">
|
||||||
|
<draggable :list="s.ingredients" group="ingredients"
|
||||||
|
:empty-insert-threshold="10">
|
||||||
|
<b-list-group-item v-for="i in s.ingredients"
|
||||||
|
v-bind:key="i.original_text"><i
|
||||||
|
class="fas fa-arrows-alt"></i> {{ i.original_text }}
|
||||||
|
</b-list-group-item>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-8 col-12">
|
||||||
|
<b-input-group>
|
||||||
|
<b-textarea
|
||||||
|
style="white-space: pre-wrap" v-model="s.instruction"
|
||||||
|
max-rows="10"></b-textarea>
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="secondary" @click="splitStep(s,'\n')"><i
|
||||||
|
class="fas fa-expand-arrows-alt"></i></b-button>
|
||||||
|
<b-button variant="danger"
|
||||||
|
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s),1)">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
<div class="text-center mt-1">
|
||||||
|
<b-button @click="mergeStep(s)" variant="primary"
|
||||||
|
v-if="index + 1 < recipe_json.steps.length"><i
|
||||||
|
class="fas fa-compress-arrows-alt"></i>
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-button variant="success"
|
||||||
|
@click="recipe_json.steps.splice(recipe_json.steps.findIndex(x => x === s) +1,0,{ingredients:[], instruction: ''})">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import draggable from "vuedraggable";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ImportViewStepEditor",
|
||||||
|
components: {
|
||||||
|
draggable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
recipe: undefined
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recipe_json: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
recipe_json: function () {
|
||||||
|
this.$emit('change', this.recipe_json)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.recipe_json = this.recipe
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
|
||||||
|
* @param step: single step
|
||||||
|
* @param split_character: character to split steps at
|
||||||
|
* @return array of step objects
|
||||||
|
*/
|
||||||
|
splitStepObject: function (step, split_character) {
|
||||||
|
let steps = []
|
||||||
|
step.instruction.split(split_character).forEach(part => {
|
||||||
|
if (part.trim() !== '') {
|
||||||
|
steps.push({'instruction': part, 'ingredients': []})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
|
||||||
|
return steps
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
|
||||||
|
* @param split_character: character to split steps at
|
||||||
|
*/
|
||||||
|
splitAllSteps: function (split_character) {
|
||||||
|
let steps = []
|
||||||
|
this.recipe_json.steps.forEach(step => {
|
||||||
|
steps = steps.concat(this.splitStepObject(step, split_character))
|
||||||
|
})
|
||||||
|
this.recipe_json.steps = steps
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Splits the given step at the split character (e.g. \n or \n\n)
|
||||||
|
* @param step: step ingredients to split
|
||||||
|
* @param split_character: character to split steps at
|
||||||
|
*/
|
||||||
|
splitStep: function (step, split_character) {
|
||||||
|
let old_index = this.recipe_json.steps.findIndex(x => x === step)
|
||||||
|
let new_steps = this.splitStepObject(step, split_character)
|
||||||
|
this.recipe_json.steps.splice(old_index, 1, ...new_steps)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Merge all steps of a given recipe_json into one
|
||||||
|
*/
|
||||||
|
mergeAllSteps: function () {
|
||||||
|
let step = {'instruction': '', 'ingredients': []}
|
||||||
|
this.recipe_json.steps.forEach(s => {
|
||||||
|
step.instruction += s.instruction + '\n'
|
||||||
|
step.ingredients = step.ingredients.concat(s.ingredients)
|
||||||
|
})
|
||||||
|
this.recipe_json.steps = [step]
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Merge two steps (the given and next one)
|
||||||
|
*/
|
||||||
|
mergeStep: function (step) {
|
||||||
|
let step_index = this.recipe_json.steps.findIndex(x => x === step)
|
||||||
|
let removed_steps = this.recipe_json.steps.splice(step_index, 2)
|
||||||
|
|
||||||
|
this.recipe_json.steps.splice(step_index, 0, {
|
||||||
|
'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
|
||||||
|
'ingredients': removed_steps.flatMap(x => x.ingredients)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
18
vue/src/apps/ImportView/main.js
Normal file
18
vue/src/apps/ImportView/main.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './ImportView.vue'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||||
|
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
publicPath = 'http://localhost:8080/'
|
||||||
|
}
|
||||||
|
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||||
|
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
i18n,
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
||||||
@@ -370,6 +370,9 @@
|
|||||||
<div v-for="(ingredient, index) in step.ingredients"
|
<div v-for="(ingredient, index) in step.ingredients"
|
||||||
:key="ingredient.id">
|
:key="ingredient.id">
|
||||||
<hr class="d-md-none"/>
|
<hr class="d-md-none"/>
|
||||||
|
<div class="text-center">
|
||||||
|
<small class="text-muted"><i class="fas fa-globe"></i> {{ingredient.original_text}}</small>
|
||||||
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="flex-grow-0 handle align-self-start">
|
<div class="flex-grow-0 handle align-self-start">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|||||||
@@ -138,6 +138,9 @@
|
|||||||
@checked-state-changed="updateIngredientCheckedState"
|
@checked-state-changed="updateIngredientCheckedState"
|
||||||
></step-component>
|
></step-component>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h5 class="d-print-none"><i class="fas fa-file-import"></i> {{ $t("Imported_From") }}</h5>
|
||||||
|
<span class="text-muted mt-1"><a :href="recipe.source_url">{{ recipe.source_url }}</a></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
|
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="recipe.keywords.length > 0">
|
<div v-if="recipe.keywords.length > 0">
|
||||||
<span :key="k.id" v-for="k in recipe.keywords" class="pl-1">
|
<span :key="k.id" v-for="k in recipe.keywords.filter((kk) => { return kk.show || kk.show === undefined })" class="pl-1">
|
||||||
<a :href="`${resolveDjangoUrl('view_search')}?keyword=${k.id}`"><b-badge pill variant="light"
|
<a :href="`${resolveDjangoUrl('view_search')}?keyword=${k.id}`"><b-badge pill variant="light"
|
||||||
class="font-weight-normal">{{ k.label }}</b-badge></a>
|
class="font-weight-normal">{{ k.label }}</b-badge></a>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<b-card no-body v-hover v-if="recipe">
|
<b-card no-body v-hover v-if="recipe">
|
||||||
<a :href="clickUrl()">
|
<a :href="clickUrl()">
|
||||||
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
|
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
|
||||||
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right float-right text-right pt-2 pr-1">
|
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right float-right text-right pt-2 pr-1" v-if="show_context_menu">
|
||||||
<a>
|
<a>
|
||||||
<recipe-context-menu :recipe="recipe" class="float-right" v-if="recipe !== null"></recipe-context-menu>
|
<recipe-context-menu :recipe="recipe" class="float-right" v-if="recipe !== null"></recipe-context-menu>
|
||||||
</a>
|
</a>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<b-card-text style="text-overflow: ellipsis">
|
<b-card-text style="text-overflow: ellipsis">
|
||||||
<template v-if="recipe !== null">
|
<template v-if="recipe !== null">
|
||||||
<recipe-rating :recipe="recipe"></recipe-rating>
|
<recipe-rating :recipe="recipe"></recipe-rating>
|
||||||
<template v-if="recipe.description !== null">
|
<template v-if="recipe.description !== null && recipe.description !== undefined">
|
||||||
<span v-if="recipe.description.length > text_length">
|
<span v-if="recipe.description.length > text_length">
|
||||||
{{ recipe.description.substr(0, text_length) + "\u2026" }}
|
{{ recipe.description.substr(0, text_length) + "\u2026" }}
|
||||||
</span>
|
</span>
|
||||||
@@ -78,6 +78,7 @@ export default {
|
|||||||
footer_text: String,
|
footer_text: String,
|
||||||
footer_icon: String,
|
footer_icon: String,
|
||||||
detailed: { type: Boolean, default: true },
|
detailed: { type: Boolean, default: true },
|
||||||
|
show_context_menu: { type: Boolean, default: true }
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -336,6 +336,11 @@
|
|||||||
"ingredient_list": "Ingredient List",
|
"ingredient_list": "Ingredient List",
|
||||||
"explain": "Explain",
|
"explain": "Explain",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
|
"Website": "Website",
|
||||||
|
"App": "App",
|
||||||
|
"Bookmarklet": "Bookmarklet",
|
||||||
|
"import_duplicates": "To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.",
|
||||||
|
"paste_json": "Paste json or html source here to load recipe.",
|
||||||
"search_no_recipes": "Could not find any recipes!",
|
"search_no_recipes": "Could not find any recipes!",
|
||||||
"search_import_help_text": "Import a recipe from an external website or application.",
|
"search_import_help_text": "Import a recipe from an external website or application.",
|
||||||
"search_create_help_text": "Create a new recipe directly in Tandoor.",
|
"search_create_help_text": "Create a new recipe directly in Tandoor.",
|
||||||
|
|||||||
24
vue/src/utils/integration.js
Normal file
24
vue/src/utils/integration.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// containing all data and functions regarding the different integrations
|
||||||
|
|
||||||
|
export const INTEGRATIONS = [
|
||||||
|
{id: 'DEFAULT', name: "Tandoor", import: true, export: true, help_url: 'https://docs.tandoor.dev/features/import_export/#default'},
|
||||||
|
{id: 'CHEFTAP', name: "Cheftap", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#cheftap'},
|
||||||
|
{id: 'CHOWDOWN', name: "Chowdown", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#chowdown'},
|
||||||
|
{id: 'COOKBOOKAPP', name: "CookBookApp", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#cookbookapp'},
|
||||||
|
{id: 'COOKMATE', name: "Cookmate", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#cookmate'},
|
||||||
|
{id: 'COPYMETHAT', name: "CopyMeThat", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#copymethat'},
|
||||||
|
{id: 'DOMESTICA', name: "Domestica", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#domestica'},
|
||||||
|
{id: 'MEALIE', name: "Mealie", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#mealie'},
|
||||||
|
{id: 'MEALMASTER', name: "Mealmaster", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#mealmaster'},
|
||||||
|
{id: 'MELARECIPES', name: "Melarecipes", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#melarecipes'},
|
||||||
|
{id: 'NEXTCLOUD', name: "Nextcloud Cookbook", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#nextcloud'},
|
||||||
|
{id: 'OPENEATS', name: "Openeats", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#openeats'},
|
||||||
|
{id: 'PAPRIKA', name: "Paprika", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#paprika'},
|
||||||
|
{id: 'PEPPERPLATE', name: "Pepperplate", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#pepperplate'},
|
||||||
|
{id: 'PLANTOEAT', name: "Plantoeat", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#plantoeat'},
|
||||||
|
{id: 'RECETTETEK', name: "RecetteTek", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#recettetek'},
|
||||||
|
{id: 'RECIPEKEEPER', name: "Recipekeeper", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#recipekeeper'},
|
||||||
|
{id: 'RECIPESAGE', name: "Recipesage", import: true, export: true, help_url: 'https://docs.tandoor.dev/features/import_export/#recipesage'},
|
||||||
|
{id: 'REZKONV', name: "Rezkonv", import: true, export: false, help_url: 'https://docs.tandoor.dev/features/import_export/#rezkonv'},
|
||||||
|
{id: 'SAFRON', name: "Safron", import: true, export: true, help_url: 'https://docs.tandoor.dev/features/import_export/#safron'},
|
||||||
|
]
|
||||||
@@ -472,7 +472,7 @@ export interface FoodRecipe {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FoodRecipe
|
* @memberof FoodRecipe
|
||||||
*/
|
*/
|
||||||
name: string;
|
name?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -537,7 +537,7 @@ export interface FoodSubstitute {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof FoodSubstitute
|
* @memberof FoodSubstitute
|
||||||
*/
|
*/
|
||||||
name: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -752,12 +752,6 @@ export interface Ingredient {
|
|||||||
* @memberof Ingredient
|
* @memberof Ingredient
|
||||||
*/
|
*/
|
||||||
original_text?: string | null;
|
original_text?: string | null;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof Ingredient
|
|
||||||
*/
|
|
||||||
used_in_recipes?: string;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -1862,6 +1856,12 @@ export interface RecipeImage {
|
|||||||
* @memberof RecipeImage
|
* @memberof RecipeImage
|
||||||
*/
|
*/
|
||||||
image?: any | null;
|
image?: any | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof RecipeImage
|
||||||
|
*/
|
||||||
|
image_url?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -1923,12 +1923,6 @@ export interface RecipeIngredients {
|
|||||||
* @memberof RecipeIngredients
|
* @memberof RecipeIngredients
|
||||||
*/
|
*/
|
||||||
original_text?: string | null;
|
original_text?: string | null;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof RecipeIngredients
|
|
||||||
*/
|
|
||||||
used_in_recipes?: string;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -2197,7 +2191,7 @@ export interface RecipeSimple {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof RecipeSimple
|
* @memberof RecipeSimple
|
||||||
*/
|
*/
|
||||||
name: string;
|
name?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -5239,10 +5233,11 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
*
|
*
|
||||||
* @param {string} id A unique integer value identifying this recipe.
|
* @param {string} id A unique integer value identifying this recipe.
|
||||||
* @param {any} [image]
|
* @param {any} [image]
|
||||||
|
* @param {string} [imageUrl]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
imageRecipe: async (id: string, image?: any, options: any = {}): Promise<RequestArgs> => {
|
imageRecipe: async (id: string, image?: any, imageUrl?: string, options: any = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'id' is not null or undefined
|
// verify required parameter 'id' is not null or undefined
|
||||||
assertParamExists('imageRecipe', 'id', id)
|
assertParamExists('imageRecipe', 'id', id)
|
||||||
const localVarPath = `/api/recipe/{id}/image/`
|
const localVarPath = `/api/recipe/{id}/image/`
|
||||||
@@ -5264,6 +5259,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
|||||||
localVarFormParams.append('image', image as any);
|
localVarFormParams.append('image', image as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageUrl !== undefined) {
|
||||||
|
localVarFormParams.append('image_url', imageUrl as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
|
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
|
||||||
|
|
||||||
@@ -10353,11 +10352,12 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
|||||||
*
|
*
|
||||||
* @param {string} id A unique integer value identifying this recipe.
|
* @param {string} id A unique integer value identifying this recipe.
|
||||||
* @param {any} [image]
|
* @param {any} [image]
|
||||||
|
* @param {string} [imageUrl]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async imageRecipe(id: string, image?: any, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RecipeImage>> {
|
async imageRecipe(id: string, image?: any, imageUrl?: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RecipeImage>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.imageRecipe(id, image, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.imageRecipe(id, image, imageUrl, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -12186,11 +12186,12 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
|||||||
*
|
*
|
||||||
* @param {string} id A unique integer value identifying this recipe.
|
* @param {string} id A unique integer value identifying this recipe.
|
||||||
* @param {any} [image]
|
* @param {any} [image]
|
||||||
|
* @param {string} [imageUrl]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
imageRecipe(id: string, image?: any, options?: any): AxiosPromise<RecipeImage> {
|
imageRecipe(id: string, image?: any, imageUrl?: string, options?: any): AxiosPromise<RecipeImage> {
|
||||||
return localVarFp.imageRecipe(id, image, options).then((request) => request(axios, basePath));
|
return localVarFp.imageRecipe(id, image, imageUrl, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -14004,12 +14005,13 @@ export class ApiApi extends BaseAPI {
|
|||||||
*
|
*
|
||||||
* @param {string} id A unique integer value identifying this recipe.
|
* @param {string} id A unique integer value identifying this recipe.
|
||||||
* @param {any} [image]
|
* @param {any} [image]
|
||||||
|
* @param {string} [imageUrl]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
* @memberof ApiApi
|
* @memberof ApiApi
|
||||||
*/
|
*/
|
||||||
public imageRecipe(id: string, image?: any, options?: any) {
|
public imageRecipe(id: string, image?: any, imageUrl?: string, options?: any) {
|
||||||
return ApiApiFp(this.configuration).imageRecipe(id, image, options).then((request) => request(this.axios, this.basePath));
|
return ApiApiFp(this.configuration).imageRecipe(id, image, imageUrl, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,37 +50,37 @@ export class StandardToasts {
|
|||||||
static FAIL_MOVE = "FAIL_MOVE"
|
static FAIL_MOVE = "FAIL_MOVE"
|
||||||
static FAIL_MERGE = "FAIL_MERGE"
|
static FAIL_MERGE = "FAIL_MERGE"
|
||||||
|
|
||||||
static makeStandardToast(toast, err_details = undefined) {
|
static makeStandardToast(toast, err_details = undefined) { //TODO err_details render very ugly, improve this maybe by using a custom toast component (in conjunction with error logging maybe)
|
||||||
switch (toast) {
|
switch (toast) {
|
||||||
case StandardToasts.SUCCESS_CREATE:
|
case StandardToasts.SUCCESS_CREATE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_FETCH:
|
case StandardToasts.SUCCESS_FETCH:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_UPDATE:
|
case StandardToasts.SUCCESS_UPDATE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_DELETE:
|
case StandardToasts.SUCCESS_DELETE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_MOVE:
|
case StandardToasts.SUCCESS_MOVE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_MERGE:
|
case StandardToasts.SUCCESS_MERGE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_CREATE:
|
case StandardToasts.FAIL_CREATE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_FETCH:
|
case StandardToasts.FAIL_FETCH:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_UPDATE:
|
case StandardToasts.FAIL_UPDATE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_DELETE:
|
case StandardToasts.FAIL_DELETE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_DELETE_PROTECTED:
|
case StandardToasts.FAIL_DELETE_PROTECTED:
|
||||||
makeToast(i18n.tc("Protected"), i18n.tc("err_deleting_protected_resource"), "danger")
|
makeToast(i18n.tc("Protected"), i18n.tc("err_deleting_protected_resource"), "danger")
|
||||||
@@ -149,6 +149,27 @@ export function resolveDjangoUrl(url, params = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Utility functions to use djangos static files
|
||||||
|
* */
|
||||||
|
|
||||||
|
export const StaticMixin = {
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* access django static files from javascript
|
||||||
|
* @param {string} param path to static file
|
||||||
|
*/
|
||||||
|
resolveDjangoStatic: function (param) {
|
||||||
|
return resolveDjangoStatic(param)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDjangoStatic(param) {
|
||||||
|
let url = localStorage.getItem('STATIC_URL') + param
|
||||||
|
return url.replace('//','/') //replace // with / in case param started with / which resulted in // after the static base url
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* other utilities
|
* other utilities
|
||||||
* */
|
* */
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const pages = {
|
|||||||
entry: "./src/apps/OfflineView/main.js",
|
entry: "./src/apps/OfflineView/main.js",
|
||||||
chunks: ["chunk-vendors"],
|
chunks: ["chunk-vendors"],
|
||||||
},
|
},
|
||||||
|
import_view: {
|
||||||
|
entry: "./src/apps/ImportView/main.js",
|
||||||
|
chunks: ["chunk-vendors"],
|
||||||
|
},
|
||||||
import_response_view: {
|
import_response_view: {
|
||||||
entry: "./src/apps/ImportResponseView/main.js",
|
entry: "./src/apps/ImportResponseView/main.js",
|
||||||
chunks: ["chunk-vendors"],
|
chunks: ["chunk-vendors"],
|
||||||
|
|||||||
@@ -12697,6 +12697,11 @@ vue-infinite-loading@^2.4.5:
|
|||||||
resolved "https://registry.yarnpkg.com/vue-infinite-loading/-/vue-infinite-loading-2.4.5.tgz#cc20fd40af7f20188006443c99b60470cf1de1b3"
|
resolved "https://registry.yarnpkg.com/vue-infinite-loading/-/vue-infinite-loading-2.4.5.tgz#cc20fd40af7f20188006443c99b60470cf1de1b3"
|
||||||
integrity sha512-xhq95Mxun060bRnsOoLE2Be6BR7jYwuC89kDe18+GmCLVrRA/dU0jrGb12Xu6NjmKs+iTW0AA6saSEmEW4cR7g==
|
integrity sha512-xhq95Mxun060bRnsOoLE2Be6BR7jYwuC89kDe18+GmCLVrRA/dU0jrGb12Xu6NjmKs+iTW0AA6saSEmEW4cR7g==
|
||||||
|
|
||||||
|
vue-jstree@^2.1.6:
|
||||||
|
version "2.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-jstree/-/vue-jstree-2.1.6.tgz#44827ad72953ed77da6590ce4e8f37f7787f8653"
|
||||||
|
integrity sha512-vtUmhLbfE2JvcnYNRXauJPkNJSRO/f9BTsbxV+ESXP/mMQPVUIYI4EkSHKSEOxVDHTU7SfLp/AxplmaAl6ctcg==
|
||||||
|
|
||||||
"vue-loader-v16@npm:vue-loader@^16.1.0":
|
"vue-loader-v16@npm:vue-loader@^16.1.0":
|
||||||
version "16.8.3"
|
version "16.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.8.3.tgz#d43e675def5ba9345d6c7f05914c13d861997087"
|
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.8.3.tgz#d43e675def5ba9345d6c7f05914c13d861997087"
|
||||||
|
|||||||
Reference in New Issue
Block a user