diff --git a/.env.template b/.env.template index 36c2357d50..635e079c97 100644 --- a/.env.template +++ b/.env.template @@ -7,7 +7,9 @@ SQL_DEBUG=0 ALLOWED_HOSTS=* # random secret key, use for example `base64 /dev/urandom | head -c50` to generate one +# ---------------------------- REQUIRED ------------------------- SECRET_KEY= +# --------------------------------------------------------------- # your default timezone See https://timezonedb.com/time-zones for a list of timezones TIMEZONE=Europe/Berlin @@ -18,7 +20,9 @@ DB_ENGINE=django.db.backends.postgresql POSTGRES_HOST=db_recipes POSTGRES_PORT=5432 POSTGRES_USER=djangouser +# ---------------------------- REQUIRED ------------------------- POSTGRES_PASSWORD= +# --------------------------------------------------------------- POSTGRES_DB=djangodb # database connection string, when used overrides other database settings. diff --git a/cookbook/admin.py b/cookbook/admin.py index 4c4fc52206..e53a27a86d 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -257,7 +257,7 @@ class ViewLogAdmin(admin.ModelAdmin): class InviteLinkAdmin(admin.ModelAdmin): list_display = ( - 'group', 'valid_until', + 'group', 'valid_until', 'space', 'created_by', 'created_at', 'used_by' ) diff --git a/cookbook/apps.py b/cookbook/apps.py index 3297d69287..5b2777f2b2 100644 --- a/cookbook/apps.py +++ b/cookbook/apps.py @@ -24,8 +24,8 @@ def ready(self): with scopes_disabled(): try: from cookbook.models import Keyword, Food - Keyword.fix_tree(fix_paths=True) - Food.fix_tree(fix_paths=True) + #Keyword.fix_tree(fix_paths=True) # disabled for now, causes to many unknown issues + #Food.fix_tree(fix_paths=True) except OperationalError: if DEBUG: traceback.print_exc() diff --git a/cookbook/forms.py b/cookbook/forms.py index 83ee6232b9..37a3263834 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -152,13 +152,14 @@ class ImportExportBase(forms.Form): OPENEATS = 'OPENEATS' PLANTOEAT = 'PLANTOEAT' COOKBOOKAPP = 'COOKBOOKAPP' + COPYMETHAT = 'COPYMETHAT' type = forms.ChoiceField(choices=( (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), - (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), + (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), )) diff --git a/cookbook/helper/recipe_html_import.py b/cookbook/helper/recipe_html_import.py index 7b779add0d..acf72917bc 100644 --- a/cookbook/helper/recipe_html_import.py +++ b/cookbook/helper/recipe_html_import.py @@ -1,13 +1,14 @@ import json import re +from json import JSONDecodeError +from urllib.parse import unquote from bs4 import BeautifulSoup from bs4.element import Tag +from recipe_scrapers._utils import get_host_name, normalize_string + from cookbook.helper import recipe_url_import as helper from cookbook.helper.scrapers.scrapers import text_scraper -from json import JSONDecodeError -from recipe_scrapers._utils import get_host_name, normalize_string -from urllib.parse import unquote def get_recipe_from_source(text, url, request): @@ -58,7 +59,7 @@ def get_children_list(children): return kid_list recipe_json = { - 'name': '', + 'name': '', 'url': '', 'description': '', 'image': '', @@ -188,6 +189,6 @@ def remove_graph(el): for x in el['@graph']: if '@type' in x and x['@type'] == 'Recipe': el = x - except TypeError: + except (TypeError, JSONDecodeError): pass return el diff --git a/cookbook/integration/copymethat.py b/cookbook/integration/copymethat.py new file mode 100644 index 0000000000..4f4a217e5e --- /dev/null +++ b/cookbook/integration/copymethat.py @@ -0,0 +1,84 @@ +import re +from io import BytesIO +from zipfile import ZipFile + +from bs4 import BeautifulSoup + +from cookbook.helper.ingredient_parser import IngredientParser +from cookbook.helper.recipe_html_import import get_recipe_from_source +from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Ingredient, Keyword +from recipes.settings import DEBUG + + +class CopyMeThat(Integration): + + def import_file_name_filter(self, zip_info_object): + if DEBUG: + print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html') + return zip_info_object.filename == 'recipes.html' + + def get_recipe_from_file(self, file): + # 'file' comes is as a beautifulsoup object + recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, ) + + for category in file.find_all("span", {"class": "recipeCategory"}): + keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space) + recipe.keywords.add(keyword) + + try: + recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip()) + recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip()) + recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip()) + recipe.save() + except AttributeError: + pass + + step = Step.objects.create(instruction='', space=self.request.space, ) + + ingredient_parser = IngredientParser(self.request, True) + for ingredient in file.find_all("li", {"class": "recipeIngredient"}): + if ingredient.text == "": + continue + amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip()) + f = ingredient_parser.get_food(ingredient) + u = ingredient_parser.get_unit(unit) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note, space=self.request.space, + )) + + for s in file.find_all("li", {"class": "instruction"}): + if s.text == "": + continue + step.instruction += s.text.strip() + ' \n\n' + + for s in file.find_all("li", {"class": "recipeNote"}): + if s.text == "": + continue + step.instruction += s.text.strip() + ' \n\n' + + try: + if file.find("a", {"id": "original_link"}).text != '': + step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text + step.save() + except AttributeError: + pass + + recipe.steps.add(step) + + # import the Primary recipe image that is stored in the Zip + try: + for f in self.files: + if '.zip' in f['name']: + import_zip = ZipFile(f['file']) + self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipeImage").get("src"))), filetype='.jpeg') + except Exception as e: + print(recipe.name, ': failed to import image ', str(e)) + + recipe.save() + return recipe + + def split_recipe_file(self, file): + soup = BeautifulSoup(file, "html.parser") + return soup.find_all("div", {"class": "recipe"}) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index d9b80eb9f9..2e5ac8513c 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -5,6 +5,7 @@ from io import BytesIO, StringIO from zipfile import ZipFile, BadZipFile +from bs4 import Tag from django.core.exceptions import ObjectDoesNotExist from django.core.files import File from django.db import IntegrityError @@ -16,7 +17,7 @@ from cookbook.forms import ImportExportBase from cookbook.helper.image_processing import get_filetype, handle_image from cookbook.models import Keyword, Recipe -from recipes.settings import DATABASES, DEBUG +from recipes.settings import DEBUG class Integration: @@ -153,9 +154,17 @@ def do_import(self, files, il, import_duplicates): file_list.append(z) il.total_recipes += len(file_list) + import cookbook + if isinstance(self, cookbook.integration.copymethat.CopyMeThat): + file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html'))) + il.total_recipes += len(file_list) + for z in file_list: try: - recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) + if isinstance(z, Tag): + recipe = self.get_recipe_from_file(z) + else: + recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) recipe.keywords.add(self.keyword) il.msg += f'{recipe.pk} - {recipe.name} \n' self.handle_duplicates(recipe, import_duplicates) diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 9448ac4083..b88570f4da 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -67,7 +67,7 @@ {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} - + {% endif %} diff --git a/cookbook/templates/url_import.html b/cookbook/templates/url_import.html index 6457f8761b..983f60378f 100644 --- a/cookbook/templates/url_import.html +++ b/cookbook/templates/url_import.html @@ -76,6 +76,7 @@

{% trans 'Import' %}

+ diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 0a8e83e162..ebbef836e8 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -11,6 +11,7 @@ from cookbook.forms import ExportForm, ImportForm, ImportExportBase from cookbook.helper.permission_helper import group_required from cookbook.integration.cookbookapp import CookBookApp +from cookbook.integration.copymethat import CopyMeThat from cookbook.integration.pepperplate import Pepperplate from cookbook.integration.cheftap import ChefTap from cookbook.integration.chowdown import Chowdown @@ -65,6 +66,8 @@ def get_integration(request, export_type): return Plantoeat(request, export_type) if export_type == ImportExportBase.COOKBOOKAPP: return CookBookApp(request, export_type) + if export_type == ImportExportBase.COPYMETHAT: + return CopyMeThat(request, export_type) @group_required('user') diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 09949be2f3..382ba2ab11 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -201,7 +201,10 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView): def form_valid(self, form): obj = form.save(commit=False) obj.created_by = self.request.user - obj.space = self.request.space + + # verify given space is actually owned by the user creating the link + if obj.space.created_by != self.request.user: + obj.space = self.request.space obj.save() if obj.email: try: diff --git a/docs/features/import_export.md b/docs/features/import_export.md index d53c30c049..9db829ecb0 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -37,6 +37,7 @@ Overview of the capabilities of the different integrations. | OpenEats | ✔️ | ❌ | ⌚ | | Plantoeat | ✔️ | ❌ | ✔ | | CookBookApp | ✔️ | ⌚ | ✔️ | +| CopyMeThat | ✔️ | ❌ | ✔️ | ✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented @@ -218,3 +219,7 @@ Plan to eat allows you to export a text file containing all your recipes. Simply ## CookBookApp CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes. + +## CopyMeThat + +CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes. \ No newline at end of file diff --git a/docs/install/docker.md b/docs/install/docker.md index 2261d4815a..4b572c4323 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -51,7 +51,7 @@ There are different versions (tags) released on docker hub. The main, and also recommended, installation option is to install this application using Docker Compose. 1. Choose your `docker-compose.yml` from the examples below. -2. Download the `.env` configuration file with `wget`, then **edit it accordingly**. +2. Download the `.env` configuration file with `wget`, then **edit it accordingly** (you NEED to set `SECRET_KEY` and `POSTGRES_PASSWORD`). ```shell wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env ``` @@ -111,6 +111,28 @@ wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/d {% include "./docker/nginx-proxy/docker-compose.yml" %} ~~~ +#### Nginx Swag by LinuxServer +[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io. + +It contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance. + +If you're running Swag on the default port, you'll just need to change the container name to yours. + +If your running Swag on a custom port, some headers must be changed: + +- Create a copy of `proxy.conf` +- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to + - `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;` +- Update `recipes.subdomain.conf` to use the new file +- Restart the linuxserver/swag container and Recipes will work correctly + +More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627). + + +In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory. + +Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup. + ## Additional Information ### Nginx vs Gunicorn diff --git a/recipes/settings.py b/recipes/settings.py index 6edb406b7e..0e79da2e2e 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -56,6 +56,7 @@ LOGIN_REDIRECT_URL = "index" LOGOUT_REDIRECT_URL = "index" +ACCOUNT_LOGOUT_REDIRECT_URL = "index" SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_COOKIE_AGE = 365 * 60 * 24 * 60 @@ -163,12 +164,12 @@ AUTHENTICATION_BACKENDS = [] # LDAP -LDAP_AUTH=bool(os.getenv('LDAP_AUTH', False)) +LDAP_AUTH = bool(os.getenv('LDAP_AUTH', False)) if LDAP_AUTH: import ldap from django_auth_ldap.config import LDAPSearch AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') - AUTH_LDAP_SERVER_URI = os.getenv('AUTH_LDAP_SERVER_URI') + AUTH_LDAP_SERVER_URI = os.getenv('AUTH_LDAP_SERVER_URI') AUTH_LDAP_BIND_DN = os.getenv('AUTH_LDAP_BIND_DN') AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD') AUTH_LDAP_USER_SEARCH = LDAPSearch( diff --git a/vue/src/apps/ModelListView/ModelListView.vue b/vue/src/apps/ModelListView/ModelListView.vue index 0f7cd74994..2d79c865a2 100644 --- a/vue/src/apps/ModelListView/ModelListView.vue +++ b/vue/src/apps/ModelListView/ModelListView.vue @@ -435,7 +435,7 @@ export default { getRecipes: function (col, item) { let parent = {} // TODO: make this generic - let params = { pageSize: 50 } + let params = { pageSize: 50, random: true } params[this.this_recipe_param] = item.id console.log("RECIPE PARAM", this.this_recipe_param, params, item.id) this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params) diff --git a/vue/src/apps/RecipeEditView/RecipeEditView.vue b/vue/src/apps/RecipeEditView/RecipeEditView.vue index 61cad5e0b0..9b38fd1ce8 100644 --- a/vue/src/apps/RecipeEditView/RecipeEditView.vue +++ b/vue/src/apps/RecipeEditView/RecipeEditView.vue @@ -49,13 +49,13 @@
- +
- +
- +
@@ -343,7 +343,7 @@
- @@ -623,9 +623,10 @@ export default { this.sortIngredients(s) } - if (this.recipe.waiting_time === ''){ this.recipe.waiting_time = 0} - if (this.recipe.working_time === ''){ this.recipe.working_time = 0} - if (this.recipe.servings === ''){ this.recipe.servings = 0} + if (this.recipe.waiting_time === '' || isNaN(this.recipe.waiting_time)){ this.recipe.waiting_time = 0} + if (this.recipe.working_time === ''|| isNaN(this.recipe.working_time)){ this.recipe.working_time = 0} + if (this.recipe.servings === ''|| isNaN(this.recipe.servings)){ this.recipe.servings = 0} + apiFactory.updateRecipe(this.recipe_id, this.recipe, {}).then((response) => { diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index c1adc7ddd9..471e337d4f 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -304,7 +304,7 @@ export default { this.settings?.search_keywords?.length === 0 && this.settings?.search_foods?.length === 0 && this.settings?.search_books?.length === 0 && - this.settings?.pagination_page === 1 && + // this.settings?.pagination_page === 1 && !this.random_search && this.settings?.search_ratings === undefined ) { diff --git a/vue/src/components/StepComponent.vue b/vue/src/components/StepComponent.vue index 403b58299a..cf58bb27c9 100644 --- a/vue/src/components/StepComponent.vue +++ b/vue/src/components/StepComponent.vue @@ -112,8 +112,8 @@ {{ step.step_recipe_data.name }}
- +