diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 8a4fc30a..468f1128 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -221,8 +221,8 @@ class IngredientParser: # some people/languages put amount and unit at the end of the ingredient string # if something like this is detected move it to the beginning so the parser can handle it - if len(ingredient) < 1000 and re.search(r'^([A-z])+(.)*[1-9](\d)*\s([A-z])+', ingredient): - match = re.search(r'[1-9](\d)*\s([A-z])+', ingredient) + if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient): + match = re.search(r'[1-9](\d)*\s*([^\W\d_])+', ingredient) print(f'reording from {ingredient} to {ingredient[match.start():match.end()] + " " + ingredient.replace(ingredient[match.start():match.end()], "")}') ingredient = ingredient[match.start():match.end()] + ' ' + ingredient.replace(ingredient[match.start():match.end()], '') diff --git a/cookbook/tests/other/test_ingredient_parser.py b/cookbook/tests/other/test_ingredient_parser.py index 90d5f0b7..d61cbc69 100644 --- a/cookbook/tests/other/test_ingredient_parser.py +++ b/cookbook/tests/other/test_ingredient_parser.py @@ -66,7 +66,9 @@ def test_ingredient_parser(): 1.0, 'Lorem', 'ipsum', 'dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut l Lorem ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut l'), "1 LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutl": ( 1.0, None, 'LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingeli', - 'LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutl') + 'LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutl'), + "砂糖 50g": (50, "g", "砂糖", ""), + "卵 4個": (4, "個", "卵", "") } # for German you could say that if an ingredient does not have diff --git a/cookbook/views/api.py b/cookbook/views/api.py index a4c210ff..feaa342c 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -54,7 +54,7 @@ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, CustomIsShared, CustomIsSpaceOwner, CustomIsUser, group_required, - is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission, CustomUserPermission, CustomTokenHasReadWriteScope, CustomTokenHasScope) + 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.scrapers.scrapers import text_scraper @@ -528,10 +528,10 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin): shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id') # onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users])) - return self.queryset\ - .annotate(shopping_status=Exists(shopping_status))\ - .prefetch_related('onhand_users', 'inherit_fields', 'child_inherit_fields', 'substitute')\ - .select_related('recipe', 'supermarket_category') + return self.queryset \ + .annotate(shopping_status=Exists(shopping_status)) \ + .prefetch_related('onhand_users', 'inherit_fields', 'child_inherit_fields', 'substitute') \ + .select_related('recipe', 'supermarket_category') @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 @@ -1380,9 +1380,8 @@ def sync_all(request): return redirect('list_recipe_import') -@group_required('user') def share_link(request, pk): - if request.space.allow_sharing: + if request.space.allow_sharing and has_group_permission(request.user, 'user'): recipe = get_object_or_404(Recipe, pk=pk, space=request.space) link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space) return JsonResponse({'pk': pk, 'share': link.uuid, diff --git a/docs/features/external_recipes.md b/docs/features/external_recipes.md index 29a0be42..4d97904b 100644 --- a/docs/features/external_recipes.md +++ b/docs/features/external_recipes.md @@ -18,7 +18,7 @@ Lastly you will need to sync with the external path and import recipes you desir There are better ways to do this but they are currently not implemented A `Storage Backend` is a remote storage location where files are **read** from. -To add a new backend click on `Storage Data` and then on `Storage Backends`. +To add a new backend click on `username >> External Recipes >> Manage External Storage >> the + next to Storage Backend List`. There click the plus button. The basic configuration is the same for all providers. @@ -37,15 +37,23 @@ The basic configuration is the same for all providers. !!! info There is currently no way to upload files through the webinterface. This is a feature that might be added later. -The local provider does not need any configuration. -For the monitor you will need to define a valid path on your host system. +The local provider does not need any configuration (username, password, token or URL). +For the monitor you will need to define a valid path on your host system. (Path) The Path depends on your setup and can be both relative and absolute. -If you use docker the default directory is `/opt/recipes/`. !!! warning "Volume" By default no data other than the mediafiles and the database is persisted. If you use the local provider make sure to mount the path you choose to monitor to your host system in order to keep it persistent. +#### Docker +If you use docker the default directory is `/opt/recipes/`. +add +``` + - ./externalfiles:/opt/recipes/externalfiles +``` +to your docker-compose.yml file under the `web_recipes >> volumes` section. This will create a folder in your docker directory named `externalfiles` under which you could choose to store external pdfs (you could of course store them anywhere, just change `./externalfiles` to your preferred location). +save the docker-compose.yml and restart your docker container. + ### Dropbox | Field | Value | @@ -66,13 +74,13 @@ If you use docker the default directory is `/opt/recipes/`. | Url | Nextcloud Server URL (e.g. `https://cloud.mydomain.com`) | | Path | (optional) webdav path (e.g. `/remote.php/dav/files/vabene1111`). If no path is supplied `/remote.php/dav/files/` plus your username will be used. | -## Adding Synced Paths -To add a new path from your Storage backend to the sync list, go to `Storage Data >> Configure Sync` and +## Adding External Recipes +To add a new path from your Storage backend to the sync list, go to `username >> External Recipes` and select the storage backend you want to use. -Then enter the path you want to monitor starting at the storage root (e.g. `/Folder/RecipesFolder`) and save it. +Then enter the path you want to monitor starting at the storage root (e.g. `/Folder/RecipesFolder`, or `/opt/recipes/externalfiles' in the docker example above) and save it. ## Syncing Data -To sync the recipes app with the storage backends press `Sync now` under `Storage Data >> Configure Sync`. +To sync the recipes app with the storage backends press `Sync now` under `username >> External Recipes` ## Discovered Recipes All files found by the sync can be found under `Manage Data >> Discovered recipes`. diff --git a/docs/system/backup.md b/docs/system/backup.md index 20e865f9..cf5a2854 100644 --- a/docs/system/backup.md +++ b/docs/system/backup.md @@ -31,7 +31,7 @@ The filenames consist of `_`. In case you screw up real The standard docker build of tandoor uses postgresql as the back end database. This can be backed up using a function called "dumpall". This generates a .SQL file containing a list of commands for a postgresql server to use to rebuild your database. You will also need to back up the media files separately. Making a full copy of the docker directory can work as a back up, but only if you know you will be using the same hardware, os, and postgresql version upon restore. If not, then the different version of postgresql won't be compatible with the existing tables. -You can back up from docker even when the tandoor container is failing, so long as the postgresql database has started successfully. +You can back up from docker even when the tandoor container is failing, so long as the postgresql database has started successfully. When using this backup method, ensure that your recipes have imported successfully. One user reported only the titles and images importing on first try, requiring a second run of the import command. the following commands assume that your docker-compose files are in a folder called "docker". replace "docker_db_recipes_1" with the name of your db container. The commands also assume you use a backup name of pgdump.sql. It's a good idea to include a date in this filename, so that successive backups do not get deleted. To back up: @@ -47,3 +47,12 @@ cat pgdump.sql | sudo docker exec -i docker_db_recipes_1 psql postgres -U django ``` This connects to the postgres table instead of the actual dgangodb table, as the import function needs to delete the table, which can't be dropped off you're connected to it. +## Backup using export and import +You can now export recipes from Tandoor using the export function. This method requires a working web interface. +1. Click on a recipe +2. Click on the three meatballs then export +3. Select the all recipes toggle and then export. This should download a zip file. + +Import: +Go to Import > from app > tandoor and select the zip file you want to import from. + diff --git a/docs/system/updating.md b/docs/system/updating.md index b38f3fd3..82084864 100644 --- a/docs/system/updating.md +++ b/docs/system/updating.md @@ -11,7 +11,6 @@ For all setups using Docker the updating process look something like this 2. Pull the latest image using `docker-compose pull` 3. Start the container again using `docker-compose up -d` - ## Manual For all setups using a manual installation updates usually involve downloading the latest source code from GitHub. @@ -20,4 +19,4 @@ After that make sure to run: 1. `manage.py collectstatic` 2. `manage.py migrate` -To apply all new migrations and collect new static files. \ No newline at end of file +To apply all new migrations and collect new static files. diff --git a/requirements.txt b/requirements.txt index e895c4e7..e7046a8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,16 +6,16 @@ django-cleanup==6.0.0 django-crispy-forms==1.14.0 django-tables2==2.4.1 djangorestframework==3.13.1 -drf-writable-nested==0.6.4 +drf-writable-nested==0.7.0 django-oauth-toolkit==2.1.0 bleach==5.0.1 bleach-allowlist==1.0.3 gunicorn==20.1.0 lxml==4.9.1 Markdown==3.3.7 -Pillow==9.1.1 +Pillow==9.2.0 psycopg2-binary==2.9.3 -python-dotenv==0.20.0 +python-dotenv==0.21.0 requests==2.28.1 six==1.16.0 webdavclient3==3.14.6 diff --git a/vue/package.json b/vue/package.json index e51a30fd..324ca55b 100644 --- a/vue/package.json +++ b/vue/package.json @@ -18,7 +18,7 @@ "babel-core": "^6.26.3", "babel-loader": "^8.2.5", "bootstrap-vue": "^2.21.2", - "core-js": "^3.20.3", + "core-js": "^3.25.0", "html2pdf.js": "^0.10.1", "lodash": "^4.17.21", "mavon-editor": "^2.10.4", @@ -56,7 +56,7 @@ "babel-eslint": "^10.1.0", "eslint": "^7.28.0", "eslint-plugin-vue": "^8.7.1", - "typescript": "~4.7.2", + "typescript": "~4.8.2", "vue-cli-plugin-i18n": "^2.3.1", "webpack-bundle-tracker": "1.5.0", "workbox-expiration": "^6.3.0", diff --git a/vue/src/apps/RecipeEditView/RecipeEditView.vue b/vue/src/apps/RecipeEditView/RecipeEditView.vue index ce0e4f7b..f1994a4a 100644 --- a/vue/src/apps/RecipeEditView/RecipeEditView.vue +++ b/vue/src/apps/RecipeEditView/RecipeEditView.vue @@ -546,7 +546,7 @@ diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index 4968f0b4..16b22e5d 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -38,7 +38,7 @@
{{ $t("Preparation") }}
- {{ recipe.working_time }} {{ $t("min") }} + {{ working_time }}
@@ -50,7 +50,7 @@
{{ $t("Waiting") }}
- {{ recipe.waiting_time }} {{ $t("min") }} + {{ waiting_time }}
@@ -160,7 +160,7 @@ import "bootstrap-vue/dist/bootstrap-vue.css" import {apiLoadRecipe} from "@/utils/api" import RecipeContextMenu from "@/components/RecipeContextMenu" -import {ResolveUrlMixin, ToastMixin} from "@/utils/utils" +import {ResolveUrlMixin, ToastMixin, calculateHourMinuteSplit} from "@/utils/utils" import PdfViewer from "@/components/PdfViewer" import ImageViewer from "@/components/ImageViewer" @@ -206,6 +206,10 @@ export default { ingredient_count() { return this.recipe?.steps.map((x) => x.ingredients).flat().length }, + working_time: function() { + return calculateHourMinuteSplit(this.recipe.working_time)}, + waiting_time: function() { + return calculateHourMinuteSplit(this.recipe.waiting_time)}, }, data() { return { diff --git a/vue/src/components/RecipeCard.vue b/vue/src/components/RecipeCard.vue index 80cbcdb2..13a68a36 100755 --- a/vue/src/components/RecipeCard.vue +++ b/vue/src/components/RecipeCard.vue @@ -8,8 +8,8 @@
- {{ recipe.working_time }} {{ $t("min") }} - {{ recipe.waiting_time }} {{ $t("min") }} + {{ working_time }} + {{ waiting_time }}
@@ -59,7 +59,7 @@