Merge branch 'develop' into feature/unit-conversion

# Conflicts:
#	vue/yarn.lock
This commit is contained in:
vabene1111 2023-02-24 20:41:01 +01:00
commit 5651beffb2
132 changed files with 9291 additions and 8915 deletions

View File

@ -97,7 +97,7 @@ GUNICORN_MEDIA=0
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
# allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
# see docs for more information https://docs.tandoor.dev/features/authentication/
# when unset: 0 (false)
REVERSE_PROXY_AUTH=0
@ -126,7 +126,7 @@ REVERSE_PROXY_AUTH=0
# ENABLE_METRICS=0
# allows you to setup OAuth providers
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
# see docs for more information https://docs.tandoor.dev/features/authentication/
# SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
# Should a newly created user from a social provider get assigned to the default space and given permission by default ?

View File

@ -1,6 +1,6 @@
name: Continuous Integration
on: [push]
on: [push, pull_request]
jobs:
build:

View File

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="vaben">
<words>
<w>pinia</w>
<w>selfhosted</w>
</words>
</dictionary>
</component>

View File

@ -32,7 +32,7 @@ admin.site.unregister(Group)
@admin.action(description='Delete all data from a space')
def delete_space_action(modeladmin, request, queryset):
for space in queryset:
space.save()
space.safe_delete()
class SpaceAdmin(admin.ModelAdmin):

View File

@ -154,6 +154,7 @@ class ImportExportBase(forms.Form):
COOKBOOKAPP = 'COOKBOOKAPP'
COPYMETHAT = 'COPYMETHAT'
COOKMATE = 'COOKMATE'
REZEPTSUITEDE = 'REZEPTSUITEDE'
PDF = 'PDF'
type = forms.ChoiceField(choices=(
@ -162,7 +163,7 @@ class ImportExportBase(forms.Form):
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
(COOKMATE, 'Cookmate')
(COOKMATE, 'Cookmate'), (REZEPTSUITEDE, 'Recipesuite.de')
))

View File

@ -126,6 +126,8 @@ class IngredientParser:
amount = 0
unit = None
note = ''
if x.strip() == '':
return amount, unit, note
did_check_frac = False
end = 0
@ -235,6 +237,10 @@ class IngredientParser:
# leading spaces before commas result in extra tokens, clean them out
ingredient = ingredient.replace(' ,', ',')
# handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description
# "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)"
ingredient = re.sub("^(\d+|\d+[\\.,]\d+) - (\d+|\d+[\\.,]\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)
# if amount and unit are connected add space in between
if re.match('([0-9])+([A-z])+\s', ingredient):
ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient)

View File

@ -123,7 +123,7 @@ def share_link_valid(recipe, share):
return c
if link := ShareLink.objects.filter(recipe=recipe, uuid=share, abuse_blocked=False).first():
if 0 < settings.SHARING_LIMIT < link.request_count:
if 0 < settings.SHARING_LIMIT < link.request_count and not link.space.no_sharing_limit:
return False
link.request_count += 1
link.save()

View File

@ -125,7 +125,8 @@ def get_from_scraper(scrape, request):
recipe_json['source_url'] = ''
try:
keywords.append(scrape.author())
if scrape.author():
keywords.append(scrape.author())
except:
pass
@ -160,32 +161,33 @@ def get_from_scraper(scrape, request):
try:
for x in scrape.ingredients():
try:
amount, unit, ingredient, note = ingredient_parser.parse(x)
ingredient = {
'amount': amount,
'food': {
'name': ingredient,
},
'unit': None,
'note': note,
'original_text': x
}
if unit:
ingredient['unit'] = {'name': unit, }
recipe_json['steps'][0]['ingredients'].append(ingredient)
except Exception:
recipe_json['steps'][0]['ingredients'].append(
{
'amount': 0,
'unit': None,
if x.strip() != '':
try:
amount, unit, ingredient, note = ingredient_parser.parse(x)
ingredient = {
'amount': amount,
'food': {
'name': x,
'name': ingredient,
},
'note': '',
'unit': None,
'note': note,
'original_text': x
}
)
if unit:
ingredient['unit'] = {'name': unit, }
recipe_json['steps'][0]['ingredients'].append(ingredient)
except Exception:
recipe_json['steps'][0]['ingredients'].append(
{
'amount': 0,
'unit': None,
'food': {
'name': x,
},
'note': '',
'original_text': x
}
)
except Exception:
pass
@ -320,6 +322,11 @@ def parse_servings_text(servings):
servings = re.sub("\d+", '', servings).strip()
except Exception:
servings = ''
if type(servings) == list:
try:
servings = parse_servings_text(servings[1])
except Exception:
pass
return str(servings)[:32]
@ -417,3 +424,18 @@ def get_images_from_soup(soup, url):
if 'http' in u:
images.append(u)
return images
def clean_dict(input_dict, key):
if type(input_dict) == dict:
for x in list(input_dict):
if x == key:
del input_dict[x]
elif type(input_dict[x]) == dict:
input_dict[x] = clean_dict(input_dict[x], key)
elif type(input_dict[x]) == list:
temp_list = []
for e in input_dict[x]:
temp_list.append(clean_dict(e, key))
return input_dict

View File

@ -47,6 +47,8 @@ class RecipeShoppingEditor():
self.mealplan = self._kwargs.get('mealplan', None)
if type(self.mealplan) in [int, float]:
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
if type(self.mealplan) == dict:
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
self.id = self._kwargs.get('id', None)
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
@ -107,7 +109,10 @@ class RecipeShoppingEditor():
self.servings = float(servings)
if mealplan := kwargs.get('mealplan', None):
self.mealplan = mealplan
if type(mealplan) == dict:
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
else:
self.mealplan = mealplan
self.recipe = mealplan.recipe
elif recipe := kwargs.get('recipe', None):
self.recipe = recipe

View File

@ -1,16 +1,12 @@
import time
import traceback
import datetime
import json
import traceback
import uuid
from io import BytesIO, StringIO
from io import BytesIO
from zipfile import BadZipFile, ZipFile
import lxml
from django.core.cache import cache
import datetime
from bs4 import Tag
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File
from django.db import IntegrityError
@ -20,8 +16,7 @@ from django.utils.translation import gettext as _
from django_scopes import scope
from lxml import etree
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype, handle_image
from cookbook.helper.image_processing import handle_image
from cookbook.models import Keyword, Recipe
from recipes.settings import DEBUG
from recipes.settings import EXPORT_FILE_CACHE_DURATION
@ -182,7 +177,7 @@ class Integration:
traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
import_zip.close()
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']:
elif '.json' in f['name'] or '.xml' 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'])
il.total_recipes += len(data_list)
for d in data_list:

View File

@ -0,0 +1,72 @@
import base64
from io import BytesIO
from xml import etree
from lxml import etree
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_time, parse_servings, parse_servings_text
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Recipe, Step, Keyword
class Rezeptsuitede(Integration):
def split_recipe_file(self, file):
return etree.parse(file).getroot().getchildren()
def get_recipe_from_file(self, file):
recipe_xml = file
recipe = Recipe.objects.create(
name=recipe_xml.find('head').attrib['title'].strip(),
created_by=self.request.user, internal=True, space=self.request.space)
if recipe_xml.find('head').attrib['servingtype']:
recipe.servings = parse_servings(recipe_xml.find('head').attrib['servingtype'].strip())
recipe.servings_text = parse_servings_text(recipe_xml.find('head').attrib['servingtype'].strip())
if recipe_xml.find('remark') is not None: # description is a list of <li>'s with text
if recipe_xml.find('remark').find('line') is not None:
recipe.description = recipe_xml.find('remark').find('line').text[:512]
for prep in recipe_xml.findall('preparation'):
try:
if prep.find('step').text:
step = Step.objects.create(
instruction=prep.find('step').text.strip(), space=self.request.space,
)
recipe.steps.add(step)
except Exception:
pass
ingredient_parser = IngredientParser(self.request, True)
if recipe_xml.find('part').find('ingredient') is not None:
ingredient_step = recipe.steps.first()
if ingredient_step is None:
ingredient_step = Step.objects.create(space=self.request.space, instruction='')
for ingredient in recipe_xml.find('part').findall('ingredient'):
f = ingredient_parser.get_food(ingredient.attrib['item'])
u = ingredient_parser.get_unit(ingredient.attrib['unit'])
amount, unit, note = ingredient_parser.parse_amount(ingredient.attrib['qty'])
ingredient_step.ingredients.add(Ingredient.objects.create(food=f, unit=u, amount=amount, space=self.request.space, ))
try:
k, created = Keyword.objects.get_or_create(name=recipe_xml.find('head').find('cat').text.strip(), space=self.request.space)
recipe.keywords.add(k)
except Exception as e:
pass
recipe.save()
try:
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_xml.find('head').find('picbin').text)), filetype='.jpeg')
except:
pass
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-11 08:52+0100\n"
"PO-Revision-Date: 2022-03-08 01:31+0000\n"
"Last-Translator: Felipe Castro <felipefcastro@gmail.com>\n"
"PO-Revision-Date: 2023-02-18 10:55+0000\n"
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
"Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/pt_BR/>\n"
"Language: pt_BR\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.10.1\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\space.html:50 .\cookbook\templates\stats.html:28
@ -158,7 +158,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:195
#: .\cookbook\templates\url_import.html:585 .\cookbook\views\lists.py:97
msgid "Keywords"
msgstr ""
msgstr "Palavras-chave"
#: .\cookbook\forms.py:131
msgid "Preparation time in minutes"
@ -513,7 +513,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:231
#: .\cookbook\templates\url_import.html:462
msgid "Servings"
msgstr ""
msgstr "Porções"
#: .\cookbook\integration\saffron.py:25
msgid "Waiting time"
@ -585,7 +585,7 @@ msgstr ""
#: .\cookbook\models.py:302 .\cookbook\templates\base.html:90
msgid "Books"
msgstr ""
msgstr "Livros"
#: .\cookbook\models.py:310
msgid "Small"
@ -598,7 +598,7 @@ msgstr ""
#: .\cookbook\models.py:310 .\cookbook\templates\generic\new_template.html:6
#: .\cookbook\templates\generic\new_template.html:14
msgid "New"
msgstr ""
msgstr "Novo"
#: .\cookbook\models.py:513
msgid " is part of a recipe step and cannot be deleted"
@ -677,7 +677,7 @@ msgstr ""
#: .\cookbook\templates\shopping_list.html:37
#: .\cookbook\templates\space.html:109
msgid "Edit"
msgstr ""
msgstr "Editar"
#: .\cookbook\tables.py:115 .\cookbook\tables.py:138
#: .\cookbook\templates\generic\delete_template.html:7
@ -715,7 +715,7 @@ msgstr ""
#: .\cookbook\templates\settings.html:17
#: .\cookbook\templates\socialaccount\connections.html:10
msgid "Settings"
msgstr ""
msgstr "Configurações"
#: .\cookbook\templates\account\email.html:13
msgid "Email"
@ -937,7 +937,7 @@ msgstr ""
#: .\cookbook\templates\account\signup.html:48
#: .\cookbook\templates\socialaccount\signup.html:39
msgid "and"
msgstr ""
msgstr "e"
#: .\cookbook\templates\account\signup.html:52
#: .\cookbook\templates\socialaccount\signup.html:43
@ -989,7 +989,7 @@ msgstr ""
#: .\cookbook\templates\shopping_list.html:208
#: .\cookbook\templates\supermarket.html:7
msgid "Supermarket"
msgstr ""
msgstr "Supermercado"
#: .\cookbook\templates\base.html:163
msgid "Supermarket Category"
@ -1027,7 +1027,7 @@ msgstr ""
#: .\cookbook\templates\shopping_list.html:165
#: .\cookbook\templates\shopping_list.html:188
msgid "Create"
msgstr ""
msgstr "Criar"
#: .\cookbook\templates\base.html:259
#: .\cookbook\templates\generic\list_template.html:14
@ -1190,7 +1190,7 @@ msgstr ""
#: .\cookbook\templates\generic\delete_template.html:26
msgid "Protected"
msgstr ""
msgstr "Protegido"
#: .\cookbook\templates\generic\delete_template.html:41
msgid "Cascade"
@ -1268,7 +1268,7 @@ msgstr ""
#: .\cookbook\templates\include\recipe_open_modal.html:18
msgid "Close"
msgstr ""
msgstr "Fechar"
#: .\cookbook\templates\include\recipe_open_modal.html:32
msgid "Open Recipe"
@ -1821,7 +1821,7 @@ msgstr ""
#: .\cookbook\templates\settings.html:162
msgid "or"
msgstr ""
msgstr "ou"
#: .\cookbook\templates\settings.html:173
msgid ""
@ -2062,7 +2062,7 @@ msgstr ""
#: .\cookbook\templates\space.html:120
msgid "user"
msgstr ""
msgstr "usuário"
#: .\cookbook\templates\space.html:121
msgid "guest"
@ -2273,7 +2273,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:214
msgid "Image"
msgstr ""
msgstr "Imagem"
#: .\cookbook\templates\url_import.html:246
msgid "Prep Time"
@ -2359,7 +2359,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:640
msgid "Information"
msgstr ""
msgstr "Informação"
#: .\cookbook\templates\url_import.html:642
msgid ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"PO-Revision-Date: 2023-02-09 13:55+0000\n"
"Last-Translator: vertilo <vertilo.dev@gmail.com>\n"
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/uk/>\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
@ -2026,7 +2028,7 @@ msgstr ""
#: .\cookbook\templates\space.html:118
msgid "user"
msgstr ""
msgstr "користувач"
#: .\cookbook\templates\space.html:119
msgid "guest"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.4 on 2023-01-20 09:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0186_automation_order_alter_automation_type'),
]
operations = [
migrations.AlterField(
model_name='space',
name='use_plural',
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.4 on 2023-02-12 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0187_alter_space_use_plural'),
]
operations = [
migrations.AddField(
model_name='space',
name='no_sharing_limit',
field=models.BooleanField(default=False),
),
]

View File

@ -260,8 +260,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
max_recipes = models.IntegerField(default=0)
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
max_users = models.IntegerField(default=0)
use_plural = models.BooleanField(default=False)
use_plural = models.BooleanField(default=True)
allow_sharing = models.BooleanField(default=True)
no_sharing_limit = models.BooleanField(default=False)
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False)

View File

@ -432,9 +432,13 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
def create(self, validated_data):
name = validated_data.pop('name').strip()
plural_name = validated_data.pop('plural_name', None)
if plural_name:
if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip()
if unit := Unit.objects.filter(Q(name=name) | Q(plural_name=name)).first():
return unit
space = validated_data.pop('space', self.context['request'].space)
obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data)
return obj
@ -544,9 +548,13 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
def create(self, validated_data):
name = validated_data.pop('name').strip()
plural_name = validated_data.pop('plural_name', None)
if plural_name:
if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip()
if food := Food.objects.filter(Q(name=name) | Q(plural_name=name)).first():
return food
space = validated_data.pop('space', self.context['request'].space)
# supermarket category needs to be handled manually as food.get or create does not create nested serializers unlike a super.create of serializer
if 'supermarket_category' in validated_data and validated_data['supermarket_category']:

View File

@ -2,6 +2,16 @@
height: 40px;
}
.two-row-text {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* number of lines to show */
line-clamp: 2;
-webkit-box-orient: vertical;
}
@media (max-width: 991.98px) {
.menu-dropdown-text {
font-size: 14px;

View File

@ -350,8 +350,8 @@
{% message_of_the_day request as message_of_the_day %}
{% if message_of_the_day %}
<div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
{{ message_of_the_day }}
<div class="bg-info" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
{{ message_of_the_day | markdown |safe }}
</div>
{% endif %}

View File

@ -7,6 +7,21 @@
{% block title %}{{ recipe.name }}{% endblock %}
{% block extra_head %}
<meta property="og:title" content="{{ recipe.name }}"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="{% base_path request 'base' %}{% url 'view_recipe' recipe.pk share %}"/>
{% if recipe.image %}
<meta property="og:image" content="{% base_path request 'base' %}{{ recipe.image.url }}"/>
<meta property="og:image:url" content="{% base_path request 'base' %}{{ recipe.image.url }}"/>
<meta property="og:image:secure" content="{% base_path request 'base' %}{{ recipe.image.url }}"/>
{% endif %}
{% if recipe.description %}
<meta property="og:description" content="{{ recipe.description }}"/>
{% endif %}
<meta property="og:site_name" content="Tandoor Recipes"/>
{% endblock %}
{% block content %}
{% recipe_rating recipe request.user as rating %}

View File

@ -10,11 +10,24 @@
{% block content_fluid %}
{{ data }}
<div id="app">
<test-view></test-view>
</div>
{% endblock %}
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>
{% render_bundle 'test_view' %}
{% endblock %}

View File

@ -57,6 +57,8 @@ def markdown(value):
]
)
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
parsed_md = parsed_md[3:] # remove outer paragraph
parsed_md = parsed_md[:len(parsed_md)-4]
return bleach.clean(parsed_md, tags, markdown_attrs)

View File

@ -97,7 +97,7 @@ class SupermarketCategoryFactory(factory.django.DjangoModelFactory):
@register
class FoodFactory(factory.django.DjangoModelFactory):
"""Food factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128])
plural_name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
supermarket_category = factory.Maybe(
@ -160,7 +160,7 @@ class RecipeBookEntryFactory(factory.django.DjangoModelFactory):
@register
class UnitFactory(factory.django.DjangoModelFactory):
"""Unit factory."""
name = factory.LazyAttribute(lambda x: faker.word())
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128])
plural_name = factory.LazyAttribute(lambda x: faker.word())
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
space = factory.SubFactory(SpaceFactory)

View File

@ -75,6 +75,8 @@ def test_ingredient_parser():
# an amount # and it starts with a lowercase letter, then that
# is a unit ("etwas", "evtl.") does not apply to English tho
# TODO maybe add/improve support for weired stuff like this https://www.rainbownourishments.com/vegan-lemon-tart/#recipe
ingredient_parser = IngredientParser(None, False, ignore_automations=True)
count = 0

View File

@ -1,6 +1,7 @@
import io
import json
import mimetypes
import pathlib
import re
import threading
import traceback
@ -56,7 +57,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
CustomIsSpaceOwner, CustomIsUser, group_required,
is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission, CustomUserPermission, CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission)
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
@ -87,7 +88,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer)
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, RecipeExportSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
@ -533,6 +534,11 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
.prefetch_related('onhand_users', 'inherit_fields', 'child_inherit_fields', 'substitute') \
.select_related('recipe', 'supermarket_category')
def get_serializer_class(self):
if self.request and self.request.query_params.get('simple', False):
return FoodSimpleSerializer
return self.serializer_class
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
def shopping(self, request, pk):
@ -655,7 +661,7 @@ class IngredientViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request and self.request.query_params.get('simple', False):
return IngredientSimpleSerializer
return IngredientSerializer
return self.serializer_class
def get_queryset(self):
queryset = self.queryset.filter(step__recipe__space=self.request.space)
@ -1169,6 +1175,18 @@ def recipe_from_source(request):
# 'recipe_html': '',
'recipe_images': [],
}, status=status.HTTP_200_OK)
if re.match('^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', url):
recipe_json = requests.get(url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], '') + '?share=' + re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
recipe_json = clean_dict(recipe_json, 'id')
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
if serialized_recipe.is_valid():
recipe = serialized_recipe.save()
recipe.image = File(handle_image(request, File(io.BytesIO(requests.get(recipe_json['image']).content), name='image'), filetype=pathlib.Path(recipe_json['image']).suffix),
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
recipe.save()
return Response({
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
}, status=status.HTTP_201_CREATED)
else:
try:
if validators.url(url, public=True):
@ -1306,6 +1324,8 @@ def import_files(request):
return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
except NotImplementedError:
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
def get_recipe_provider(recipe):

View File

@ -31,6 +31,7 @@ from cookbook.integration.plantoeat import Plantoeat
from cookbook.integration.recettetek import RecetteTek
from cookbook.integration.recipekeeper import RecipeKeeper
from cookbook.integration.recipesage import RecipeSage
from cookbook.integration.rezeptsuitede import Rezeptsuitede
from cookbook.integration.rezkonv import RezKonv
from cookbook.integration.saffron import Saffron
from cookbook.models import ExportLog, ImportLog, Recipe, UserPreference
@ -80,6 +81,8 @@ def get_integration(request, export_type):
return MelaRecipes(request, export_type)
if export_type == ImportExportBase.COOKMATE:
return Cookmate(request, export_type)
if export_type == ImportExportBase.REZEPTSUITEDE:
return Rezeptsuitede(request, export_type)
@group_required('user')

View File

@ -49,7 +49,7 @@ If removed, the nginx webserver needs to be replaced by something else that serv
`GUNICORN_MEDIA` needs to be enabled to allow media serving by the application container itself.
## Why does the Text/Markdown preview look different than the final recipe ?
## Why does the Text/Markdown preview look different than the final recipe?
Tandoor has always rendered the recipe instructions markdown on the server. This also allows tandoor to implement things like ingredient templating and scaling in text.
To make editing easier a markdown editor was added to the frontend with integrated preview as a temporary solution. Since the markdown editor uses a different
@ -66,7 +66,7 @@ To create a new user click on your name (top right corner) and select 'space set
It is not possible to create users through the admin because users must be assigned a default group and space.
To change a users space you need to go to the admin and select User Infos.
To change a user's space you need to go to the admin and select User Infos.
If you use an external auth provider or proxy authentication make sure to specify a default group and space in the
environment configuration.
@ -78,7 +78,7 @@ In technical terms it is a multi tenant system.
You can compare a space to something like google drive or dropbox.
There is only one installation of the Dropbox system, but it handles multiple users without them noticing each other.
For Tandoor that means all people that work together on one recipe collection can be in one space.
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (through the admin interface).
If you want to host the collection of your friends, family, or neighbor you can create a separate space for them (through the admin interface).
Sharing between spaces is currently not possible but is planned for future releases.

View File

@ -31,6 +31,7 @@ Overview of the capabilities of the different integrations.
| ChefTap | ✔️ | ❌ | ❌ |
| Pepperplate | ✔️ | ⌚ | ❌ |
| RecipeSage | ✔️ | ✔️ | ✔️ |
| Rezeptsuite.de | ✔️ | ❌ | ✔️ |
| Domestica | ✔️ | ⌚ | ✔️ |
| MealMaster | ✔️ | ❌ | ❌ |
| RezKonv | ✔️ | ❌ | ❌ |
@ -233,6 +234,9 @@ Cookmate allows you to export a `.mcb` file which you can simply upload to tando
## RecetteTek
RecetteTek exports are `.rtk` files which can simply be uploaded to tandoor to import all your recipes.
## Rezeptsuite.de
Rezeptsuite.de exports are `.xml` 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.

View File

@ -180,11 +180,11 @@ server {
#error_log /var/log/nginx/error.log;
# serve media files
location /static {
location /static/ {
alias /var/www/recipes/staticfiles;
}
location /media {
location /media/ {
alias /var/www/recipes/mediafiles;
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,64 +18,64 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr "Englisch"
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr "Deutsch"
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
#, fuzzy
#| msgid "English"
msgid "Polish"
msgstr "Englisch"
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,62 +17,62 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -19,62 +19,62 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
"2);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,62 +17,62 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,62 +17,62 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:369
#: .\recipes\settings.py:382
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:370
#: .\recipes\settings.py:383
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:371
#: .\recipes\settings.py:384
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:372
#: .\recipes\settings.py:385
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:373
#: .\recipes\settings.py:386
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:374
#: .\recipes\settings.py:387
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:375
#: .\recipes\settings.py:388
msgid "English"
msgstr ""
#: .\recipes\settings.py:376
#: .\recipes\settings.py:389
msgid "French"
msgstr ""
#: .\recipes\settings.py:377
#: .\recipes\settings.py:390
msgid "German"
msgstr ""
#: .\recipes\settings.py:378
#: .\recipes\settings.py:391
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:379
#: .\recipes\settings.py:392
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:380
#: .\recipes\settings.py:393
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:381
#: .\recipes\settings.py:394
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:382
#: .\recipes\settings.py:395
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:396
msgid "Swedish"
msgstr ""

View File

@ -1,5 +1,5 @@
Django==4.1.4
cryptography==38.0.4
Django==4.1.7
cryptography==39.0.1
django-annoying==0.10.6
django-autocomplete-light==3.9.4
django-cleanup==6.0.0
@ -8,13 +8,13 @@ django-tables2==2.4.1
djangorestframework==3.14.0
drf-writable-nested==0.7.0
django-oauth-toolkit==2.2.0
django-debug-toolbar==3.7.0
django-debug-toolbar==3.8.1
bleach==5.0.1
bleach-allowlist==1.0.3
gunicorn==20.1.0
lxml==4.9.2
Markdown==3.4.1
Pillow==9.3.0
Pillow==9.4.0
psycopg2-binary==2.9.5
python-dotenv==0.21.0
requests==2.28.1
@ -30,7 +30,7 @@ Jinja2==3.1.2
django-webpack-loader==1.8.0
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
django-allauth==0.52.0
recipe-scrapers==14.24.0
recipe-scrapers==14.30.0
django-scopes==1.2.0.post1
pytest==7.2.0
pytest-django==4.5.2

View File

@ -9,20 +9,24 @@
},
"dependencies": {
"@babel/eslint-parser": "^7.19.1",
"@kevinfaguiar/vue-twemoji-picker": "^5.7.4",
"@emoji-mart/data": "^1.1.1",
"@popperjs/core": "^2.11.6",
"@riophae/vue-treeselect": "^0.4.0",
"@vue/cli": "^5.0.8",
"@vue/composition-api": "1.7.1",
"axios": "^1.2.0",
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-loader": "^9.1.0",
"bootstrap-vue": "^2.23.1",
"core-js": "^3.27.1",
"emoji-mart": "^5.4.0",
"emoji-mart-vue-fast": "^12.0.1",
"html2pdf.js": "^0.10.1",
"lodash": "^4.17.21",
"mavon-editor": "^2.10.4",
"moment": "^2.29.4",
"pinia": "^2.0.30",
"prismjs": "^1.29.0",
"string-similarity": "^4.0.4",
"vue": "^2.6.14",
@ -36,12 +40,12 @@
"vue-multiselect": "^2.1.6",
"vue-property-decorator": "^9.1.2",
"vue-sanitize": "^0.2.2",
"vue-simple-calendar": "^5.0.0",
"vue-simple-calendar": "TandoorRecipes/vue-simple-calendar#lastvue2",
"vue-template-compiler": "2.6.14",
"vue2-touch-events": "^3.2.2",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0",
"workbox-webpack-plugin": "^6.5.4"
"workbox-webpack-plugin": "^6.5.4",
"workbox-window": "^6.5.4"
},
"devDependencies": {
"@kazupon/vue-i18n-loader": "^0.5.0",
@ -60,6 +64,7 @@
"typescript": "~4.9.3",
"vue-cli-plugin-i18n": "^2.3.1",
"webpack-bundle-tracker": "1.8.0",
"workbox-background-sync": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-navigation-preload": "^6.5.4",
"workbox-precaching": "^6.5.4",

View File

@ -18,7 +18,8 @@
</div>
</div>
</div>
<div class="mb-3" v-for="book in filteredBooks" :key="book.id">
<div style="padding-bottom: 55px">
<div class="mb-3" v-for="book in filteredBooks" :key="book.id">
<div class="row">
<div class="col-md-12">
<b-card class="d-flex flex-column" v-hover v-on:click="openBook(book.id)">
@ -53,7 +54,18 @@
@reload="openBook(current_book, true)"
></cookbook-slider>
</transition>
</div>
</div>
<bottom-navigation-bar>
<template #custom_create_functions>
<a class="dropdown-item" @click="createNew()"><i
class="fa fa-book"></i> {{$t("Create")}}</a>
<div class="dropdown-divider" ></div>
</template>
</bottom-navigation-bar>
</div>
</template>
@ -66,13 +78,14 @@ import { ApiApiFactory } from "@/utils/openapi/api"
import CookbookSlider from "@/components/CookbookSlider"
import LoadingSpinner from "@/components/LoadingSpinner"
import { StandardToasts, ApiMixin } from "@/utils/utils"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
Vue.use(BootstrapVue)
export default {
name: "CookbookView",
mixins: [ApiMixin],
components: { LoadingSpinner, CookbookSlider },
components: { LoadingSpinner, CookbookSlider, BottomNavigationBar },
data() {
return {
cookbooks: [],

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './CookbookView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ExportResponseView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ExportView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ImportResponseView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -24,8 +24,11 @@
<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"><i class="far fa-copy fa-fw"></i> {{ $t('Multiple') }}</span><span
v-if="!import_multiple"><i class="far fa-file fa-fw"></i> {{ $t('Single') }}</span></b-checkbox>
v-if="import_multiple"><i
class="far fa-copy fa-fw"></i> {{ $t('Multiple') }}</span><span
v-if="!import_multiple"><i
class="far fa-file fa-fw"></i> {{ $t('Single') }}</span>
</b-checkbox>
</div>
</div>
<b-input-group class="mt-2" :class="{ bounce: empty_input }"
@ -52,23 +55,23 @@
</b-button>
<!-- recent imports, nice for testing/development -->
<!-- <div class="row mt-2"> -->
<!-- <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 class="row mt-2"> -->
<!-- <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>-->
<!-- </div>-->
</div>
</div>
@ -238,17 +241,22 @@
</b-row>
</b-card-body>
<b-card-footer class="text-center">
<div class="d-flex justify-content-center mb-3" v-if="import_loading">
<b-spinner variant="primary"></b-spinner>
</div>
<b-button-group>
<b-button @click="importRecipe('view')" v-if="!import_multiple">Import &
<b-button @click="importRecipe('view')" v-if="!import_multiple"
:disabled="import_loading">Import &
View
</b-button> <!-- TODO localize -->
<b-button @click="importRecipe('edit')" variant="success"
v-if="!import_multiple">Import & Edit
v-if="!import_multiple" :disabled="import_loading">Import & Edit
</b-button>
<b-button @click="importRecipe('import')" v-if="!import_multiple">Import &
<b-button @click="importRecipe('import')" v-if="!import_multiple"
:disabled="import_loading">Import &
Restart
</b-button>
<b-button @click="location.reload()">Restart
<b-button @click="location.reload()" :disabled="import_loading">Restart
</b-button>
</b-button-group>
</b-card-footer>
@ -462,6 +470,7 @@ export default {
source_data: '',
recipe_json: undefined,
use_plural: false,
import_loading: false,
// recipe_html: undefined,
// recipe_tree: undefined,
recipe_images: [],
@ -495,6 +504,13 @@ export default {
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
let urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("url")) {
this.website_url = urlParams.get('url')
this.loadRecipe(this.website_url)
}
},
methods: {
/**
@ -504,6 +520,7 @@ export default {
* @param silent do not show any messages for imports
*/
importRecipe: function (action, data, silent) {
this.import_loading = true
if (this.recipe_json !== undefined) {
this.$set(this.recipe_json, 'keywords', this.recipe_json.keywords.filter(k => k.show))
}
@ -528,12 +545,14 @@ export default {
if (recipe_json.source_url !== '') {
this.failed_imports.push(recipe_json.source_url)
}
this.import_loading = false
if (!silent) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
}
})
} else {
console.log('cant import recipe without data')
this.import_loading = false
if (!silent) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
}
@ -563,6 +582,7 @@ export default {
this.imported_recipes.push(recipe)
break;
case 'nothing':
this.import_loading = false
break;
}
},
@ -614,6 +634,11 @@ export default {
}
return axios.post(resolveDjangoUrl('api_recipe_from_source'), payload,).then((response) => {
if (response.status === 201 && 'link' in response.data) {
window.location = response.data.link
return
}
this.loading = false
this.recipe_json = response.data['recipe_json'];

View File

@ -1,63 +1,101 @@
<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="autoSortIngredients()" variant="secondary" v-b-tooltip.hover v-if="recipe_json.steps.length > 1"
:title="$t('Auto_Sort_Help')"><i class="fas fa-random"></i> {{ $t('Auto_Sort') }}
</b-button>
<b-button @click="splitAllSteps('\n')" variant="secondary" class="ml-1" 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 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="autoSortIngredients()" variant="secondary" v-b-tooltip.hover v-if="recipe_json.steps.length > 1"
:title="$t('Auto_Sort_Help')"><i class="fas fa-random"></i> {{ $t('Auto_Sort') }}
</b-button>
<b-button @click="splitAllSteps('\n')" variant="secondary" class="ml-1" 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 mr-2"></i>
<b-badge variant="light">{{ i.amount.toFixed(2) }}</b-badge>
<b-badge variant="secondary" v-if="i.unit">{{ i.unit.name }}</b-badge>
<b-badge variant="info" v-if="i.food">{{ i.food.name }}</b-badge>
<i>{{ i.original_text }}</i>
<b-button @click="prepareIngredientEditModal(s,i)" v-b-modal.ingredient_edit_modal class="float-right btn-sm"><i class="fas fa-pencil-alt"></i></b-button>
</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>
<b-modal id="ingredient_edit_modal" :title="$t('Edit')">
<div v-if="current_edit_ingredient !== null">
<b-form-group v-bind:label="$t('Original_Text')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.original_text" type="text" disabled></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Amount')" class="mb-3">
<b-form-input v-model.number="current_edit_ingredient.amount" type="number"></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Unit')" class="mb-3" v-if="current_edit_ingredient.unit !== null">
<b-form-input v-model="current_edit_ingredient.unit.name" type="text"></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Food')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.food.name" type="text"></b-form-input>
</b-form-group>
<b-form-group v-bind:label="$t('Note')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.note" type="text"></b-form-input>
</b-form-group>
</div>
<template v-slot:modal-footer>
<div class="row w-100">
<div class="col-auto justify-content-end">
<b-button class="mx-1" @click="destroyIngredientEditModal()">{{ $t('Ok') }}</b-button>
<b-button class="mx-1" @click="removeIngredient(current_edit_step,current_edit_ingredient);destroyIngredientEditModal()" variant="danger">{{ $t('Delete') }}</b-button>
</div>
</div>
</template>
</b-modal>
</div>
</div>
</div>
</div>
</template>
<script>
@ -67,116 +105,161 @@ import draggable from "vuedraggable";
import stringSimilarity from "string-similarity"
export default {
name: "ImportViewStepEditor",
components: {
draggable
},
props: {
recipe: undefined
},
data() {
return {
recipe_json: undefined
}
},
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
name: "ImportViewStepEditor",
components: {
draggable
},
},
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': []})
props: {
recipe: undefined
},
data() {
return {
recipe_json: undefined,
current_edit_ingredient: null,
current_edit_step: null,
}
})
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
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
},
},
/**
* 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)
mounted() {
this.recipe_json = this.recipe
},
/**
* 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)
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)
})
},
/**
* automatically assign ingredients to steps based on text matching
*/
autoSortIngredients: function () {
let ingredients = this.recipe_json.steps.flatMap(s => s.ingredients)
this.recipe_json.steps.forEach(s => s.ingredients = [])
this.recipe_json.steps.splice(step_index, 0, {
'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
'ingredients': removed_steps.flatMap(x => x.ingredients)
})
},
/**
* automatically assign ingredients to steps based on text matching
*/
autoSortIngredients: function () {
let ingredients = this.recipe_json.steps.flatMap(s => s.ingredients)
this.recipe_json.steps.forEach(s => s.ingredients = [])
ingredients.forEach(i => {
let found = false
this.recipe_json.steps.forEach(s => {
if (s.instruction.includes(i.food.name.trim()) && !found) {
found = true
s.ingredients.push(i)
}
})
if (!found) {
let best_match = {rating: 0, step: this.recipe_json.steps[0]}
this.recipe_json.steps.forEach(s => {
let match = stringSimilarity.findBestMatch(i.food.name.trim(), s.instruction.split(' '))
if (match.bestMatch.rating > best_match.rating) {
best_match = {rating: match.bestMatch.rating, step: s}
ingredients.forEach(i => {
let found = false
this.recipe_json.steps.forEach(s => {
if (s.instruction.includes(i.food.name.trim()) && !found) {
found = true
s.ingredients.push(i)
}
})
if (!found) {
let best_match = {rating: 0, step: this.recipe_json.steps[0]}
this.recipe_json.steps.forEach(s => {
let match = stringSimilarity.findBestMatch(i.food.name.trim(), s.instruction.split(' '))
if (match.bestMatch.rating > best_match.rating) {
best_match = {rating: match.bestMatch.rating, step: s}
}
})
best_match.step.ingredients.push(i)
found = true
}
})
},
/**
* Prepare variable that holds currently edited ingredient for modal to manipulate it
* add default placeholder for food/unit in case it is not present, so it can be edited as well
* @param ingredient
*/
prepareIngredientEditModal: function (step, ingredient) {
if (ingredient.unit === null) {
ingredient.unit = {
"name": ""
}
}
})
best_match.step.ingredients.push(i)
found = true
if (ingredient.food === null) {
ingredient.food = {
"name": ""
}
}
this.current_edit_ingredient = ingredient
this.current_edit_step = step
},
/**
* can be called to remove an ingredient from the given step
* @param step step to remove ingredient from
* @param ingredient ingredient to remove
*/
removeIngredient: function (step, ingredient) {
step.ingredients = step.ingredients.filter((i) => i !== ingredient)
},
/**
* cleanup method called to close modal
* closes modal UI and cleanups variables
*/
destroyIngredientEditModal: function () {
this.$bvModal.hide('ingredient_edit_modal')
if (this.current_edit_ingredient.unit.name === ''){
this.current_edit_ingredient.unit = null
}
if (this.current_edit_ingredient.food.name === ''){
this.current_edit_ingredient.food = null
}
this.current_edit_ingredient = null
this.current_edit_step = null
}
})
}
}
}
</script>

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ImportView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './IngredientEditorView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -2,7 +2,7 @@
<div>
<b-tabs content-class="mt-3" v-model="current_tab">
<b-tab :title="$t('Planner')" active>
<div class="row calender-row">
<div class="row calender-row d-none d-lg-block">
<div class="col-12 calender-parent">
<calendar-view
:show-date="showDate"
@ -48,6 +48,79 @@
</calendar-view>
</div>
</div>
<div class="row d-block d-lg-none">
<div>
<div class="col-12">
<div class="col-12 d-flex justify-content-center mt-2">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
<b-button-group class="mx-1">
<b-button v-html="'<<'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-form-datepicker right button-only button-variant="secondary" @context="datePickerChanged"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>>'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
</div>
</div>
<div class="col-12 mt-2" style="padding-bottom: 60px">
<div v-for="day in mobileSimpleGrid" v-bind:key="day.day">
<b-list-group>
<b-list-group-item>
<div class="d-flex flex-row align-middle">
<h6 class="mb-0 mt-1 align-middle">{{ day.date_label }}</h6>
<div class="flex-grow-1 text-right">
<b-button class="btn-sm btn-outline-primary" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div>
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id" >
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.entry.recipe.image" rounded="circle" v-if="plan.entry.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<div class="flex-grow-1 ml-2"
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
<span class="two-row-text">
<a :href="resolveDjangoUrl('view_recipe', plan.entry.recipe.id)" v-if="plan.entry.recipe">{{ plan.entry.recipe.name }}</a>
<span v-else>{{ plan.entry.title }}</span>
</span>
<span v-if="plan.entry.note">
<small>{{ plan.entry.note }}</small> <br/>
</span>
<small class="text-muted">
<span v-if="plan.entry.shopping" class="font-light"><i class="fas fa-shopping-cart fa-xs "/></span>
{{ plan.entry.meal_type_name }}
<span v-if="plan.entry.recipe">
- <i class="fa fa-clock"></i> {{ plan.entry.recipe.working_time + plan.entry.recipe.waiting_time }} {{ $t('min') }}
</span>
</small>
</div>
<div class="hover-button">
<a class="pr-2" @click.stop="openContextMenu($event, {originalItem: plan})"><i class="fas fa-ellipsis-v"></i></a>
</div>
</div>
</b-list-group-item>
</b-list-group>
</div>
</div>
</div>
</div>
</b-tab>
<b-tab :title="$t('Settings')">
<div class="row mt-3">
@ -166,7 +239,7 @@
<ContextMenuItem
@click="
$refs.menu.close()
moveEntryLeft(contextData)
moveEntryLeft(contextData.originalItem)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i>
@ -175,7 +248,7 @@
<ContextMenuItem
@click="
$refs.menu.close()
moveEntryRight(contextData)
moveEntryRight(contextData.originalItem)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i>
@ -192,7 +265,7 @@
<ContextMenuItem
@click="
$refs.menu.close()
deleteEntry(contextData)
deleteEntry(contextData.originalItem)
"
>
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i>
@ -203,53 +276,27 @@
<meal-plan-edit-modal
:entry="entryEditing"
:modal_title="modal_title"
:edit_modal_show="edit_modal_show"
@save-entry="editEntry"
@delete-entry="deleteEntry"
:create_date="mealplan_default_date"
@reload-meal-types="refreshMealTypes"
></meal-plan-edit-modal>
<transition name="slide-fade">
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)"
v-if="current_tab === 0">
<div class="col-md-3 col-6 mb-1 mb-md-0">
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
</button>
</div>
<div class="col-md-3 col-6 mb-1 mb-md-0">
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
><i class="fas fa-download"></i>
{{ $t("Export_To_ICal") }}
</a>
</div>
<div class="col-md-3 col-6 mb-1 mb-md-0">
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top
:title="$t('Coming_Soon')">
{{ $t("Auto_Planner") }}
</button>
</div>
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
<b-button-group class="mx-1">
<b-button v-html="'<<'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
<b-button v-html="'<'" @click="setStartingDay(-1)" class="p-2 pr-3 pl-3"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>'" @click="setStartingDay(1)" class="p-2 pr-3 pl-3"></b-button>
<b-button v-html="'>>'" class="p-2 pr-3 pl-3"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
</div>
<div class="row d-none d-lg-block">
<div class="col-12 float-right">
<button class="btn btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
</button>
<a class="btn btn-primary shadow-none" :href="iCalUrl"><i class="fas fa-download"></i>
{{ $t("Export_To_ICal") }}
</a>
</div>
</transition>
</div>
<bottom-navigation-bar :create_links="[{label:$t('Export_To_ICal'), url: iCalUrl, icon:'fas fa-download'}]">
<template #custom_create_functions>
<a class="dropdown-item" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}</a>
</template>
</bottom-navigation-bar>
</div>
</template>
@ -272,6 +319,8 @@ import VueCookies from "vue-cookies"
import {ApiMixin, StandardToasts, ResolveUrlMixin} from "@/utils/utils"
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle"
import {ApiApiFactory} from "@/utils/openapi/api"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
import {useMealPlanStore} from "@/stores/MealPlanStore";
const {makeToast} = require("@/utils/utils")
@ -292,6 +341,7 @@ export default {
MealPlanCalenderHeader,
EmojiInput,
draggable,
BottomNavigationBar,
},
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
data: function () {
@ -319,29 +369,18 @@ export default {
{text: this.$t("Year"), value: "year"},
],
displayPeriodCount: [1, 2, 3],
entryEditing: {
date: null,
id: -1,
meal_type: null,
note: "",
note_markdown: "",
recipe: null,
servings: 1,
shared: [],
title: "",
title_placeholder: this.$t("Title"),
},
},
shopping_list: [],
current_period: null,
entryEditing: {},
edit_modal_show: false,
entryEditing: null,
mealplan_default_date: null,
ical_url: window.ICAL_URL,
image_placeholder: window.IMAGE_PLACEHOLDER,
}
},
computed: {
modal_title: function () {
if (this.entryEditing.id === -1) {
if (this.entryEditing === null || this.entryEditing?.id === -1) {
return this.$t("Create_Meal_Plan_Entry")
} else {
return this.$t("Edit_Meal_Plan_Entry")
@ -349,7 +388,7 @@ export default {
},
plan_items: function () {
let items = []
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
items.push(this.buildItem(entry))
})
return items
@ -383,6 +422,22 @@ export default {
return ""
}
},
mobileSimpleGrid() {
let grid = []
if (useMealPlanStore().plan_list.length > 0 && this.current_period !== null) {
for (const x of Array(7).keys()) {
let moment_date = moment(this.current_period.periodStart).add(x, "d")
grid.push({
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format('ddd DD.MM'),
plan_entries: this.plan_items.filter((m) => moment(m.startDate).isSame(moment_date, 'day'))
})
}
}
return grid
}
},
mounted() {
this.$nextTick(function () {
@ -392,6 +447,7 @@ export default {
})
this.$root.$on("change", this.updateEmoji)
this.$i18n.locale = window.CUSTOM_LOCALE
moment.locale(window.CUSTOM_LOCALE)
},
watch: {
settings: {
@ -489,33 +545,26 @@ export default {
}
})
},
editEntry(edit_entry) {
if (edit_entry.id !== -1) {
this.plan_entries.forEach((entry, index) => {
if (entry.id === edit_entry.id) {
this.$set(this.plan_entries, index, edit_entry)
this.saveEntry(this.plan_entries[index])
}
})
} else {
this.createEntry(edit_entry)
}
datePickerChanged(ctx) {
this.setShowDate(ctx.selectedDate)
},
setShowDate(d) {
this.showDate = d
},
createEntryClick(data) {
this.entryEditing = this.options.entryEditing
this.entryEditing.date = moment(data).format("YYYY-MM-DD")
this.$bvModal.show(`edit-modal`)
this.mealplan_default_date = moment(data).format("YYYY-MM-DD")
this.entryEditing = null
this.$nextTick(function () {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
},
findEntry(id) {
return this.plan_entries.filter((entry) => {
return useMealPlanStore().plan_list.filter((entry) => {
return entry.id === id
})[0]
},
moveEntry(null_object, target_date, drag_event) {
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
if (entry.id === this.dragged_item.id) {
if (drag_event.ctrlKey) {
let new_entry = Object.assign({}, entry)
@ -529,7 +578,7 @@ export default {
})
},
moveEntryLeft(data) {
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
if (entry.id === data.id) {
entry.date = moment(entry.date).subtract(1, "d")
this.saveEntry(entry)
@ -537,7 +586,7 @@ export default {
})
},
moveEntryRight(data) {
this.plan_entries.forEach((entry) => {
useMealPlanStore().plan_list.forEach((entry) => {
if (entry.id === data.id) {
entry.date = moment(entry.date).add(1, "d")
this.saveEntry(entry)
@ -545,20 +594,7 @@ export default {
})
},
deleteEntry(data) {
this.plan_entries.forEach((entry, index, list) => {
if (entry.id === data.id) {
let apiClient = new ApiApiFactory()
apiClient
.destroyMealPlan(entry.id)
.then((e) => {
list.splice(index, 1)
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
})
useMealPlanStore().deleteObject(data)
},
entryClick(data) {
let entry = this.findEntry(data.id)
@ -568,7 +604,7 @@ export default {
this.$refs.menu.open($event, value)
},
openEntryEdit(entry) {
this.$bvModal.show(`edit-modal`)
this.$bvModal.show(`id_meal_plan_edit_modal`)
this.entryEditing = entry
this.entryEditing.date = moment(entry.date).format("YYYY-MM-DD")
if (this.entryEditing.recipe != null) {
@ -577,18 +613,9 @@ export default {
},
periodChangedCallback(date) {
this.current_period = date
let apiClient = new ApiApiFactory()
apiClient
.listMealPlans({
query: {
from_date: moment(date.periodStart).format("YYYY-MM-DD"),
to_date: moment(date.periodEnd).format("YYYY-MM-DD"),
},
})
.then((result) => {
this.plan_entries = result.data
})
useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD"))
this.refreshMealTypes()
},
refreshMealTypes() {
@ -604,25 +631,11 @@ export default {
saveEntry(entry) {
entry.date = moment(entry.date).format("YYYY-MM-DD")
let apiClient = new ApiApiFactory()
apiClient.updateMealPlan(entry.id, entry).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
useMealPlanStore().updateObject(entry)
},
createEntry(entry) {
entry.date = moment(entry.date).format("YYYY-MM-DD")
let apiClient = new ApiApiFactory()
apiClient
.createMealPlan(entry)
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
.then((entry_result) => {
this.plan_entries.push(entry_result.data)
})
useMealPlanStore().createObject(entry)
},
buildItem(plan_entry) {
//dirty hack to order items within a day
@ -634,6 +647,15 @@ export default {
entry: plan_entry,
}
},
showMealPlanEditModal: function (entry, date) {
this.mealplan_default_date = date
this.entryEditing = entry
this.$nextTick(function () {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
}
},
directives: {
hover: {
@ -651,6 +673,10 @@ export default {
</script>
<style>
#id_base_container {
margin-top: 12px
}
.slide-fade-enter-active {
transition: all 0.3s ease;
}

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './MealPlanView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,7 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ModelListView'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './OfflineView.vue'
import i18n from "@/i18n";
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './ProfileView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './RecipeEditView'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,5 +1,5 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<div id="app" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher"/>
<div class="row">
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
@ -90,7 +90,7 @@
<b-form-group v-if="ui.show_meal_plan"
v-bind:label="$t('Meal_Plan_Days')"
label-for="popover-input-5" label-cols="8" class="mb-1">
<b-form-input type="number" v-model="ui.meal_plan_days"
<b-form-input type="number" v-model.number="ui.meal_plan_days"
id="popover-input-5" size="sm"
class="mt-1"></b-form-input>
</b-form-group>
@ -797,8 +797,9 @@
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
<div style="overflow-x:visible; overflow-y: hidden;white-space: nowrap;">
<b-dropdown id="sortby" :text="sortByLabel" variant="outline-primary" size="sm" style="overflow-y: visible; overflow-x: visible; position: static"
class="shadow-none" toggle-class="text-decoration-none" >
<b-dropdown id="sortby" :text="sortByLabel" variant="outline-primary" size="sm"
style="overflow-y: visible; overflow-x: visible; position: static"
class="shadow-none" toggle-class="text-decoration-none">
<div v-for="o in sortOptions" :key="o.id">
<b-dropdown-item
v-on:click="
@ -812,7 +813,7 @@
</b-dropdown>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1"
@click="resetSearch()"><i class="fas fa-file-alt"></i> {{
@click="resetSearch()" v-if="searchFiltered()"><i class="fas fa-file-alt"></i> {{
search.pagination_page
}}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }} <i
class="fas fa-times-circle"></i>
@ -828,34 +829,92 @@
</div>
</div>
<template v-if="!searchFiltered() && ui.show_meal_plan && meal_plan_grid.length > 0">
<hr/>
<div class="row">
<div class="col col-md-12">
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); column-gap: 0.5rem;row-gap: 0.5rem; grid-auto-rows: max-content; ">
<div v-for="day in meal_plan_grid" v-bind:key="day.day" :class="{'d-none d-sm-block': day.plan_entries.length === 0}">
<b-list-group >
<b-list-group-item class="hover-div pb-0">
<div class="d-flex flex-row align-items-center">
<div>
<h6>{{ day.date_label }}</h6>
</div>
<div class="flex-grow-1 text-right">
<b-button class="hover-button btn-outline-primary btn-sm" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div>
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.id" class="hover-div">
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.recipe.image" rounded="circle" v-if="plan.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<div class="flex-grow-1 ml-2"
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
<span class="two-row-text">
<a :href="resolveDjangoUrl('view_recipe', plan.recipe.id)" v-if="plan.recipe">{{ plan.recipe.name }}</a>
<span v-else>{{ plan.title }}</span>
</span>
</div>
<div class="hover-button">
<b-button @click="showMealPlanEditModal(plan,null)" class="btn-outline-primary btn-sm"><i class="fas fa-pencil-alt"></i></b-button>
</div>
</div>
</b-list-group-item>
</b-list-group>
</div>
</div>
</div>
</div>
<hr/>
</template>
<div v-if="recipes.length > 0" class="mt-4">
<div class="row">
<div class="col col-md-12">
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.4rem">
<template v-if="!searchFiltered()">
<recipe-card
v-bind:key="`mp_${m.id}`"
v-for="m in meal_plans"
:recipe="m.recipe"
:meal_plan="m"
:use_plural="use_plural"
:footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"
></recipe-card>
</template>
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); column-gap: 0.5rem;row-gap: 1rem; grid-auto-rows: max-content; ">
<!-- TODO remove once new meal plan view has proven to be good -->
<!-- <template v-if="!searchFiltered()">-->
<!-- <recipe-card-->
<!-- v-bind:key="`mp_${m.id}`"-->
<!-- v-for="m in meal_plans"-->
<!-- :recipe="m.recipe"-->
<!-- :meal_plan="m"-->
<!-- :use_plural="use_plural"-->
<!-- :footer_text="m.meal_type_name"-->
<!-- footer_icon="far fa-calendar-alt"-->
<!-- ></recipe-card>-->
<!-- </template>-->
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"
:footer_text="isRecentOrNew(r)[0]"
:footer_icon="isRecentOrNew(r)[1]"
:use_plural="use_plural">
</recipe-card>
</recipe-card>
</div>
</div>
</div>
<div class="row" style="margin-top: 2vh" v-if="!random_search">
<div class="col col-md-12">
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count"
<b-pagination v-model="search.pagination_page" :total-rows="pagination_count" first-number
last-number size="lg"
:per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
</div>
</div>
@ -894,6 +953,15 @@
</div>
</div>
</div>
<meal-plan-edit-modal
:entry="mealplan_entry_edit"
:create_date="mealplan_default_date"
></meal-plan-edit-modal>
<bottom-navigation-bar>
</bottom-navigation-bar>
</div>
</div>
</div>
@ -916,7 +984,10 @@ import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprec
import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import { ApiApiFactory } from "@/utils/openapi/api"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore";
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
import MealPlanEditModal from "@/components/MealPlanEditModal.vue";
Vue.use(VueCookies)
Vue.use(BootstrapVue)
@ -927,7 +998,7 @@ let UI_COOKIE_NAME = "ui_search_settings"
export default {
name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin, ToastMixin],
components: {GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect},
components: {GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect, BottomNavigationBar, MealPlanEditModal},
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
@ -935,6 +1006,7 @@ export default {
recipes_loading: true,
facets: {Books: [], Foods: [], Keywords: []},
meal_plans: [],
meal_plan_store: null,
last_viewed_recipes: [],
sortMenu: false,
use_plural: false,
@ -1015,9 +1087,27 @@ export default {
pagination_count: 0,
random_search: false,
debug: false,
mealplan_default_date: null,
mealplan_entry_edit: null,
image_placeholder: window.IMAGE_PLACEHOLDER,
}
},
computed: {
meal_plan_grid: function () {
let grid = []
if (this.meal_plan_store !== null && this.meal_plan_store.plan_list.length > 0) {
for (const x of Array(this.ui.meal_plan_days).keys()) {
let moment_date = moment().add(x, "d")
grid.push({
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format('ddd DD.MM'),
plan_entries: this.meal_plan_store.plan_list.filter((m) => moment(m.date).isSame(moment_date, 'day'))
})
}
}
return grid
},
locale: function () {
return window.CUSTOM_LOCALE
},
@ -1120,6 +1210,12 @@ export default {
})
return sort_order
},
isMobile: function () { //TODO move to central helper
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
},
isTouch: function () {
return window.matchMedia("(pointer: coarse)").matches
}
},
mounted() {
@ -1169,6 +1265,7 @@ export default {
this.use_plural = r.data.use_plural
})
this.$i18n.locale = window.CUSTOM_LOCALE
moment.locale(window.CUSTOM_LOCALE)
this.debug = localStorage.getItem("DEBUG") == "True" || false
},
watch: {
@ -1257,21 +1354,26 @@ export default {
return [...new Map(data.map((item) => [key(item), item])).values()]
},
loadMealPlan: function () {
if (this.ui.show_meal_plan) {
let params = {
options: {
query: {
from_date: moment().format("YYYY-MM-DD"),
to_date: moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"),
},
},
}
this.genericAPI(this.Models.MEAL_PLAN, this.Actions.LIST, params).then((result) => {
this.meal_plans = result.data
})
} else {
this.meal_plans = []
}
console.log('loadMealpLan')
this.meal_plan_store = useMealPlanStore()
this.meal_plan_store.refreshFromAPI(moment().format("YYYY-MM-DD"), moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"))
// if (this.ui.show_meal_plan) {
// let params = {
// options: {
// query: {
// from_date: moment().format("YYYY-MM-DD"),
// to_date: moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"),
// },
// },
// }
// this.genericAPI(this.Models.MEAL_PLAN, this.Actions.LIST, params).then((result) => {
// this.meal_plans = result.data
// })
// } else {
// this.meal_plans = []
// }
},
genericSelectChanged: function (obj) {
if (obj.var.includes("::")) {
@ -1544,6 +1646,15 @@ export default {
type.filter((x) => x.operator === false && x.not === false).length > 1
)
},
showMealPlanEditModal: function (entry, date) {
this.mealplan_default_date = date
this.mealplan_entry_edit = entry
this.$nextTick(function () {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
}
},
}
</script>
@ -1579,4 +1690,12 @@ export default {
width: 30px;
}
.hover-button {
display: none;
}
.hover-div:hover .hover-button {
display: inline-block;
}
</style>

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './RecipeSearchView'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -149,11 +149,14 @@
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh"
v-if="share_uid !== 'None'">
v-if="share_uid !== 'None' && !loading">
<div class="col col-md-12">
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)">{{ $t("Report Abuse") }}</a>
<import-tandoor></import-tandoor> <br/>
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" class="mt-3">{{ $t("Report Abuse") }}</a>
</div>
</div>
<bottom-navigation-bar></bottom-navigation-bar>
</div>
</template>
@ -182,6 +185,8 @@ import NutritionComponent from "@/components/NutritionComponent"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
import {ApiApiFactory} from "@/utils/openapi/api";
import ImportTandoor from "@/components/Modals/ImportTandoor.vue";
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
Vue.prototype.moment = moment
@ -191,6 +196,7 @@ export default {
name: "RecipeView",
mixins: [ResolveUrlMixin, ToastMixin],
components: {
ImportTandoor,
LastCooked,
RecipeRating,
PdfViewer,
@ -204,6 +210,7 @@ export default {
AddRecipeToBook,
RecipeSwitcher,
CustomInputSpinButton,
BottomNavigationBar,
},
computed: {
ingredient_factor: function () {

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './RecipeView.vue'
import i18n from "@/i18n";
import {createPinia, PiniaVuePlugin} from 'pinia'
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './SettingsView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,7 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<div id="app">
<b-alert :show="!online" dismissible class="small float-up" variant="warning">{{ $t("OfflineAlert") }}</b-alert>
<div class="row float-top w-100">
<div class="col-auto no-gutter ml-auto">
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
@ -469,30 +470,6 @@
</b-tab>
</b-tabs>
<transition name="slided-fade">
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none"
style="background: rgba(255, 255, 255, 0.6);width: 105%;" v-if="current_tab === 0">
<div class="col-6">
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode; "
><i class="fas fa-cart-plus"></i>
{{ $t("New_Entry") }}
</a>
</div>
<div class="col-6">
<b-dropdown id="dropdown-dropup" block dropup variant="primary" class="shadow-none">
<template #button-content><i class="fas fa-download"></i> {{ $t("Export") }}</template>
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')"
icon="far fa-file-pdf"/>
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv"
:label="$t('download_csv')" icon="fas fa-file-csv"/>
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')"
icon="fas fa-clipboard-list"/>
<CopyToClipboard :items="csvData" :settings="settings" format="table"
:label="$t('copy_markdown_table')" icon="fab fa-markdown"/>
</b-dropdown>
</div>
</div>
</transition>
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
<div>
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
@ -588,6 +565,27 @@
</ContextMenu>
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)"
:modal_id="new_recipe.id" @finish="finishShopping" :list_recipe="new_recipe.list_recipe"/>
<bottom-navigation-bar>
<template #custom_create_functions>
<a class="dropdown-item" @click="entrymode = !entrymode; "
><i class="fas fa-cart-plus"></i>
{{ $t("New_Entry") }}
</a>
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')"
icon="far fa-file-pdf fa-fw"/>
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv"
:label="$t('download_csv')" icon="fas fa-file-csv fa-fw"/>
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')"
icon="fas fa-clipboard-list fa-fw"/>
<CopyToClipboard :items="csvData" :settings="settings" format="table"
:label="$t('copy_markdown_table')" icon="fab fa-markdown fa-fw"/>
<div class="dropdown-divider"></div>
</template>
</bottom-navigation-bar>
</div>
</template>
@ -615,6 +613,8 @@ import ShoppingSettingsComponent from "@/components/Settings/ShoppingSettingsCom
Vue.use(BootstrapVue)
Vue.use(VueCookies)
let SETTINGS_COOKIE_NAME = "shopping_settings"
import {Workbox} from 'workbox-window';
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
export default {
name: "ShoppingListView",
@ -630,7 +630,8 @@ export default {
CopyToClipboard,
ShoppingModal,
draggable,
ShoppingSettingsComponent
ShoppingSettingsComponent,
BottomNavigationBar,
},
data() {
@ -903,9 +904,30 @@ export default {
}
})
this.$i18n.locale = window.CUSTOM_LOCALE
console.log(window.CUSTOM_LOCALE)
},
methods: {
/**
* failed requests to sync entry check events are automatically re-queued by the service worker for sync
* this command allows to manually force replaying those events before re-enabling automatic sync
*/
replaySyncQueue: function () {
const wb = new Workbox('/service-worker.js');
wb.register();
wb.messageSW({type: 'BGSYNC_REPLAY_REQUESTS'}).then((r) => {
console.log('Background sync queue replayed!', r);
})
},
/**
* get the number of entries left in the sync queue for entry check events
* @returns {Promise<Number>} promise resolving to the number of entries left
*/
getSyncQueueLength: function () {
const wb = new Workbox('/service-worker.js');
wb.register();
return wb.messageSW({type: 'BGSYNC_COUNT_QUEUE'}).then((r) => {
return r
})
},
setFocus() {
if (this.ui.entry_mode_simple) {
this.$refs['amount_input_simple'].focus()
@ -1043,21 +1065,27 @@ export default {
} else {
this.loading = true
}
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
.then((results) => {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
} else {
console.log("no data returned")
}
this.loading = false
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params).then((results) => {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
} else {
if (!this.auto_sync_blocked) {
this.mergeShoppingList(results.data)
}
console.log("no data returned")
}
})
this.loading = false
} else {
if (!this.auto_sync_blocked) {
this.getSyncQueueLength().then((r) => {
if (r === 0) {
this.mergeShoppingList(results.data)
} else {
this.auto_sync_running = false
this.replaySyncQueue()
}
})
}
}
})
.catch((err) => {
if (!autosync) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
@ -1205,7 +1233,7 @@ export default {
let api = new ApiApiFactory()
if (field) {
// assume if field is changing it should no longer be inherited
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
food.inherit_fields = food.inherit_fields?.filter((x) => x.field !== field)
}
return api

View File

@ -1,6 +1,7 @@
import i18n from "@/i18n"
import Vue from "vue"
import App from "./ShoppingListView"
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,7 +12,11 @@ if (process.env.NODE_ENV === "development") {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: (h) => h(App),
}).$mount("#app")

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './SpaceManageView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import App from './SupermarketView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') {
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

Some files were not shown because too many files have changed in this diff Show More