Compare commits

..

199 Commits

Author SHA1 Message Date
vabene1111
0862c0f0bc add migration to automatically assign group to existing superusers 2021-04-06 18:10:22 +02:00
vabene1111
a811d4a55c Merge branch 'beta' into develop 2021-04-05 18:21:55 +02:00
vabene1111
01a53ad8ec paprika improvements and importer fixes 2021-04-05 17:55:06 +02:00
vabene1111
c7e20716f5 more testing deployments 2021-04-05 17:16:26 +02:00
vabene1111
7adc4ad50a testing with newer base image 2021-04-05 16:01:49 +02:00
vabene1111
61ded5094f testing with manual wheel install 2021-04-05 15:45:21 +02:00
vabene1111
7aff5dc44a set cryptography manually 2021-04-05 15:24:00 +02:00
vabene1111
b5e5ff9bf8 testing docker build 2021-04-05 15:03:29 +02:00
vabene1111
a8434ce745 Merge pull request #520 from vabene1111/dependabot/pip/python-dotenv-0.17.0
Bump python-dotenv from 0.16.0 to 0.17.0
2021-04-05 12:16:30 +02:00
vabene1111
cf926295cf Merge pull request #521 from vabene1111/dependabot/pip/pytest-6.2.3
Bump pytest from 6.2.2 to 6.2.3
2021-04-05 12:16:23 +02:00
dependabot[bot]
dbd7ae4adc Bump pytest from 6.2.2 to 6.2.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.2...6.2.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 06:57:49 +00:00
dependabot[bot]
ba20fa1ff5 Bump python-dotenv from 0.16.0 to 0.17.0
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.16.0 to 0.17.0.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v0.16.0...v0.17.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-05 06:57:35 +00:00
vabene1111
a5b92f5672 fixed template render issues 2021-04-04 11:16:48 +02:00
vabene1111
ed412d11b7 Merge pull request #516 from vabene1111/dependabot/pip/pillow-8.2.0
Bump pillow from 8.1.2 to 8.2.0
2021-04-02 13:07:48 +02:00
dependabot[bot]
b36d0620e0 Bump pillow from 8.1.2 to 8.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.2 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.2...8.2.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-02 06:02:28 +00:00
vabene1111
d16d77f640 Merge pull request #511 from vabene1111/dependabot/pip/recipe-scrapers-12.2.2
Bump recipe-scrapers from 12.2.1 to 12.2.2
2021-03-29 20:49:55 +02:00
vabene1111
d72f90b90e Merge pull request #512 from vabene1111/dependabot/pip/djangorestframework-3.12.4
Bump djangorestframework from 3.12.3 to 3.12.4
2021-03-29 20:49:49 +02:00
vabene1111
9c78bcd662 Merge pull request #513 from vabene1111/dependabot/pip/python-dotenv-0.16.0
Bump python-dotenv from 0.15.0 to 0.16.0
2021-03-29 20:49:45 +02:00
vabene1111
b8c8fc3b58 Merge pull request #514 from vabene1111/dependabot/pip/gunicorn-20.1.0
Bump gunicorn from 20.0.4 to 20.1.0
2021-03-29 20:49:39 +02:00
dependabot[bot]
e1110000ab Bump gunicorn from 20.0.4 to 20.1.0
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 20.0.4 to 20.1.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/20.0.4...20.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 06:54:27 +00:00
dependabot[bot]
00194f68bf Bump python-dotenv from 0.15.0 to 0.16.0
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v0.15.0...v0.16.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 06:54:20 +00:00
dependabot[bot]
4f4c324c30 Bump djangorestframework from 3.12.3 to 3.12.4
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.12.3 to 3.12.4.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 06:54:13 +00:00
dependabot[bot]
106129e779 Bump recipe-scrapers from 12.2.1 to 12.2.2
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 12.2.1 to 12.2.2.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/12.2.1...12.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 06:54:06 +00:00
vabene1111
c5d509bf9e added default space/group for o auth signup 2021-03-28 22:16:12 +02:00
vabene1111
8b0f9bc2e7 fixed broken asset link in base html 2021-03-28 22:00:51 +02:00
vabene1111
4db17874c4 added meal master import 2021-03-28 21:50:52 +02:00
vabene1111
f6c491a8e6 added rezkonv importer 2021-03-28 21:13:26 +02:00
vabene1111
e4a9f56352 update integration file split 2021-03-28 19:49:03 +02:00
vabene1111
c1287407a3 added export support for recipe sage 2021-03-28 19:29:09 +02:00
vabene1111
ba1e18410a Merge pull request #510 from ModdedGamers/develop
Cheftap import tweak
2021-03-28 19:08:47 +02:00
vabene1111
7f8e29f1bc added recipe sage and domestica imports 2021-03-28 18:58:37 +02:00
Modded Gamers
6334bee608 Update import_export.md 2021-03-28 12:38:56 -04:00
Modded Gamers
681c57201a Cheftap import tweak
When I submitted my testing .zip, I didn't submit my normal export, because that has some personal data in the recipes. Instead, I made one by had, which was a mistake. The folder contained inside is also called cheftap_export.
2021-03-28 12:37:59 -04:00
vabene1111
738f0781b2 added import for pepperplate 2021-03-28 18:22:30 +02:00
vabene1111
6143e31e1a tweaked chef tap import 2021-03-28 18:04:12 +02:00
vabene1111
d6e6ab24c2 Cheftap import 2021-03-28 17:56:53 +02:00
vabene1111
0492182803 Merge pull request #503 from vabene1111/dependabot/pip/lxml-4.6.3
Bump lxml from 4.6.2 to 4.6.3
2021-03-28 17:10:36 +02:00
vabene1111
22713d884e Merge pull request #504 from vabene1111/dependabot/pip/django-crispy-forms-1.11.2
Bump django-crispy-forms from 1.11.1 to 1.11.2
2021-03-28 17:10:30 +02:00
vabene1111
bd14b77f62 Merge pull request #507 from vabene1111/dependabot/pip/djangorestframework-3.12.3
Bump djangorestframework from 3.12.2 to 3.12.3
2021-03-28 17:10:27 +02:00
dependabot[bot]
6a89ed1bb6 Bump djangorestframework from 3.12.2 to 3.12.3
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.12.2 to 3.12.3.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.12.2...3.12.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-26 05:58:54 +00:00
vabene1111
25717f6a79 space max 2021-03-24 00:06:34 +01:00
dependabot[bot]
d3ecd52fd2 Bump django-crispy-forms from 1.11.1 to 1.11.2
Bumps [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) from 1.11.1 to 1.11.2.
- [Release notes](https://github.com/django-crispy-forms/django-crispy-forms/releases)
- [Changelog](https://github.com/django-crispy-forms/django-crispy-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/django-crispy-forms/django-crispy-forms/compare/1.11.1...1.11.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-22 07:16:11 +00:00
dependabot[bot]
1830eb4edc Bump lxml from 4.6.2 to 4.6.3
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.2 to 4.6.3.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.2...lxml-4.6.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-22 07:16:06 +00:00
vabene1111
81a8734fac Merge pull request #499 from sebimarkgraf/fix/432-ios-webclip-support
Add iOS webclip icon support
2021-03-20 22:41:13 +01:00
vabene1111
abcef54e72 Merge pull request #498 from vabene1111/dependabot/pip/recipe-scrapers-12.2.1
Bump recipe-scrapers from 12.2.0 to 12.2.1
2021-03-20 22:30:43 +01:00
vabene1111
e15c92cda5 Merge pull request #501 from smilerz/main_fork
fix json direct import when wrapped in @graph
2021-03-20 22:26:06 +01:00
smilerz
45dba6fad2 fix json direct import when wrapped in @graph 2021-03-19 13:23:55 -05:00
Sebastian Markgraf
f67bb3cb98 Add generated icons from icongenerator. 2021-03-19 15:49:12 +01:00
Sebastian Markgraf
53b584da56 Fix errors in favicon SVG. 2021-03-19 12:48:58 +01:00
vabene1111
58fc26904b fixed url importer stuff 2021-03-19 10:05:17 +01:00
dependabot[bot]
7527646319 Bump recipe-scrapers from 12.2.0 to 12.2.1
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 12.2.0 to 12.2.1.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/12.2.0...12.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-19 05:51:22 +00:00
vabene1111
d3b1139a22 fixed migration 2021-03-19 00:33:06 +01:00
vabene1111
f04a51c1ad fixed empty units/foods (again) 2021-03-19 00:19:29 +01:00
vabene1111
7fbff9f3b5 sligthly improved login redirects 2021-03-18 23:42:24 +01:00
vabene1111
bb0f3e1778 build recipe view 2021-03-18 23:25:25 +01:00
vabene1111
6ab8d6bd0d Merge pull request #497 from smilerz/recipe_description
Recipe description
2021-03-18 23:23:35 +01:00
vabene1111
69a51e0640 fixed broken messages queing up 2021-03-18 23:20:31 +01:00
vabene1111
1425e795ff telegram create list if not exists and remove debug code 2021-03-18 22:51:06 +01:00
vabene1111
afadc61d5d telegram bot 2021-03-18 22:34:53 +01:00
smilerz
ce8524b247 added data validation on name and description length 2021-03-18 16:03:13 -05:00
smilerz
fd09ae1510 add description to recipe on submit 2021-03-18 15:55:11 -05:00
smilerz
9137fbfb97 Merge remote-tracking branch 'upstream/develop' into recipe_description 2021-03-18 15:33:50 -05:00
vabene1111
f0ac55c20e Merge pull request #388 from cesarblancg/develop
Optimized dockerfile
2021-03-18 20:53:14 +01:00
vabene1111
249663bd91 Merge branch 'develop' into develop 2021-03-18 20:53:08 +01:00
vabene1111
55920501b8 small tweaks 2021-03-18 20:48:28 +01:00
vabene1111
cc3e00e75f Merge branch 'develop' into url_import_recipes
# Conflicts:
#	cookbook/helper/recipe_url_import.py
#	cookbook/tests/api/test_api_keyword.py
#	cookbook/tests/other/test_edits_recipe.py
#	cookbook/views/api.py
#	requirements.txt
2021-03-18 20:38:51 +01:00
vabene1111
661f7ae789 fixed slice fix again 2021-03-18 20:34:37 +01:00
vabene1111
8bfbd96398 fixed slicing issue 2021-03-18 20:24:42 +01:00
vabene1111
8a051b531d import response view 2021-03-18 20:00:13 +01:00
vabene1111
de9456e3d7 import log api 2021-03-18 18:48:29 +01:00
vabene1111
950315936e import log working 2021-03-18 18:30:12 +01:00
vabene1111
af1bc19fd8 fixed importer threading stuff 2021-03-18 18:08:22 +01:00
vabene1111
906da25301 prevent duplicate imports 2021-03-18 17:02:02 +01:00
vabene1111
fe1ddf1237 improved paprika importer 2021-03-18 14:43:19 +01:00
vabene1111
48c90c483a merged PR 474 adding manual json import 2021-03-18 12:36:00 +01:00
vabene1111
3c1b6a5f3a updated unraid docs image 2021-03-18 12:08:12 +01:00
vabene1111
780c929162 fixed url import space 2021-03-18 12:07:02 +01:00
vabene1111
e00b6b9293 fixed meal plan creation from recipe context menu 2021-03-18 11:56:47 +01:00
vabene1111
8c9ee37c46 updated external image viewer 2021-03-18 11:53:32 +01:00
vabene1111
45a0bda758 updated transaltion 2021-03-18 11:53:14 +01:00
vabene1111
e71417e77f Merge pull request #480 from auanasgheps/patch-1
Update Italian Translation
2021-03-18 11:19:58 +01:00
vabene1111
29e6eda9cd Merge pull request #463 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_nl
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'nl'
2021-03-18 11:19:18 +01:00
vabene1111
32c984c7f0 Merge pull request #482 from vabene1111/dependabot/pip/pillow-8.1.2
Bump pillow from 8.1.0 to 8.1.2
2021-03-18 11:19:02 +01:00
vabene1111
b68d2ba384 Merge pull request #485 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_es
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'es'
2021-03-18 11:18:28 +01:00
vabene1111
daf0be37fd upgraded javascritp dependencies 2021-03-18 11:18:09 +01:00
vabene1111
2e625715cc updated help request template 2021-03-18 11:14:34 +01:00
vabene1111
6a25428b3c fixed shopping list share permission 2021-03-18 11:04:55 +01:00
vabene1111
cb78f75f19 fixed shopping list auto sync flickering 2021-03-18 10:56:12 +01:00
vabene1111
77cfcb4602 and pytest django 2021-03-17 23:52:41 +01:00
vabene1111
d2a4a9d953 addded pytest as dependency 2021-03-17 23:50:37 +01:00
vabene1111
9280540927 updated ci command 2021-03-17 23:48:49 +01:00
vabene1111
2f77532111 Merge branch 'feature/spaces' into develop 2021-03-17 23:47:35 +01:00
vabene1111
2149f4034b several shopping list improvements 2021-03-17 23:28:28 +01:00
vabene1111
76eeed1a77 all tests migrated and improved 2021-03-17 22:10:58 +01:00
vabene1111
ad0d802e41 more tests 2021-03-17 21:18:54 +01:00
vabene1111
e41464cb31 recipe api test 2021-03-17 20:53:12 +01:00
vabene1111
ab3f7bf671 step api test 2021-03-17 20:46:49 +01:00
vabene1111
e968a57c06 shopping list api tests and refactors 2021-03-17 20:42:17 +01:00
vabene1111
641feede74 space nested serializers 2021-03-17 19:55:34 +01:00
vabene1111
b48708652f delete model check fixes + sync log 2021-03-17 00:27:53 +01:00
vabene1111
c2addc1121 storage api test 2021-03-17 00:14:54 +01:00
vabene1111
a25109e16c added sync api test 2021-03-17 00:03:36 +01:00
vabene1111
ae81b10dbd added supermarket api test 2021-03-16 23:50:51 +01:00
vabene1111
4d6d84bf5b fixed some tests + added user pref tests 2021-03-16 23:46:54 +01:00
vabene1111
a8a132e2a1 fixed recipe book entry api and added test 2021-03-12 12:33:20 +01:00
vabene1111
3b0413c30e add test for recipe book 2021-03-12 10:50:08 +01:00
vabene1111
d767743b64 user name api test 2021-03-12 10:16:51 +01:00
vabene1111
e8f7caebd1 test unit api 2021-03-12 09:52:46 +01:00
vabene1111
a7b7272bec view log api test 2021-03-12 09:50:20 +01:00
vabene1111
d7402f60c5 meal plan api tests 2021-03-12 09:44:16 +01:00
vabene1111
d049cf6d3d ingredient test finished 2021-03-11 21:50:54 +01:00
vabene1111
fdcdf6a026 added food test 2021-03-11 18:23:00 +01:00
vabene1111
7e38e946a5 fixed various space related bugs 2021-03-11 16:41:05 +01:00
vabene1111
b552badff7 fixed limit random recipe 2021-03-11 15:37:37 +01:00
transifex-integration[bot]
d10c84b66e Apply translations in es
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'es' language.
2021-03-10 23:51:17 +00:00
vabene1111
e7f8d58a7d added test for cook log 2021-03-10 21:25:18 +01:00
vabene1111
528f329ebb new keyword api test working 2021-03-10 20:31:00 +01:00
vabene1111
b9b7a125f0 more tests working but scopes broken 2021-03-10 19:46:08 +01:00
vabene1111
4742056223 tests basically working 2021-03-10 18:18:19 +01:00
vabene1111
ba4c3b95e5 removed deprecated functions/packages 2021-03-10 18:17:31 +01:00
dependabot[bot]
4c03520371 Bump pillow from 8.1.0 to 8.1.2
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.0 to 8.1.2.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.0...8.1.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 06:58:23 +00:00
smilerz
1f5ddd9af7 limit description in search to 2 lines 2021-03-06 17:36:51 -06:00
smilerz
d3e6b34a63 added description to search and recipe view 2021-03-06 16:32:59 -06:00
Oliver Cervera
323f424630 Update Italian Translation
Italian translation updates.
Manual PR since Transifex doesn't do it.
2021-03-06 13:03:03 +01:00
smilerz
eed6e9d3a5 catch index error 2021-03-04 15:30:20 -06:00
smilerz
f4af7ffb0b except do it right this time 2021-03-04 15:03:33 -06:00
smilerz
3b072d5dd9 check for scrape.schema before using it 2021-03-04 14:59:56 -06:00
smilerz
11240dcf48 Merge branch 'recipe_description' of github.com:smilerz/recipes into recipe_description 2021-03-04 09:30:27 -06:00
smilerz
0b5fc1a9f4 added description to url_import 2021-03-04 09:20:26 -06:00
smilerz
049449bda3 removed recipe description from url_import template 2021-03-04 09:19:25 -06:00
smilerz
e5a19302f0 added description to url_import 2021-03-04 08:38:01 -06:00
smilerz
e1fa939757 pulled description field from url_import template 2021-03-04 08:33:02 -06:00
smilerz
3da33e364e fixed test 2021-03-03 21:15:12 -06:00
smilerz
bfaed434cc refactored url_import to use recipe-scraper 2021-03-03 21:08:34 -06:00
vabene1111
b6acc17e5a Create url_import.md 2021-02-26 10:02:14 +01:00
vabene1111
5fd03e7cdc basics of pytest 2021-02-25 22:36:20 +01:00
vabene1111
54e71f2910 last commit 2021-02-25 22:20:06 +01:00
vabene1111
c99c944130 fixed normal django test running with scopes 2021-02-25 22:19:51 +01:00
transifex-integration[bot]
94c9185bcf Apply translations in nl
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'nl' language.
2021-02-25 18:59:22 +00:00
vabene1111
74e731e334 inject space context into serializer writes 2021-02-25 16:45:47 +01:00
vabene1111
d01f7409bf removed django-random-queryset
the looping strategy is not efficient when using spacing as ids for a single user might be spaced far apart. A single user also only has a few hundred recipes so using the order by ? method should be more efficient and also automatically be compatible with spaces
2021-02-25 16:44:42 +01:00
vabene1111
29ab6cfb2d Merge branch 'develop' into feature/spaces
# Conflicts:
#	cookbook/views/data.py
#	cookbook/views/views.py
2021-02-25 15:59:32 +01:00
vabene1111
59b2da933d fixed tests 2021-02-25 08:27:22 +01:00
vabene1111
99b5f9a3ec Merge pull request #457 from ProfessorSalty/height-bugfix
Fix height issues in Safari by removing height rules
2021-02-25 08:18:37 +01:00
vabene1111
9057eac42c Merge pull request #460 from vabene1111/dependabot/pip/markdown-3.3.4
Bump markdown from 3.3.3 to 3.3.4
2021-02-25 08:18:27 +01:00
vabene1111
24b5cdff85 Merge pull request #459 from smilerz/fix-456
Fix 456
2021-02-25 08:14:55 +01:00
dependabot[bot]
f2630c3ba0 Bump markdown from 3.3.3 to 3.3.4
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.3.3...3.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-25 06:02:05 +00:00
smilerz
21740522bc fixed URL import when ingredient is a string 2021-02-24 20:13:06 -06:00
smilerz
47090ce863 added tests for add keyword and add duplicate keyword 2021-02-24 18:52:43 -06:00
smilerz
3ac22c08ff fixed duplicate keyword handling 2021-02-24 18:52:04 -06:00
smilerz
cc62b088fd fix URL import when recipeYield is a list 2021-02-24 17:52:40 -06:00
Greg Smith
2c34425135 Remove fixed height rule 2021-02-24 17:36:06 -05:00
vabene1111
205f76d128 fix that does not really fix anything
see #453/454
2021-02-23 21:43:14 +01:00
vabene1111
4147bc61c7 Merge pull request #390 from smilerz/develop
Add keywords during edit & url import
2021-02-23 21:12:45 +01:00
vabene1111
dfae453925 Merge pull request #450 from albcp/develop
Add environment variable to include Sub-Path in resolve.js.
2021-02-23 21:09:56 +01:00
vabene1111
7507cae44c added some info to .env for DB_OPTIONS 2021-02-23 20:56:53 +01:00
vabene1111
28312774bd Merge pull request #434 from angelnu/develop
Add DB_OPTIONS to enable DBs with SSL
2021-02-23 20:55:23 +01:00
vabene1111
058723d583 compiled translations and added hy 2021-02-23 20:54:42 +01:00
vabene1111
db4abdd31d Merge pull request #426 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_it
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'it'
2021-02-23 20:50:35 +01:00
vabene1111
727b0e9e61 Merge pull request #403 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_hy
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'hy'
2021-02-23 20:49:40 +01:00
vabene1111
aa41146735 Merge pull request #448 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_de
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'de'
2021-02-23 20:48:51 +01:00
vabene1111
670fc9bf35 Merge pull request #451 from vabene1111/dependabot/pip/django-autocomplete-light-3.8.2
Bump django-autocomplete-light from 3.8.1 to 3.8.2
2021-02-23 20:48:27 +01:00
vabene1111
d9a5649adc Merge pull request #446 from vabene1111/dependabot/pip/django-3.1.7
Bump django from 3.1.6 to 3.1.7
2021-02-23 20:48:15 +01:00
vabene1111
5ed300a3ea Merge pull request #445 from vabene1111/dependabot/pip/django-crispy-forms-1.11.1
Bump django-crispy-forms from 1.11.0 to 1.11.1
2021-02-23 20:48:04 +01:00
vabene1111
59cc22a877 wip serializer permission 2021-02-23 20:44:13 +01:00
Alberto
8dffc07072 get JS_REVERSE_SCRIPT_PREFIX from SCRIPT_NAME 2021-02-23 18:57:59 +01:00
dependabot[bot]
76c7ad1ff5 Bump django-autocomplete-light from 3.8.1 to 3.8.2
Bumps [django-autocomplete-light](https://github.com/yourlabs/django-autocomplete-light) from 3.8.1 to 3.8.2.
- [Release notes](https://github.com/yourlabs/django-autocomplete-light/releases)
- [Changelog](https://github.com/yourlabs/django-autocomplete-light/blob/master/CHANGELOG)
- [Commits](https://github.com/yourlabs/django-autocomplete-light/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-23 06:02:25 +00:00
vabene1111
7f391c25a4 fixed test 2021-02-22 19:59:43 +01:00
Alberto
bccc41d177 Add environment variable to include Sub-Path in resolve.js. 2021-02-22 19:56:33 +01:00
transifex-integration[bot]
cc882082d2 Apply translations in de
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'de' language.
2021-02-22 15:41:54 +00:00
dependabot[bot]
689918c1ac Bump django from 3.1.6 to 3.1.7
Bumps [django](https://github.com/django/django) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.6...3.1.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-22 07:00:34 +00:00
dependabot[bot]
1c43be3899 Bump django-crispy-forms from 1.11.0 to 1.11.1
Bumps [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/django-crispy-forms/django-crispy-forms/releases)
- [Changelog](https://github.com/django-crispy-forms/django-crispy-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/django-crispy-forms/django-crispy-forms/compare/1.11.0...1.11.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-22 07:00:29 +00:00
vabene1111
40387428e7 recipe filter 2021-02-21 16:49:34 +01:00
vabene1111
46fb02376e recipe share basics 2021-02-21 16:37:04 +01:00
vabene1111
24e43e3e2e updated migrations + setup process update 2021-02-21 15:34:35 +01:00
vabene1111
846c660811 usr signup and more 2021-02-21 15:08:43 +01:00
vabene1111
beb4aa634f forms 2021-02-20 23:42:44 +01:00
vabene1111
58c6077925 basic test stuff 2021-02-20 22:56:29 +01:00
vabene1111
d7675d4b80 ingredients and helpers 2021-02-20 21:26:16 +01:00
vabene1111
e2b1115b3b fixed servings blank 2021-02-20 18:50:28 +01:00
vabene1111
96c963795e more spaces work 2021-02-20 18:47:14 +01:00
Vegetto
804adde964 Parse DB_OPTIONS dict 2021-02-20 16:54:42 +01:00
Vegetto
5aa918f478 Document DB_OPTIONS env 2021-02-20 16:53:02 +01:00
Vegetto
a44f72a030 Add DB_OPTIONS to enable DBs with SSL 2021-02-20 16:42:48 +01:00
vabene1111
ad163509b4 updated edit and added space find methods 2021-02-19 18:09:42 +01:00
vabene1111
fb58d35029 space stuff partially working 2021-02-19 15:50:55 +01:00
transifex-integration[bot]
79f823cd62 Apply translations in it
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'it' language.
2021-02-19 13:42:50 +00:00
vabene1111
c42266b82c basic space stuff 2021-02-19 11:53:30 +01:00
smilerz
c60c3f1876 Revert "minor edit"
This reverts commit 35f3ecc7eb.
2021-02-18 07:28:26 -06:00
smilerz
fc5455a0f2 Revert "added new keyword management page"
This reverts commit 6962b0e218.
2021-02-18 07:27:43 -06:00
smilerz
28d8f62af7 Revert "add filter to new keywords page"
This reverts commit c6dd55df4e.
2021-02-18 07:25:31 -06:00
smilerz
c6dd55df4e add filter to new keywords page 2021-02-17 10:45:31 -06:00
smilerz
6962b0e218 added new keyword management page 2021-02-17 07:09:19 -06:00
transifex-integration[bot]
8bf4a32dfd Apply translations in hy
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'hy' language.
2021-02-16 19:31:09 +00:00
smilerz
505650518e Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2021-02-16 07:45:22 -06:00
smilerz
35f3ecc7eb minor edit 2021-02-16 07:44:41 -06:00
smilerz
e0a0eeeecc add keyword during url import 2021-02-09 12:55:40 -06:00
smilerz
4a4dafd69c add keyword during recipe edit 2021-02-09 11:17:31 -06:00
cesarblancg
c857d092b1 Optimized dockerfile 2021-02-04 10:07:43 +01:00
159 changed files with 11814 additions and 6859 deletions

View File

@@ -13,6 +13,7 @@ TIMEZONE=Europe/Berlin
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
DB_ENGINE=django.db.backends.postgresql
# DB_OPTIONS= {} # e.g. {"sslmode":"require"} to enable ssl
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangouser
@@ -57,3 +58,10 @@ REVERSE_PROXY_AUTH=0
# see docs for more information https://vabene1111.github.io/recipes/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 ?
# ATTENTION: This feature might be deprecated in favor of a space join and public viewing system in the future
# default 0 (false), when 1 (true) users will be assigned space and group
# SOCIAL_DEFAULT_ACCESS = 1
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
# SOCIAL_DEFAULT_GROUP=guest

View File

@@ -6,14 +6,16 @@ labels: setup issue
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Issue
## Issue
Please describe your problem here
## Setup Info
Version: (can be found on the system page since v0.8.4)
OS: e.g. Ubuntu 20.02
Other relevant information regarding your problem (proxies, firewalls, etc.)
### `.env`
Please include your `.env` config file (**make sure to remove/replace all secrets**)
```
@@ -25,3 +27,7 @@ When running with docker compose please provide your `docker-compose.yml`
```
docker-compose.yml content
```
### Logs
If you feel like there is anything interesting please post the output of `docker-compose logs` at
container startup and when the issue happens.

22
.github/ISSUE_TEMPLATE/url_import.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Website Import
about: Anything related to website imports
title: ''
labels: enhancement, url_import
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Information
Exact URL you are trying to import from:
When did the issue happen: When pressing the search button with the url / when importing after the page has loaded
Response/Message shown
```
Message
```

View File

@@ -25,4 +25,4 @@ jobs:
python3 manage.py collectstatic_js_reverse
- name: Django Testing project
run: |
python3 manage.py test
pytest

1
.gitignore vendored
View File

@@ -78,3 +78,4 @@ postgresql/
/docker-compose.override.yml
vue/node_modules
.vscode/

View File

@@ -7,6 +7,7 @@
<w>gunicorn</w>
<w>ical</w>
<w>mealie</w>
<w>pepperplate</w>
<w>safron</w>
<w>traefik</w>
</words>

3
.idea/recipes.iml generated
View File

@@ -29,4 +29,7 @@
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="pytest" />
</component>
</module>

View File

@@ -1,18 +1,28 @@
FROM python:3.8-alpine
FROM python:3.9-alpine3.12
RUN apk add --no-cache postgresql-libs gettext zlib libjpeg libxml2-dev libxslt-dev
#Install all dependencies.
RUN apk add --no-cache postgresql-libs gettext zlib libjpeg libxml2-dev libxslt-dev py-cryptography
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
#This port will be used by gunicorn.
EXPOSE 8080
#Create app dir and install requirements.
RUN mkdir /opt/recipes
WORKDIR /opt/recipes
COPY . ./
RUN chmod +x boot.sh
COPY requirements.txt ./
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libressl-dev libffi-dev cargo && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
venv/bin/pip install wheel==0.36.2 && \
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps
#Copy project and execute it.
COPY . ./
RUN chmod +x boot.sh
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@@ -1,25 +1,35 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User, Group
from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation)
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, ImportLog, TelegramBot)
class CustomUserAdmin(UserAdmin):
def has_add_permission(self, request, obj=None):
return False
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
admin.site.unregister(Group)
class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'message')
list_display = ('name', 'created_by', 'message')
admin.site.register(Space, SpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = (
'name', 'theme', 'nav_color',
'default_page', 'search_style', 'comments'
)
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',)
@staticmethod
def name(obj):
@@ -203,3 +213,17 @@ class NutritionInformationAdmin(admin.ModelAdmin):
admin.site.register(NutritionInformation, NutritionInformationAdmin)
class ImportLogAdmin(admin.ModelAdmin):
list_display = ('id', 'type', 'running', 'created_by', 'created_at',)
admin.site.register(ImportLog, ImportLogAdmin)
class TelegramBotAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'created_by',)
admin.site.register(TelegramBot, TelegramBotAdmin)

View File

@@ -3,77 +3,79 @@ from django.conf import settings
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from cookbook.forms import MultiSelectWidget
from cookbook.models import Food, Keyword, Recipe, ShoppingList
with scopes_disabled():
class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(
queryset=Keyword.objects.none(),
widget=MultiSelectWidget,
method='filter_keywords'
)
foods = django_filters.ModelMultipleChoiceFilter(
queryset=Food.objects.none(),
widget=MultiSelectWidget,
method='filter_foods',
label=_('Ingredients')
)
class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(
queryset=Keyword.objects.all(),
widget=MultiSelectWidget,
method='filter_keywords'
)
foods = django_filters.ModelMultipleChoiceFilter(
queryset=Food.objects.all(),
widget=MultiSelectWidget,
method='filter_foods',
label=_('Ingredients')
)
def __init__(self, data=None, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(data, *args, **kwargs)
self.filters['foods'].queryset = Food.objects.filter(space=space).all()
self.filters['keywords'].queryset = Keyword.objects.filter(space=space).all()
@staticmethod
def filter_keywords(queryset, name, value):
if not name == 'keywords':
@staticmethod
def filter_keywords(queryset, name, value):
if not name == 'keywords':
return queryset
for x in value:
queryset = queryset.filter(keywords=x)
return queryset
for x in value:
queryset = queryset.filter(keywords=x)
return queryset
@staticmethod
def filter_foods(queryset, name, value):
if not name == 'foods':
@staticmethod
def filter_foods(queryset, name, value):
if not name == 'foods':
return queryset
for x in value:
queryset = queryset.filter(steps__ingredients__food__name=x).distinct()
return queryset
for x in value:
queryset = queryset.filter(
steps__ingredients__food__name=x
).distinct()
return queryset
@staticmethod
def filter_name(queryset, name, value):
if not name == 'name':
@staticmethod
def filter_name(queryset, name, value):
if not name == 'name':
return queryset
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
queryset = queryset.annotate(similarity=TrigramSimilarity('name', value), ).filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)).order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=value)
return queryset
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2': # noqa: E501
queryset = queryset \
.annotate(similarity=TrigramSimilarity('name', value), ) \
.filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)) \
.order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=value)
return queryset
class Meta:
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']
class Meta:
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']
class IngredientFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class FoodFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Food
fields = ['name']
class Meta:
model = Food
fields = ['name']
class ShoppingListFilter(django_filters.FilterSet):
class ShoppingListFilter(django_filters.FilterSet):
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
data.setdefault("finished", False)
super(ShoppingListFilter, self).__init__(data, *args, **kwargs)
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
data.setdefault("finished", False)
super().__init__(data, *args, **kwargs)
class Meta:
model = ShoppingList
fields = ['finished']
class Meta:
model = ShoppingList
fields = ['finished']

View File

@@ -1,11 +1,12 @@
from django import forms
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from emoji_picker.widgets import EmojiPickerTextInput
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
UserPreference)
UserPreference, SupermarketCategory, MealType, Space)
class SelectWidget(widgets.Select):
@@ -74,18 +75,18 @@ class UserNameForm(forms.ModelForm):
class ExternalRecipeForm(forms.ModelForm):
file_path = forms.CharField(disabled=True, required=False)
storage = forms.ModelChoiceField(
queryset=Storage.objects.all(),
disabled=True,
required=False
)
file_uid = forms.CharField(disabled=True, required=False)
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['keywords'].queryset = Keyword.objects.filter(space=space).all()
class Meta:
model = Recipe
fields = (
'name', 'keywords', 'description', 'servings', 'working_time', 'waiting_time',
'file_path', 'storage', 'file_uid'
'name', 'description', 'servings', 'working_time', 'waiting_time',
'file_path', 'file_uid', 'keywords'
)
labels = {
@@ -97,38 +98,9 @@ class ExternalRecipeForm(forms.ModelForm):
'file_uid': _('Storage UID'),
}
widgets = {'keywords': MultiSelectWidget}
class InternalRecipeForm(forms.ModelForm):
ingredients = forms.CharField(widget=forms.HiddenInput(), required=False)
class Meta:
model = Recipe
fields = (
'name', 'image', 'working_time',
'waiting_time', 'servings', 'keywords'
)
labels = {
'name': _('Name'),
'keywords': _('Keywords'),
'working_time': _('Preparation time in minutes'),
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
'servings': _('Number of servings'),
field_classes = {
'keywords': SafeModelMultipleChoiceField,
}
widgets = {'keywords': MultiSelectWidget}
class ShoppingForm(forms.Form):
recipe = forms.ModelMultipleChoiceField(
queryset=Recipe.objects.filter(internal=True).all(),
widget=MultiSelectWidget
)
markdown_format = forms.BooleanField(
help_text=_('Include <code>- [ ]</code> in list for easier usage in markdown based documents.'), # noqa: E501
required=False,
initial=False
)
class ImportExportBase(forms.Form):
@@ -138,54 +110,81 @@ class ImportExportBase(forms.Form):
MEALIE = 'MEALIE'
CHOWDOWN = 'CHOWDOWN'
SAFRON = 'SAFRON'
CHEFTAP = 'CHEFTAP'
PEPPERPLATE = 'PEPPERPLATE'
RECIPESAGE = 'RECIPESAGE'
DOMESTICA = 'DOMESTICA'
MEALMASTER = 'MEALMASTER'
REZKONV = 'REZKONV'
type = forms.ChoiceField(choices=(
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
(PEPPERPLATE, 'Pepperplate'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'),
))
class ImportForm(ImportExportBase):
files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True}))
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), required=False)
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget)
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none())
all = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['recipes'].queryset = Recipe.objects.filter(space=space).all()
class UnitMergeForm(forms.Form):
prefix = 'unit'
new_unit = forms.ModelChoiceField(
queryset=Unit.objects.all(),
new_unit = SafeModelChoiceField(
queryset=Unit.objects.none(),
widget=SelectWidget,
label=_('New Unit'),
help_text=_('New unit that other gets replaced by.'),
)
old_unit = forms.ModelChoiceField(
queryset=Unit.objects.all(),
old_unit = SafeModelChoiceField(
queryset=Unit.objects.none(),
widget=SelectWidget,
label=_('Old Unit'),
help_text=_('Unit that should be replaced.'),
)
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['new_unit'].queryset = Unit.objects.filter(space=space).all()
self.fields['old_unit'].queryset = Unit.objects.filter(space=space).all()
class FoodMergeForm(forms.Form):
prefix = 'food'
new_food = forms.ModelChoiceField(
queryset=Food.objects.all(),
new_food = SafeModelChoiceField(
queryset=Food.objects.none(),
widget=SelectWidget,
label=_('New Food'),
help_text=_('New food that other gets replaced by.'),
)
old_food = forms.ModelChoiceField(
queryset=Food.objects.all(),
old_food = SafeModelChoiceField(
queryset=Food.objects.none(),
widget=SelectWidget,
label=_('Old Food'),
help_text=_('Food that should be replaced.'),
)
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['new_food'].queryset = Food.objects.filter(space=space).all()
self.fields['old_food'].queryset = Food.objects.filter(space=space).all()
class CommentForm(forms.ModelForm):
prefix = 'comment'
@@ -210,11 +209,23 @@ class KeywordForm(forms.ModelForm):
class FoodForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
self.fields['supermarket_category'].queryset = SupermarketCategory.objects.filter(space=space).all()
class Meta:
model = Food
fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
widgets = {'recipe': SelectWidget}
field_classes = {
'recipe': SafeModelChoiceField,
'supermarket_category': SafeModelChoiceField,
}
class StorageForm(forms.ModelForm):
username = forms.CharField(
@@ -222,18 +233,16 @@ class StorageForm(forms.ModelForm):
required=False
)
password = forms.CharField(
widget=forms.TextInput(
attrs={'autocomplete': 'new-password', 'type': 'password'}
),
widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
required=False,
help_text=_('Leave empty for dropbox and enter app password for nextcloud.') # noqa: E501
help_text=_('Leave empty for dropbox and enter app password for nextcloud.')
)
token = forms.CharField(
widget=forms.TextInput(
attrs={'autocomplete': 'new-password', 'type': 'password'}
),
required=False,
help_text=_('Leave empty for nextcloud and enter api token for dropbox.') # noqa: E501
help_text=_('Leave empty for nextcloud and enter api token for dropbox.')
)
class Meta:
@@ -241,34 +250,63 @@ class StorageForm(forms.ModelForm):
fields = ('name', 'method', 'username', 'password', 'token', 'url', 'path')
help_texts = {
'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'), # noqa: E501
'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'),
}
class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark'
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['book'].queryset = RecipeBook.objects.filter(space=space).all()
class Meta:
model = RecipeBookEntry
fields = ('book',)
field_classes = {
'book': SafeModelChoiceField,
}
class SyncForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['storage'].queryset = Storage.objects.filter(space=space).all()
class Meta:
model = Sync
fields = ('storage', 'path', 'active')
field_classes = {
'storage': SafeModelChoiceField,
}
class BatchEditForm(forms.Form):
search = forms.CharField(label=_('Search String'))
keywords = forms.ModelMultipleChoiceField(
queryset=Keyword.objects.all().order_by('id'),
queryset=Keyword.objects.none(),
required=False,
widget=MultiSelectWidget
)
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['keywords'].queryset = Keyword.objects.filter(space=space).all().order_by('id')
class ImportRecipeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['keywords'].queryset = Keyword.objects.filter(space=space).all()
class Meta:
model = Recipe
fields = ('name', 'keywords', 'file_path', 'file_uid')
@@ -280,16 +318,33 @@ class ImportRecipeForm(forms.ModelForm):
'file_uid': _('File ID'),
}
widgets = {'keywords': MultiSelectWidget}
field_classes = {
'keywords': SafeModelChoiceField,
}
class RecipeBookForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
class Meta:
model = RecipeBook
fields = ('name', 'icon', 'description', 'shared')
widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget}
field_classes = {
'shared': SafeModelMultipleChoiceField,
}
class MealPlanForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
self.fields['meal_type'].queryset = MealType.objects.filter(space=space).all()
self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
def clean(self):
cleaned_data = super(MealPlanForm, self).clean()
@@ -318,15 +373,28 @@ class MealPlanForm(forms.ModelForm):
'date': DateWidget,
'shared': MultiSelectWidget
}
field_classes = {
'recipe': SafeModelChoiceField,
'meal_type': SafeModelChoiceField,
'shared': SafeModelMultipleChoiceField,
}
class InviteLinkForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['space'].queryset = Space.objects.filter(created_by=user).all()
class Meta:
model = InviteLink
fields = ('username', 'group', 'valid_until')
fields = ('username', 'group', 'valid_until', 'space')
help_texts = {
'username': _('A username is not required, if left blank the new user can choose one.') # noqa: E501
}
field_classes = {
'space': SafeModelChoiceField,
}
class UserCreateForm(forms.Form):

View File

@@ -0,0 +1,8 @@
from django.test.runner import DiscoverRunner
from django_scopes import scopes_disabled
class CustomTestRunner(DiscoverRunner):
def run_tests(self, *args, **kwargs):
with scopes_disabled():
return super().run_tests(*args, **kwargs)

View File

@@ -10,7 +10,7 @@ class BaseAutocomplete(autocomplete.Select2QuerySetView):
if not self.request.user.is_authenticated:
return self.model.objects.none()
qs = self.model.objects.all()
qs = self.model.objects.filter(space=self.request.space).all()
if self.q:
qs = qs.filter(name__icontains=self.q)

View File

@@ -1,6 +1,8 @@
import string
import unicodedata
from cookbook.models import Unit, Food
def parse_fraction(x):
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
@@ -157,3 +159,18 @@ def parse(x):
except ValueError:
ingredient = ' '.join(tokens[1:])
return amount, unit.strip(), ingredient.strip(), note.strip()
# small utility functions to prevent emtpy unit/food creation
def get_unit(unit, space):
if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=unit, space=space)
return u
return None
def get_food(food, space):
if len(food) > 0:
f, created = Food.objects.get_or_create(name=food, space=space)
return f
return None

View File

@@ -1,6 +1,9 @@
"""
Source: https://djangosnippets.org/snippets/1703/
"""
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
from cookbook.models import ShareLink
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
@@ -40,8 +43,7 @@ def has_group_permission(user, groups):
return False
groups_allowed = get_allowed_groups(groups)
if user.is_authenticated:
if (user.is_superuser
| bool(user.groups.filter(name__in=groups_allowed))):
if bool(user.groups.filter(name__in=groups_allowed)):
return True
return False
@@ -56,19 +58,12 @@ def is_object_owner(user, obj):
:param obj any object that should be tested
:return: true if user is owner of object, false otherwise
"""
# TODO this could be improved/cleaned up by adding
# get_owner methods to all models that allow owner checks
if not user.is_authenticated:
return False
if user.is_superuser:
return True
if owner := getattr(obj, 'created_by', None):
return owner == user
if owner := getattr(obj, 'user', None):
return owner == user
if getattr(obj, 'get_owner', None):
try:
return obj.get_owner() == user
return False
except:
return False
def is_object_shared(user, obj):
@@ -84,9 +79,7 @@ def is_object_shared(user, obj):
# share checks for relevant objects
if not user.is_authenticated:
return False
if user.is_superuser:
return True
return user in obj.shared.all()
return user in obj.get_shared()
def share_link_valid(recipe, share):
@@ -97,11 +90,7 @@ def share_link_valid(recipe, share):
:return: true if a share link with the given recipe and uuid exists
"""
try:
return (
True
if ShareLink.objects.filter(recipe=recipe, uuid=share).exists()
else False
)
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
except ValidationError:
return False
@@ -119,7 +108,7 @@ def group_required(*groups_required):
def in_groups(u):
return has_group_permission(u, groups_required)
return user_passes_test(in_groups, login_url='view_no_group')
return user_passes_test(in_groups, login_url='view_no_perm')
class GroupRequiredMixin(object):
@@ -131,13 +120,17 @@ class GroupRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required):
messages.add_message(
request,
messages.ERROR,
_('You do not have the required permissions to view this page!') # noqa: E501
)
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
@@ -145,25 +138,22 @@ class OwnerRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(
request,
messages.ERROR,
_('You are not logged in and therefore cannot view this page!')
)
return HttpResponseRedirect(
reverse_lazy('account_login') + '?next=' + request.path
)
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
if not is_object_owner(request.user, self.get_object()):
messages.add_message(
request,
messages.ERROR,
_('You cannot interact with this object as it is not owned by you!') # noqa: E501
)
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self) \
.dispatch(request, *args, **kwargs)
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
# Django Rest Framework Permission classes

View File

@@ -10,9 +10,10 @@ from cookbook.models import Keyword
from django.http import JsonResponse
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from recipe_scrapers import _utils
def get_from_html(html_text, url):
def get_from_html(html_text, url, space):
soup = BeautifulSoup(html_text, "html.parser")
# first try finding ld+json as its most common
@@ -31,7 +32,7 @@ def get_from_html(html_text, url):
if ('@type' in ld_json_item
and ld_json_item['@type'] == 'Recipe'):
return JsonResponse(find_recipe_json(ld_json_item, url))
return JsonResponse(find_recipe_json(ld_json_item, url, space))
except JSONDecodeError:
return JsonResponse(
{
@@ -45,7 +46,7 @@ def get_from_html(html_text, url):
for i in items:
md_json = json.loads(i.json())
if 'schema.org/Recipe' in str(md_json['type']):
return JsonResponse(find_recipe_json(md_json['properties'], url))
return JsonResponse(find_recipe_json(md_json['properties'], url, space))
return JsonResponse(
{
@@ -55,7 +56,7 @@ def get_from_html(html_text, url):
status=400)
def find_recipe_json(ld_json, url):
def find_recipe_json(ld_json, url, space):
if type(ld_json['name']) == list:
try:
ld_json['name'] = ld_json['name'][0]
@@ -69,8 +70,10 @@ def find_recipe_json(ld_json, url):
if 'recipeIngredient' in ld_json:
# some pages have comma separated ingredients in a single array entry
if (len(ld_json['recipeIngredient']) == 1
and len(ld_json['recipeIngredient'][0]) > 30):
and type(ld_json['recipeIngredient']) == list):
ld_json['recipeIngredient'] = ld_json['recipeIngredient'][0].split(',') # noqa: E501
elif type(ld_json['recipeIngredient']) == str:
ld_json['recipeIngredient'] = ld_json['recipeIngredient'].split(',')
for x in ld_json['recipeIngredient']:
if '\n' in x:
@@ -82,6 +85,7 @@ def find_recipe_json(ld_json, url):
for x in ld_json['recipeIngredient']:
if x.replace(' ', '') != '':
x = x.replace('&frac12;', "0.5").replace('&frac14;', "0.25").replace('&frac34;', "0.75")
try:
amount, unit, ingredient, note = parse_ingredient(x)
if ingredient:
@@ -122,28 +126,7 @@ def find_recipe_json(ld_json, url):
ld_json['recipeIngredient'] = []
if 'keywords' in ld_json:
keywords = []
# keywords as string
if type(ld_json['keywords']) == str:
ld_json['keywords'] = ld_json['keywords'].split(',')
# keywords as string in list
if (type(ld_json['keywords']) == list
and len(ld_json['keywords']) == 1
and ',' in ld_json['keywords'][0]):
ld_json['keywords'] = ld_json['keywords'][0].split(',')
# keywords as list
for kw in ld_json['keywords']:
if k := Keyword.objects.filter(name=kw).first():
keywords.append({'id': str(k.id), 'text': str(k).strip()})
else:
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw.strip()})
ld_json['keywords'] = keywords
else:
ld_json['keywords'] = []
ld_json['keywords'] = parse_keywords(listify_keywords(ld_json['keywords']), space)
if 'recipeInstructions' in ld_json:
instructions = ''
@@ -218,19 +201,143 @@ def find_recipe_json(ld_json, url):
else:
ld_json['prepTime'] = 0
ld_json['servings'] = 1
try:
if 'recipeYield' in ld_json:
if type(ld_json['recipeYield']) == str:
ld_json['servings'] = int(re.findall(r'\b\d+\b', ld_json['recipeYield'])[0])
elif type(ld_json['recipeYield']) == list:
ld_json['servings'] = int(re.findall(r'\b\d+\b', ld_json['recipeYield'][0])[0])
except Exception as e:
print(e)
ld_json['servings'] = 1
for key in list(ld_json):
if key not in [
'prepTime', 'cookTime', 'image', 'recipeInstructions',
'keywords', 'name', 'recipeIngredient', 'servings'
'keywords', 'name', 'recipeIngredient', 'servings', 'description'
]:
ld_json.pop(key, None)
return ld_json
def get_from_scraper(scrape, space):
# converting the scrape_me object to the existing json format based on ld+json
recipe_json = {}
recipe_json['name'] = scrape.title()
try:
description = scrape.schema.data.get("description") or ''
recipe_json['prepTime'] = _utils.get_minutes(scrape.schema.data.get("prepTime")) or 0
recipe_json['cookTime'] = _utils.get_minutes(scrape.schema.data.get("cookTime")) or 0
except AttributeError:
description = ''
recipe_json['prepTime'] = 0
recipe_json['cookTime'] = 0
recipe_json['description'] = description
try:
servings = scrape.yields()
servings = int(re.findall(r'\b\d+\b', servings)[0])
except (AttributeError, ValueError, IndexError):
servings = 1
recipe_json['servings'] = servings
if recipe_json['cookTime'] + recipe_json['prepTime'] == 0:
try:
recipe_json['prepTime'] = scrape.total_time()
except AttributeError:
pass
try:
recipe_json['image'] = scrape.image()
except AttributeError:
pass
keywords = []
try:
if scrape.schema.data.get("keywords"):
keywords += listify_keywords(scrape.schema.data.get("keywords"))
if scrape.schema.data.get('recipeCategory'):
keywords += listify_keywords(scrape.schema.data.get("recipeCategory"))
if scrape.schema.data.get('recipeCuisine'):
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), space)
except AttributeError:
recipe_json['keywords'] = keywords
try:
ingredients = []
for x in scrape.ingredients():
try:
amount, unit, ingredient, note = parse_ingredient(x)
if ingredient:
ingredients.append(
{
'amount': amount,
'unit': {
'text': unit,
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': ingredient,
'id': random.randrange(10000, 99999)
},
'note': note,
'original': x
}
)
except Exception:
ingredients.append(
{
'amount': 0,
'unit': {
'text': '',
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': x,
'id': random.randrange(10000, 99999)
},
'note': '',
'original': x
}
)
recipe_json['recipeIngredient'] = ingredients
except AttributeError:
recipe_json['recipeIngredient'] = ingredients
try:
recipe_json['recipeInstructions'] = scrape.instructions()
except AttributeError:
recipe_json['recipeInstructions'] = ""
recipe_json['recipeInstructions'] += "\n\nImported from " + scrape.url
return recipe_json
def parse_keywords(keyword_json, space):
keywords = []
# keywords as list
for kw in keyword_json:
if k := Keyword.objects.filter(name=kw, space=space).first():
keywords.append({'id': str(k.id), 'text': str(k)})
else:
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw})
return keywords
def listify_keywords(keyword_list):
# keywords as string
if type(keyword_list) == str:
keyword_list = keyword_list.split(',')
# keywords as string in list
if (type(keyword_list) == list
and len(keyword_list) == 1
and ',' in keyword_list[0]):
keyword_list = keyword_list[0].split(',')
return [x.strip() for x in keyword_list]

View File

@@ -0,0 +1,33 @@
from django.shortcuts import redirect
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from cookbook.views import views
class ScopeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
if request.path.startswith('/admin/'):
with scopes_disabled():
return self.get_response(request)
with scopes_disabled():
if request.user.userpreference.space is None and not reverse('account_logout') in request.path:
return views.no_space(request)
if request.user.groups.count() == 0 and not reverse('account_logout') in request.path:
return views.no_groups(request)
request.space = request.user.userpreference.space
# with scopes_disabled():
with scope(space=request.space):
return self.get_response(request)
else:
with scopes_disabled():
request.space = None
return self.get_response(request)

View File

@@ -1,10 +1,10 @@
import bleach
import markdown as md
from bleach_whitelist import markdown_attrs, markdown_tags
from bleach_allowlist import markdown_attrs, markdown_tags
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from jinja2 import Template, TemplateSyntaxError
from jinja2 import Template, TemplateSyntaxError, UndefinedError
from gettext import gettext as _
class IngredientObject(object):
amount = ""
@@ -57,6 +57,8 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
template = Template(instructions)
instructions = template.render(ingredients=ingredients)
except TemplateSyntaxError:
pass
return _('Could not parse template code.') + ' Error: Template Syntax broken'
except UndefinedError:
return _('Could not parse template code.') + ' Error: Undefined Error'
return instructions

View File

@@ -0,0 +1,59 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
class Pepperplate(Integration):
def get_recipe_from_file(self, file):
ingredient_mode = False
direction_mode = False
ingredients = []
directions = []
for fl in file.readlines():
line = fl.decode("utf-8")
if 'Title:' in line:
title = line.replace('Title:', '').replace('"', '').strip()
if 'Description:' in line:
description = line.replace('Description:', '').strip()
if 'Original URL:' in line or 'Source:' in line or 'Yield:' in line or 'Total:' in line:
if len(line.strip().split(':')[1]) > 0:
directions.append(line.strip() + '\n')
if ingredient_mode:
if len(line) > 2 and 'Instructions:' not in line:
ingredients.append(line.strip())
if direction_mode:
if len(line) > 2:
directions.append(line.strip() + '\n')
if 'Ingredients:' in line:
ingredient_mode = True
if 'Instructions:' in line:
ingredient_mode = False
direction_mode = True
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

View File

@@ -0,0 +1,60 @@
import re
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
class ChefTap(Integration):
def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename)
return re.match(r'^cheftap_export/([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename)
def get_recipe_from_file(self, file):
source_url = ''
ingredient_mode = 0
ingredients = []
directions = []
for i, fl in enumerate(file.readlines(), start=0):
line = fl.decode("utf-8")
if i == 0:
title = line.strip()
else:
if line.startswith('https:') or line.startswith('http:'):
source_url = line.strip()
else:
if ingredient_mode == 1 and len(line.strip()) == 0:
ingredient_mode = 2
if re.match(r'^([0-9])[^.](.)*$', line) and ingredient_mode < 2:
ingredient_mode = 1
ingredients.append(line.strip())
else:
directions.append(line.strip())
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions))
if source_url != '':
step.instruction += '\n' + source_url
step.save()
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

View File

@@ -3,7 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
@@ -47,10 +47,10 @@ class Chowdown(Integration):
if description_mode and len(line) > 3 and '---' not in line:
descriptions.append(line)
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, )
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
for k in tags.split(','):
keyword, created = Keyword.objects.get_or_create(name=k.strip())
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(
@@ -59,16 +59,16 @@ class Chowdown(Integration):
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^images/{image}$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))

View File

@@ -0,0 +1,56 @@
import base64
import json
from io import BytesIO
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
class Domestica(Integration):
def get_recipe_from_file(self, file):
recipe = Recipe.objects.create(
name=file['name'].strip(),
created_by=self.request.user, internal=True,
space=self.request.space)
if file['servings'] != '':
recipe.servings = file['servings']
if file['timeCook'] != '':
recipe.waiting_time = file['timeCook']
if file['timePrep'] != '':
recipe.working_time = file['timePrep']
recipe.save()
step = Step.objects.create(
instruction=file['directions']
)
if file['source'] != '':
step.instruction += '\n' + file['source']
for ingredient in file['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
if file['image'] != '':
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))))
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def split_recipe_file(self, file):
return json.loads(file.read().decode("utf-8"))

View File

@@ -1,33 +1,38 @@
import datetime
import json
import uuid
from io import BytesIO, StringIO
from zipfile import ZipFile, BadZipFile
from django.contrib import messages
from django.core.files import File
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from django.http import HttpResponse
from django.utils.formats import date_format
from django.utils.translation import gettext as _
from cookbook.models import Keyword
from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.models import Keyword, Recipe
class Integration:
request = None
keyword = None
files = None
export_type = None
ignored_recipes = []
def __init__(self, request):
def __init__(self, request, export_type):
"""
Integration for importing and exporting recipes
:param request: request context of import session (used to link user to created objects)
"""
self.request = request
self.export_type = export_type
self.keyword = Keyword.objects.create(
name=f'Import {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}.{datetime.datetime.now().strftime("%S")}',
description=f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}',
icon='📥'
name=f'Import {export_type} {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}.{datetime.datetime.now().strftime("%S")}',
description=f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}',
icon='📥',
space=request.space
)
def do_export(self, recipes):
@@ -36,33 +41,44 @@ class Integration:
:param recipes: list of recipe objects
:return: HttpResponse with a ZIP file that is directly downloaded
"""
export_zip_stream = BytesIO()
export_zip_obj = ZipFile(export_zip_stream, 'w')
for r in recipes:
if r.internal:
recipe_zip_stream = BytesIO()
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
# TODO this is temporary, find a better solution for different export formats when doing other exporters
if self.export_type != ImportExportBase.RECIPESAGE:
export_zip_stream = BytesIO()
export_zip_obj = ZipFile(export_zip_stream, 'w')
recipe_stream = StringIO()
filename, data = self.get_file_from_recipe(r)
recipe_stream.write(data)
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
for r in recipes:
if r.internal and r.space == self.request.space:
recipe_zip_stream = BytesIO()
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
try:
recipe_zip_obj.write(r.image.path, 'image.png')
except ValueError:
pass
recipe_stream = StringIO()
filename, data = self.get_file_from_recipe(r)
recipe_stream.write(data)
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
recipe_zip_obj.close()
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
try:
recipe_zip_obj.write(r.image.path, 'image.png')
except ValueError:
pass
export_zip_obj.close()
recipe_zip_obj.close()
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
response = HttpResponse(export_zip_stream.getvalue(), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="export.zip"'
return response
export_zip_obj.close()
response = HttpResponse(export_zip_stream.getvalue(), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="export.zip"'
return response
else:
json_list = []
for r in recipes:
json_list.append(self.get_file_from_recipe(r))
response = HttpResponse(json.dumps(json_list), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="recipes.json"'
return response
def import_file_name_filter(self, zip_info_object):
"""
@@ -74,29 +90,63 @@ class Integration:
"""
return True
def do_import(self, files):
def do_import(self, files, il, import_duplicates):
"""
Imports given files
:param import_duplicates: if true duplicates are imported as well
:param files: List of in memory files
:param il: Import Log object to refresh while running
:return: HttpResponseRedirect to the recipe search showing all imported recipes
"""
try:
self.files = files
for f in files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
for z in import_zip.filelist:
if self.import_file_name_filter(z):
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
import_zip.close()
else:
recipe = self.get_recipe_from_file(f.file)
recipe.keywords.add(self.keyword)
except BadZipFile:
messages.add_message(self.request, messages.ERROR, _('Importer expected a .zip file. Did you choose the correct importer type for your data ?'))
with scope(space=self.request.space):
self.keyword.name = _('Import') + ' ' + str(il.pk)
self.keyword.save()
return HttpResponseRedirect(reverse('view_search') + '?keywords=' + str(self.keyword.pk))
try:
self.files = files
for f in files:
if '.zip' in f['name'] or '.paprikarecipes' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if self.import_file_name_filter(z):
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)
import_zip.close()
elif '.json' in f['name'] or '.txt' in f['name']:
data_list = self.split_recipe_file(f['file'])
for d in data_list:
recipe = self.get_recipe_from_file(d)
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
else:
recipe = self.get_recipe_from_file(f['file'])
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
except BadZipFile:
il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
if len(self.ignored_recipes) > 0:
il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(self.ignored_recipes) + '\n\n'
il.keyword = self.keyword
il.msg += (_('Imported %s recipes.') % Recipe.objects.filter(keywords=self.keyword).count()) + '\n'
il.running = False
il.save()
def handle_duplicates(self, recipe, import_duplicates):
"""
Checks if a recipe is already present, if so deletes it
:param recipe: Recipe object
:param import_duplicates: if duplicates should be imported
"""
if Recipe.objects.filter(space=self.request.space, name=recipe.name).count() > 1 and not import_duplicates:
recipe.delete()
self.ignored_recipes.append(recipe.name)
@staticmethod
def import_recipe_image(recipe, image_file):
@@ -114,7 +164,15 @@ class Integration:
:param file: ByteIO or any file like object, depends on provider
:return: Recipe object
"""
raise NotImplementedError('Method not implemented in storage integration')
raise NotImplementedError('Method not implemented in integration')
def split_recipe_file(self, file):
"""
Takes a file that contains multiple recipes and splits it into a list of strings of various formats (e.g. json, text, ..)
:param file: ByteIO or any file like object, depends on provider
:return: list of strings
"""
raise NotImplementedError('Method not implemented in integration')
def get_file_from_recipe(self, recipe):
"""
@@ -125,4 +183,4 @@ class Integration:
- name - file name in export
- data - string content for file to get created in export zip
"""
raise NotImplementedError('Method not implemented in storage integration')
raise NotImplementedError('Method not implemented in integration')

View File

@@ -3,7 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@@ -18,7 +18,7 @@ class Mealie(Integration):
recipe = Recipe.objects.create(
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
created_by=self.request.user, internal=True)
created_by=self.request.user, internal=True, space=self.request.space)
# TODO parse times (given in PT2H3M )
@@ -32,16 +32,16 @@ class Mealie(Integration):
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))

View File

@@ -0,0 +1,83 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
class MealMaster(Integration):
def get_recipe_from_file(self, file):
print('------------ getting recipe')
servings = 1
ingredients = []
directions = []
for line in file.replace('\r', '').split('\n'):
print('testing line')
if not line.startswith('MMMMM') and line.strip != '':
if 'Title:' in line:
title = line.replace('Title:', '').strip()
else:
if 'Categories:' in line:
tags = line.replace('Categories:', '').strip()
else:
if 'Yield:' in line:
servings_text = line.replace('Yield:', '').strip()
else:
if re.match('\s{2,}([0-9])+', line):
ingredients.append(line.strip())
else:
directions.append(line.strip())
try:
servings = re.findall('([0-9])+', servings_text)[0]
except Exception as e:
print('failed parsing servings ', e)
recipe = Recipe.objects.create(name=title, servings=servings, created_by=self.request.user, internal=True, space=self.request.space)
for k in tags.split(','):
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)
current_recipe = ''
else:
current_recipe = ''
else:
current_recipe += line + '\n'
if current_recipe != '':
recipe_list.append(current_recipe)
return recipe_list

View File

@@ -3,7 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@@ -19,7 +19,7 @@ class NextcloudCookbook(Integration):
recipe = Recipe.objects.create(
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
created_by=self.request.user, internal=True,
servings=recipe_json['recipeYield'])
servings=recipe_json['recipeYield'], space=self.request.space)
# TODO parse times (given in PT2H3M )
# TODO parse keywords
@@ -34,16 +34,16 @@ class NextcloudCookbook(Integration):
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))

View File

@@ -1,56 +1,73 @@
import base64
import gzip
import json
import re
from io import BytesIO
from zipfile import ZipFile
import microdata
from bs4 import BeautifulSoup
from cookbook.helper.recipe_url_import import find_recipe_json
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Ingredient, Unit
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
class Paprika(Integration):
def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename)
return re.match(r'^Recipes/([A-Za-z\s])+.html$', zip_info_object.filename)
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def get_recipe_from_file(self, file):
html_text = file.getvalue().decode("utf-8")
with gzip.open(file, 'r') as recipe_zip:
recipe_json = json.loads(recipe_zip.read().decode("utf-8"))
items = microdata.get_items(html_text)
for i in items:
md_json = json.loads(i.json())
if 'schema.org/Recipe' in str(md_json['type']):
recipe_json = find_recipe_json(md_json['properties'], '')
recipe = Recipe.objects.create(name=recipe_json['name'].strip(), created_by=self.request.user, internal=True)
step = Step.objects.create(
instruction=recipe_json['recipeInstructions']
)
recipe = Recipe.objects.create(
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
created_by=self.request.user, internal=True, space=self.request.space)
for ingredient in recipe_json['recipeIngredient']:
f, created = Food.objects.get_or_create(name=ingredient['ingredient']['text'])
u, created = Unit.objects.get_or_create(name=ingredient['unit']['text'])
try:
if re.match(r'([0-9])+\s(.)*', recipe_json['servings'] ):
s = recipe_json['servings'].split(' ')
recipe.servings = s[0]
recipe.servings_text = s[1]
if len(recipe_json['cook_time'].strip()) > 0:
recipe.waiting_time = re.findall(r'\d+', recipe_json['cook_time'])[0]
if len(recipe_json['prep_time'].strip()) > 0:
recipe.working_time = re.findall(r'\d+', recipe_json['prep_time'])[0]
except Exception:
pass
recipe.save()
instructions = recipe_json['directions']
if len(recipe_json['notes'].strip()) > 0:
instructions += '\n\n### ' + _('Notes') + ' \n' + recipe_json['notes']
if len(recipe_json['nutritional_info'].strip()) > 0:
instructions += '\n\n### ' + _('Nutritional Information') + ' \n' + recipe_json['nutritional_info']
if len(recipe_json['source'].strip()) > 0 or len(recipe_json['source_url'].strip()) > 0:
instructions += '\n\n### ' + _('Source') + ' \n' + recipe_json['source'].strip() + ' \n' + recipe_json['source_url'].strip()
step = Step.objects.create(
instruction=instructions
)
if 'categories' in recipe_json:
for c in recipe_json['categories']:
keyword, created = Keyword.objects.get_or_create(name=c.strip(), space=self.request.space)
recipe.keywords.add(keyword)
for ingredient in recipe_json['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient['amount'], note=ingredient['note']
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
recipe.steps.add(step)
soup = BeautifulSoup(html_text, "html.parser")
image = soup.find('img')
image_name = image.attrs['src'].strip().replace('Images/', '')
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
for z in import_zip.filelist:
if re.match(f'^Recipes/Images/{image_name}$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
return recipe
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
return recipe

View File

@@ -0,0 +1,93 @@
import base64
import json
from io import BytesIO
import requests
from rest_framework.renderers import JSONRenderer
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
class RecipeSage(Integration):
def get_recipe_from_file(self, file):
recipe = Recipe.objects.create(
name=file['name'].strip(),
created_by=self.request.user, internal=True,
space=self.request.space)
try:
if file['recipeYield'] != '':
recipe.servings = int(file['recipeYield'])
if file['totalTime'] != '':
recipe.waiting_time = int(file['totalTime']) - int(file['timePrep'])
if file['prepTime'] != '':
recipe.working_time = int(file['timePrep'])
recipe.save()
except Exception as e:
print('failed to parse yield or time ', str(e))
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
instruction=s['text']
)
if not ingredients_added:
ingredients_added = True
for ingredient in file['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
if len(file['image']) > 0:
try:
response = requests.get(file['image'][0])
self.import_recipe_image(recipe, BytesIO(response.content))
except Exception as e:
print('failed to import image ', str(e))
return recipe
def get_file_from_recipe(self, recipe):
data = {
'@context': 'http://schema.org',
'@type': 'Recipe',
'creditText': '',
'isBasedOn': '',
'name': recipe.name,
'description': recipe.description,
'prepTime': str(recipe.working_time),
'totalTime': str(recipe.waiting_time + recipe.working_time),
'recipeYield': str(recipe.servings),
'image': [],
'recipeCategory': [],
'comment': [],
'recipeIngredient': [],
'recipeInstructions': [],
}
for s in recipe.steps.all():
if s.type != Step.TIME:
data['recipeInstructions'].append({
'@type': 'HowToStep',
'text': s.instruction
})
for i in s.ingredients.all():
data['recipeIngredient'].append(f'{float(i.amount)} {i.unit} {i.food}')
return data
def split_recipe_file(self, file):
return json.loads(file.read().decode("utf-8"))

View File

@@ -0,0 +1,82 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
class RezKonv(Integration):
def get_recipe_from_file(self, file):
ingredient_mode = False
direction_mode = False
ingredients = []
directions = []
for line in file.replace('\r', '').split('\n'):
if 'Titel:' in line:
title = line.replace('Titel:', '').strip()
if 'Kategorien:' in line:
tags = line.replace('Kategorien:', '').strip()
if ingredient_mode and ('quelle' in line.lower() or 'source' in line.lower()):
ingredient_mode = False
if ingredient_mode:
if line != '' and '===' not in line and 'Zubereitung' not in line:
ingredients.append(line.strip())
if direction_mode:
if line.strip() != '' and line.strip() != '=====':
directions.append(line.strip())
if 'Zutaten:' in line:
ingredient_mode = True
if 'Zubereitung:' in line:
ingredient_mode = False
direction_mode = True
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
for k in tags.split(','):
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
if line.startswith('=====') and 'rezkonv' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)
current_recipe = ''
else:
current_recipe = ''
else:
current_recipe += line + '\n'
if current_recipe != '':
recipe_list.append(current_recipe)
return recipe_list

View File

@@ -1,6 +1,6 @@
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@@ -41,14 +41,14 @@ class Safron(Integration):
ingredient_mode = False
direction_mode = True
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, )
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions))
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
# Translators:
# 31a3ead7f9b1ec8ada1a36808eee4069_988cec9 <9478557dfb8b6cd81570ee9e754f1719_904168>, 2020
# Frank Engbers <ikbenfrank@gmail.com>, 2020
# kampsj <jkamps@gmail.com>, 2021
#
#
#, fuzzy
msgid ""
msgstr ""
@@ -16,12 +16,11 @@ msgstr ""
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
"Last-Translator: kampsj <jkamps@gmail.com>, 2021\n"
"Language-Team: Dutch (https://www.transifex.com/django-recipes/teams/110507/"
"nl/)\n"
"Language: nl\n"
"Language-Team: Dutch (https://www.transifex.com/django-recipes/teams/110507/nl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
@@ -88,7 +87,7 @@ msgstr ""
#: .\cookbook\forms.py:55
msgid "Makes the navbar stick to the top of the page."
msgstr ""
msgstr "Zet de navbar vast aan de bovenkant van de pagina."
#: .\cookbook\forms.py:71
msgid ""
@@ -127,10 +126,8 @@ msgid "Storage UID"
msgstr "Opslag UID"
#: .\cookbook\forms.py:117
#, fuzzy
#| msgid "Number of Days"
msgid "Number of servings"
msgstr "Aantal dagen"
msgstr "Porties"
#: .\cookbook\forms.py:128
msgid ""
@@ -142,7 +139,7 @@ msgstr ""
#: .\cookbook\forms.py:143
msgid "Default"
msgstr ""
msgstr "Standaard waarde"
#: .\cookbook\forms.py:162
msgid "New Unit"
@@ -190,11 +187,11 @@ msgstr "Laat leeg voor nextcloud en vul de api token in voor dropbox."
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
"Leave empty for dropbox and enter only base url for nextcloud "
"(<code>/remote.php/webdav/</code> is added automatically)"
msgstr ""
"Laat leeg voor dropbox en vul enkel de base url voor nextcloud in. (<code>/"
"remote.php/webdav/</code> wordt automatisch toegevoegd.)"
"Laat leeg voor dropbox en vul enkel de base url voor nextcloud in. "
"(<code>/remote.php/webdav/</code> wordt automatisch toegevoegd.)"
#: .\cookbook\forms.py:263
msgid "Search String"
@@ -211,17 +208,17 @@ msgstr "Je moet minimaal één recept of titel te specificeren."
#: .\cookbook\forms.py:312
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Je kan in de instellingen standaard gebruikers in stellen om de recepten met "
"te delen."
"Je kan in de instellingen standaard gebruikers in stellen om de recepten met"
" te delen."
#: .\cookbook\forms.py:313
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\">docs here</a>"
"You can use markdown to format this field. See the <a "
"href=\"/docs/markdown/\">docs here</a>"
msgstr ""
"Je kunt markdown gebruiken om dit veld te op te maken. Bekijk de <a href=\"/"
"docs/markdown/\">documentatie hier</a>."
"Je kunt markdown gebruiken om dit veld te op te maken. Bekijk de <a "
"href=\"/docs/markdown/\">documentatie hier</a>."
#: .\cookbook\forms.py:328
msgid "A username is not required, if left blank the new user can choose one."
@@ -257,8 +254,8 @@ msgstr ""
#: .\cookbook\helper\recipe_url_import.py:53
msgid ""
"The requested site does not provide any recognized data format to import the "
"recipe from."
"The requested site does not provide any recognized data format to import the"
" recipe from."
msgstr ""
"De opgevraagde site biedt geen bekend gegevensformaat aan om het recept van "
"te importeren."
@@ -272,6 +269,7 @@ msgid ""
"Importer expected a .zip file. Did you choose the correct importer type for "
"your data ?"
msgstr ""
"De importtool verwachtte een .zip bestand. Heb je het juiste type gekozen?"
#: .\cookbook\integration\safron.py:23
#: .\cookbook\templates\forms\edit_internal_recipe.html:65
@@ -281,8 +279,6 @@ msgid "Servings"
msgstr "Porties"
#: .\cookbook\integration\safron.py:25
#, fuzzy
#| msgid "Waiting time ~"
msgid "Waiting time"
msgstr "Wachttijd"
@@ -299,7 +295,7 @@ msgstr "Kookboek"
#: .\cookbook\integration\safron.py:31
msgid "Section"
msgstr ""
msgstr "Sectie"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:12
msgid "Breakfast"
@@ -395,37 +391,35 @@ msgstr "Inloggen"
#: .\cookbook\templates\account\login.html:13
#: .\cookbook\templates\account\login.html:28
msgid "Sign In"
msgstr ""
msgstr "Log in"
#: .\cookbook\templates\account\login.html:38
msgid "Social Login"
msgstr ""
msgstr "Socials login"
#: .\cookbook\templates\account\login.html:39
msgid "You can use any of the following providers to sign in."
msgstr ""
msgstr "Je kan een van de volgende providers gebruiken om in te loggen."
#: .\cookbook\templates\account\logout.html:5
#: .\cookbook\templates\account\logout.html:9
#: .\cookbook\templates\account\logout.html:18
msgid "Sign Out"
msgstr ""
msgstr "Log uit"
#: .\cookbook\templates\account\logout.html:11
#, fuzzy
#| msgid "Are you sure that you want to merge these two units?"
msgid "Are you sure you want to sign out?"
msgstr "Weet je zeker dat je deze twee eenheden wil samenvoegen?"
msgstr "Weet je zeker dat je uit wil loggen?"
#: .\cookbook\templates\account\password_reset.html:5
#: .\cookbook\templates\account\password_reset_done.html:5
msgid "Password Reset"
msgstr ""
msgstr "Wachtwoord reset"
#: .\cookbook\templates\account\password_reset.html:9
#: .\cookbook\templates\account\password_reset_done.html:9
msgid "Password reset is not implemented for the time being!"
msgstr ""
msgstr "Wachtwoord reset is nog niet geïmplementeerd!"
#: .\cookbook\templates\account\signup.html:5
msgid "Register"
@@ -555,8 +549,8 @@ msgid ""
"On this Page you can manage all storage folder locations that should be "
"monitored and synced."
msgstr ""
"Op deze pagina kaan je alle opslag mappen die gesynchroniseerd en gemonitord "
"worden beheren."
"Op deze pagina kaan je alle opslag mappen die gesynchroniseerd en gemonitord"
" worden beheren."
#: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format"
@@ -643,20 +637,16 @@ msgid "Waiting Time"
msgstr "Wachttijd"
#: .\cookbook\templates\forms\edit_internal_recipe.html:68
#, fuzzy
#| msgid "Servings"
msgid "Servings Text"
msgstr "Porties"
msgstr "Porties tekst"
#: .\cookbook\templates\forms\edit_internal_recipe.html:79
msgid "Select Keywords"
msgstr "Selecteer sleutelwoorden"
#: .\cookbook\templates\forms\edit_internal_recipe.html:93
#, fuzzy
#| msgid "Nutrition"
msgid "Description"
msgstr "Voedingswaarde"
msgstr "Beschrijving"
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
msgid "Nutrition"
@@ -772,7 +762,7 @@ msgstr "Hoeveelheid inschakelen"
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
msgid "Copy Template Reference"
msgstr ""
msgstr "Kopieer sjabloon referentie"
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
#: .\cookbook\templates\url_import.html:177
@@ -820,18 +810,14 @@ msgstr "Ingrediënten bewerken"
#: .\cookbook\templates\forms\ingredients.html:16
msgid ""
"\n"
" The following form can be used if, accidentally, two (or more) units "
"or ingredients where created that should be\n"
" The following form can be used if, accidentally, two (or more) units or ingredients where created that should be\n"
" the same.\n"
" It merges two units or ingredients and updates all recipes using "
"them.\n"
" It merges two units or ingredients and updates all recipes using them.\n"
" "
msgstr ""
"\n"
"Het volgende formulier kan worden gebruikt wanneer per ongeluk twee (of "
"meer) eenheden of ingrediënten zijn gecreëerd dat eigenlijk hetzelfde zijn.\n"
"Het doet de twee eenheden of ingrediënten samenvoegen en alle bijbehorende "
"recepten updaten."
"Het volgende formulier kan worden gebruikt wanneer per ongeluk twee (of meer) eenheden of ingrediënten zijn gecreëerd dat eigenlijk hetzelfde zijn.\n"
"Het doet de twee eenheden of ingrediënten samenvoegen en alle bijbehorende recepten updaten."
#: .\cookbook\templates\forms\ingredients.html:24
#: .\cookbook\templates\stats.html:26
@@ -953,22 +939,16 @@ msgstr "Veiligheidswaarschuwing"
#: .\cookbook\templates\include\storage_backend_warning.html:5
msgid ""
"\n"
" The <b>Password and Token</b> field are stored as <b>plain text</b> "
"inside the database.\n"
" This is necessary because they are needed to make API requests, but "
"it also increases the risk of\n"
" The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.\n"
" This is necessary because they are needed to make API requests, but it also increases the risk of\n"
" someone stealing it. <br/>\n"
" To limit the possible damage tokens or accounts with limited access "
"can be used.\n"
" To limit the possible damage tokens or accounts with limited access can be used.\n"
" "
msgstr ""
"\n"
"Het <b>wachtwoord en token</b> veld worden als <b>plain text</b> in de "
"database opgeslagen.\n"
"Dit is benodigd omdat deze benodigd zijn voor de API requests, Echter "
"verhoogd dit ook het risico van diefstal.<br/>\n"
"Om mogelijke schade te beperken kunt u gebruik maken van account met "
"gelimiteerde toegang."
"Het <b>wachtwoord en token</b> veld worden als <b>plain text</b> in de database opgeslagen.\n"
"Dit is benodigd omdat deze benodigd zijn voor de API requests, Echter verhoogd dit ook het risico van diefstal.<br/>\n"
"Om mogelijke schade te beperken kunt u gebruik maken van account met gelimiteerde toegang."
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
@@ -1011,26 +991,17 @@ msgstr "Markdown informatie"
#: .\cookbook\templates\markdown_info.html:14
msgid ""
"\n"
" Markdown is lightweight markup language that can be used to format "
"plain text easily.\n"
" This site uses the <a href=\"https://python-markdown.github.io/\" "
"target=\"_blank\">Python Markdown</a> library to\n"
" convert your text into nice looking HTML. Its full markdown "
"documentation can be found\n"
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
"target=\"_blank\">here</a>.\n"
" An incomplete but most likely sufficient documentation can be found "
"below.\n"
" Markdown is lightweight markup language that can be used to format plain text easily.\n"
" This site uses the <a href=\"https://python-markdown.github.io/\" target=\"_blank\">Python Markdown</a> library to\n"
" convert your text into nice looking HTML. Its full markdown documentation can be found\n"
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" target=\"_blank\">here</a>.\n"
" An incomplete but most likely sufficient documentation can be found below.\n"
" "
msgstr ""
"\n"
"Markdown is een lichtgewicht opmaak taal die gebruikt kan worden om tekst "
"eenvoudig op te maken.\n"
"Deze site gebruikt de <a href=\"https://python-markdown.github.io/\" target="
"\"_blank\">Python Markdown</a> bibliotheek\n"
"om je tekst in mooi uitziende HTML om te zetten. De volledige documentatie "
"kan <a href=\"https://daringfireball.net/projects/markdown/syntax\" target="
"\"_blank\">hier</a>gevonden worden.\n"
"Markdown is een lichtgewicht opmaak taal die gebruikt kan worden om tekst eenvoudig op te maken.\n"
"Deze site gebruikt de <a href=\"https://python-markdown.github.io/\" target=\"_blank\">Python Markdown</a> bibliotheek\n"
"om je tekst in mooi uitziende HTML om te zetten. De volledige documentatie kan <a href=\"https://daringfireball.net/projects/markdown/syntax\" target=\"_blank\">hier</a>gevonden worden.\n"
"Onvolledige, maar waarschijnlijk voldoende, informatie staat hieronder."
#: .\cookbook\templates\markdown_info.html:25
@@ -1130,19 +1101,15 @@ msgid "Tables"
msgstr "Tabellen"
#: .\cookbook\templates\markdown_info.html:153
#, fuzzy
#| msgid ""
#| "Markdown tables are hard to create by hand. It is recommended to use a "
#| "table editor like <a href=\"https://www.tablesgenerator.com/"
#| "markdown_tables\" target=\"_blank\">this</a> one."
msgid ""
"Markdown tables are hard to create by hand. It is recommended to use a table "
"editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
"\"noreferrer noopener\" target=\"_blank\">this one.</a>"
"Markdown tables are hard to create by hand. It is recommended to use a table"
" editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" "
"rel=\"noreferrer noopener\" target=\"_blank\">this one.</a>"
msgstr ""
"Het is lastig om markdown tabellen handmatig te creëren. Het is geadviseerd "
"dat u een tabel bewerker zoals <a href=\"https://www.tablesgenerator.com/"
"markdown_tables\" target=\"_blank\">deze</a> gebruikt."
"Het is lastig om met de hand Markdown tabellen te maken. Het wordt "
"aangeraden om een tabel editor zoals <a "
"href=\"https://www.tablesgenerator.com/markdown_tables\" rel=\"noreferrer "
"noopener\" target=\"_blank\">deze</a> te gebruiken."
#: .\cookbook\templates\markdown_info.html:155
#: .\cookbook\templates\markdown_info.html:157
@@ -1180,18 +1147,18 @@ msgstr "Notitie (optioneel)"
#: .\cookbook\templates\meal_plan.html:143
msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\" target=\"_blank\" rel=\"noopener noreferrer\">docs here</a>"
"You can use markdown to format this field. See the <a "
"href=\"/docs/markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">docs "
"here</a>"
msgstr ""
"Je kan markdown gebruiken om dit veld op te maken. Zie de <a href=\"/docs/"
"markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">documentatie</a>"
"Je kan markdown gebruiken om dit veld op te maken. Zie de <a "
"href=\"/docs/markdown/\" target=\"_blank\" rel=\"noopener "
"noreferrer\">documentatie</a>"
#: .\cookbook\templates\meal_plan.html:147
#: .\cookbook\templates\meal_plan.html:251
#, fuzzy
#| msgid "Servings"
msgid "Serving Count"
msgstr "Porties"
msgstr "Portie teller"
#: .\cookbook\templates\meal_plan.html:153
msgid "Create only note"
@@ -1226,8 +1193,8 @@ msgstr "Weekdag aanpassing"
#: .\cookbook\templates\meal_plan.html:209
msgid ""
"Number of days starting from the first day of the week to offset the default "
"view."
"Number of days starting from the first day of the week to offset the default"
" view."
msgstr ""
"Aantal dagen startende met de eerste dag van de week om het standaard "
"overzicht aan te passen."
@@ -1269,94 +1236,37 @@ msgid "Meal Plan Help"
msgstr "Maaltijdplanner hulp"
#: .\cookbook\templates\meal_plan.html:344
#, fuzzy
#| msgid ""
#| "\n"
#| " <p>The meal plan module allows planning of "
#| "meals both with recipes or just notes.</p>\n"
#| " <p>Simply select a recipe from the list of "
#| "recently viewed recipes or search the one you\n"
#| " want and drag it to the desired plan "
#| "position. You can also add a note and a title and\n"
#| " then drag the recipe to create a plan "
#| "entry with a custom title and note. Creating only\n"
#| " Notes is possible by dragging the create "
#| "note box into the plan.</p>\n"
#| " <p>Click on a recipe in order to open the "
#| "detail view. Here you can also add it to the\n"
#| " shopping list. You can also add all "
#| "recipes of a day to the shopping list by\n"
#| " clicking the shopping cart at the top of "
#| "the table.</p>\n"
#| " <p>Since a common use case is to plan meals "
#| "together you can define\n"
#| " users you want to share your plan with in "
#| "the settings.\n"
#| " </p>\n"
#| " <p>You can also edit the types of meals you "
#| "want to plan. If you share your plan with\n"
#| " someone with\n"
#| " different meals, their meal types will "
#| "appear in your list as well. To prevent\n"
#| " duplicates (e.g. Other and Misc.)\n"
#| " name your meal types the same as the "
#| "users you share your meals with and they will be\n"
#| " merged.</p>\n"
#| " "
msgid ""
"\n"
" <p>The meal plan module allows planning of meals "
"both with recipes and notes.</p>\n"
" <p>Simply select a recipe from the list of "
"recently viewed recipes or search the one you\n"
" want and drag it to the desired plan "
"position. You can also add a note and a title and\n"
" then drag the recipe to create a plan entry "
"with a custom title and note. Creating only\n"
" Notes is possible by dragging the create "
"note box into the plan.</p>\n"
" <p>Click on a recipe in order to open the "
"detailed view. There you can also add it to the\n"
" shopping list. You can also add all recipes "
"of a day to the shopping list by\n"
" clicking the shopping cart at the top of the "
"table.</p>\n"
" <p>Since a common use case is to plan meals "
"together you can define\n"
" users you want to share your plan with in "
"the settings.\n"
" <p>The meal plan module allows planning of meals both with recipes and notes.</p>\n"
" <p>Simply select a recipe from the list of recently viewed recipes or search the one you\n"
" want and drag it to the desired plan position. You can also add a note and a title and\n"
" then drag the recipe to create a plan entry with a custom title and note. Creating only\n"
" Notes is possible by dragging the create note box into the plan.</p>\n"
" <p>Click on a recipe in order to open the detailed view. There you can also add it to the\n"
" shopping list. You can also add all recipes of a day to the shopping list by\n"
" clicking the shopping cart at the top of the table.</p>\n"
" <p>Since a common use case is to plan meals together you can define\n"
" users you want to share your plan with in the settings.\n"
" </p>\n"
" <p>You can also edit the types of meals you want "
"to plan. If you share your plan with\n"
" <p>You can also edit the types of meals you want to plan. If you share your plan with\n"
" someone with\n"
" different meals, their meal types will "
"appear in your list as well. To prevent\n"
" different meals, their meal types will appear in your list as well. To prevent\n"
" duplicates (e.g. Other and Misc.)\n"
" name your meal types the same as the users "
"you share your meals with and they will be\n"
" name your meal types the same as the users you share your meals with and they will be\n"
" merged.</p>\n"
" "
msgstr ""
"\n"
"<p>De maaltijdplanner maakt het mogelijk maaltijden op basis van recepten of "
"notities te plannen.</p>\n"
"<p>Selecteer een recept uit de lijst van recent bekeken recepten of zoek het "
"recept dat je wil en sleep het naar de gewenste positie. Je kan ook eerst "
"een notitie en titel toevoegen en dan het recept naar de gewenste positie "
"slepen om een maaltijdplan met een aangepaste titel en notitie te maken. "
"Alleen notities aanmaken is ook mogelijk door 'Maak notitie' in het "
"maaltijdplan te slepen.</p>\n"
"<p>Klik op een recept om het te openen en de details te bekijken. Hier kan "
"je het ook aan de boodschappenlijst toevoegen door op het winkelwagentje "
"bovenaan de tabel te klikken.</p>\n"
"<p>Omdat maaltijden vaak gezamenlijk worden gepland kan je in de "
"instellingen gebruikers aangeven met wie je het maaltijdplan wil delen.</p>\n"
"<p>Je kan ook de soort maaltijden die je wil plannen bewerken. Als je jouw "
"plan deelt met iemand met andere soorten, dan zullen deze ook in jouw lijst "
"verschijnen. Gelijknamige soorten worden samengevoegd. Zorg er daarom voor "
"dat de gebruikte soorten overeenkomen met de gebruiker met wie je je "
"maaltijdplannen deelt. Dit voorkomt dubbelingen (zoals Overige en "
"Willekeurig).</p>"
"<p>De maaltijdplan module maakt plannen van maaltijden met recepten en notities mogelijk.</p>\n"
"<p>Selecteer een recept van de lijst recent bekeken recepten of zoek naar\n"
"het gewenste recept en sleep het naar de juiste positie in het maaltijdplan. Je kan ook eerst een notitie en titel toevoegen en dan het recept naar de juiste positie slepen om een unieke maaltijdplan inschrijving te maken.\n"
"Alleen notities aanmaken is mogelijk door het Maak notitie vlak in het maaltijdplan te slepen.</p>\n"
"<p>Klik op een recept om de gedetailleerde weergave te openen. Daar kan je het ook toevoegen aan je boodschappenlijst.\n"
"Je kan ook alle recepten van een dag aan je boodschappenlijst toevoegen door op het winkelwagentje boven aan de tabel te klikken.</p>\n"
"<p>Omdat maaltijden samen gepland kunnen worden kan je in de instellingen kiezen met welke gebruikers je je maaltijd plan wil delen.\n"
"</p>\n"
"<p>Je kan ook het type maaltijd dat je wil plannen bewerken. Als je een maaltijdplan deelt met iemand met andere maaltijden, dan zullen hun maaltijdtypes ook in jouw lijst verschijnen. Geef, om dubbelingen (zoals Overig en Anders) te voorkomen, je maaltijdtypes daarom dezelfde naam als de gebruikers waarmee je maaltijdplannen deelt. In dat geval worden de maaltijden samengevoegd.</p>"
#: .\cookbook\templates\meal_plan_entry.html:6
msgid "Meal Plan View"
@@ -1373,27 +1283,31 @@ msgstr "Andere maaltijden op deze dag"
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\offline.html:6
msgid "Offline"
msgstr ""
msgstr "Offline"
#: .\cookbook\templates\no_groups_info.html:12
msgid "No Permissions"
msgstr ""
msgstr "Geen rechten"
#: .\cookbook\templates\no_groups_info.html:15
msgid ""
"You do not have any groups and therefor cannot use this application. Please "
"contact your administrator."
msgstr ""
"Je hebt geen groepen en kan daarom deze applicatie niet gebruiken. Neem "
"contact op met je beheerder."
#: .\cookbook\templates\offline.html:19
msgid "You are currently offline!"
msgstr ""
msgstr "Je bent op dit moment offline!"
#: .\cookbook\templates\offline.html:20
msgid ""
"The recipes listed below are available for offline viewing because you have "
"recently viewed them. Keep in mind that data might be outdated."
msgstr ""
"De recepten hieronder zijn beschikbaar om offline te bekijken omdat je ze "
"recent bekeken hebt. Houd er rekening mee dat de data mogelijk verouderd is."
#: .\cookbook\templates\recipe_view.html:21 .\cookbook\templates\stats.html:47
msgid "Comments"
@@ -1438,7 +1352,7 @@ msgstr "Account"
#: .\cookbook\templates\settings.html:38
msgid "Link social account"
msgstr ""
msgstr "Koppel account socials"
#: .\cookbook\templates\settings.html:42
msgid "Language"
@@ -1462,8 +1376,8 @@ msgstr ""
#: .\cookbook\templates\settings.html:92
msgid ""
"Use the token as an Authorization header prefixed by the word token as shown "
"in the following examples:"
"Use the token as an Authorization header prefixed by the word token as shown"
" in the following examples:"
msgstr ""
"Gebruik de token als een 'Authorization header'voorafgegaan door het woord "
"token zoals in de volgende voorbeelden:"
@@ -1484,7 +1398,8 @@ msgstr "Setup"
msgid ""
"To start using this application you must first create a superuser account."
msgstr ""
"Om te starten met de applicatie moet je eerst een superuser account aanmaken."
"Om te starten met de applicatie moet je eerst een superuser account "
"aanmaken."
#: .\cookbook\templates\setup.html:20
msgid "Create Superuser account"
@@ -1500,13 +1415,11 @@ msgstr "Geen recepten geselecteerd"
#: .\cookbook\templates\shopping_list.html:145
msgid "Entry Mode"
msgstr ""
msgstr "Invoermodus"
#: .\cookbook\templates\shopping_list.html:153
#, fuzzy
#| msgid "New Entry"
msgid "Add Entry"
msgstr "Nieuw item"
msgstr "Zet op lijst"
#: .\cookbook\templates\shopping_list.html:168
msgid "Amount"
@@ -1514,13 +1427,11 @@ msgstr "Hoeveelheid"
#: .\cookbook\templates\shopping_list.html:224
msgid "Supermarket"
msgstr ""
msgstr "Supermarkt"
#: .\cookbook\templates\shopping_list.html:234
#, fuzzy
#| msgid "Select User"
msgid "Select Supermarket"
msgstr "Selecteer gebruiker"
msgstr "Selecteer supermarkt"
#: .\cookbook\templates\shopping_list.html:258
msgid "Select User"
@@ -1549,26 +1460,28 @@ msgstr "Er is een fout opgetreden bij het maken van een hulpbron!"
#: .\cookbook\templates\socialaccount\connections.html:4
#: .\cookbook\templates\socialaccount\connections.html:7
msgid "Account Connections"
msgstr ""
msgstr "Account verbindingen"
#: .\cookbook\templates\socialaccount\connections.html:10
msgid ""
"You can sign in to your account using any of the following third party\n"
" accounts:"
msgstr ""
"Je kan inloggen met een account van een van de onderstaande derde partijen:"
#: .\cookbook\templates\socialaccount\connections.html:36
msgid "Remove"
msgstr ""
msgstr "Verwijder"
#: .\cookbook\templates\socialaccount\connections.html:44
msgid ""
"You currently have no social network accounts connected to this account."
msgstr ""
"Je hebt op dit moment geen sociaalnetwerk account aan dit account gekoppeld."
#: .\cookbook\templates\socialaccount\connections.html:47
msgid "Add a 3rd Party Account"
msgstr ""
msgstr "Voeg account van een 3e partij toe"
#: .\cookbook\templates\stats.html:4
msgid "Stats"
@@ -1621,19 +1534,15 @@ msgstr "Systeeminformatie"
#: .\cookbook\templates\system.html:51
msgid ""
"\n"
" Django Recipes is an open source free software application. It can "
"be found on\n"
" Django Recipes is an open source free software application. It can be found on\n"
" <a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
" Changelogs can be found <a href=\"https://github.com/vabene1111/"
"recipes/releases\">here</a>.\n"
" Changelogs can be found <a href=\"https://github.com/vabene1111/recipes/releases\">here</a>.\n"
" "
msgstr ""
"\n"
"Django Recipes is een open source gratis software applicatie. Het kan "
"gevonden worden op\n"
"Django Recipes is een open source gratis software applicatie. Het kan gevonden worden op\n"
"<a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
"Wijzigingenoverzichten kunnen <a href=\"https://github.com/vabene1111/"
"recipes/releases\">hier</a> gevonden worden."
"Wijzigingenoverzichten kunnen <a href=\"https://github.com/vabene1111/recipes/releases\">hier</a> gevonden worden."
#: .\cookbook\templates\system.html:65
msgid "Media Serving"
@@ -1653,15 +1562,12 @@ msgstr "Ok"
msgid ""
"Serving media files directly using gunicorn/python is <b>not recommend</b>!\n"
" Please follow the steps described\n"
" <a href=\"https://github.com/vabene1111/recipes/releases/"
"tag/0.8.1\">here</a> to update\n"
" <a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">here</a> to update\n"
" your installation.\n"
" "
msgstr ""
"Mediabestanden rechtstreeks aanbieden met gunicorn/python is <b>niet "
"aanbevolen</b>!\n"
"Volg de stappen zoals <a href=\"https://github.com/vabene1111/recipes/"
"releases/tag/0.8.1\">hier</a> beschreven om je installatie te updaten."
"Mediabestanden rechtstreeks aanbieden met gunicorn/python is <b>niet aanbevolen</b>!\n"
"Volg de stappen zoals <a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">hier</a> beschreven om je installatie te updaten."
#: .\cookbook\templates\system.html:74 .\cookbook\templates\system.html:90
#: .\cookbook\templates\system.html:105 .\cookbook\templates\system.html:119
@@ -1675,20 +1581,15 @@ msgstr "Geheime sleutel"
#: .\cookbook\templates\system.html:83
msgid ""
"\n"
" You do not have a <code>SECRET_KEY</code> configured in your "
"<code>.env</code> file. Django defaulted to the\n"
" You do not have a <code>SECRET_KEY</code> configured in your <code>.env</code> file. Django defaulted to the\n"
" standard key\n"
" provided with the installation which is publicly know and "
"insecure! Please set\n"
" <code>SECRET_KEY</code> int the <code>.env</code> configuration "
"file.\n"
" provided with the installation which is publicly know and insecure! Please set\n"
" <code>SECRET_KEY</code> int the <code>.env</code> configuration file.\n"
" "
msgstr ""
"\n"
"Je hebt geen <code>SECRET_KEY</code> geconfigureerd in je .env bestand.\n"
"Django is overgegaan naar de standaard sleutel die openbaar en onveilig is! "
"Stel alsjeblieft <code>SECRET_KEY</code>in in het <code>.env</code> "
"configuratiebestand."
"Django is overgegaan naar de standaard sleutel die openbaar en onveilig is! Stel alsjeblieft <code>SECRET_KEY</code>in in het <code>.env</code> configuratiebestand."
#: .\cookbook\templates\system.html:95
msgid "Debug Mode"
@@ -1697,17 +1598,13 @@ msgstr "Debug modus"
#: .\cookbook\templates\system.html:99
msgid ""
"\n"
" This application is still running in debug mode. This is most "
"likely not needed. Turn of debug mode by\n"
" This application is still running in debug mode. This is most likely not needed. Turn of debug mode by\n"
" setting\n"
" <code>DEBUG=0</code> int the <code>.env</code> configuration "
"file.\n"
" <code>DEBUG=0</code> int the <code>.env</code> configuration file.\n"
" "
msgstr ""
"\n"
"Deze applicatie draait in debug modus. Dit is waarschijnlijk niet nodig. "
"Schakel debug modus uit door de instelling <code>DEBUG=0</code> in het "
"<code>.env</code>configuratiebestand aan te passen."
"Deze applicatie draait in debug modus. Dit is waarschijnlijk niet nodig. Schakel debug modus uit door de instelling <code>DEBUG=0</code> in het <code>.env</code>configuratiebestand aan te passen."
#: .\cookbook\templates\system.html:110
msgid "Database"
@@ -1720,15 +1617,12 @@ msgstr "Info"
#: .\cookbook\templates\system.html:114
msgid ""
"\n"
" This application is not running with a Postgres database "
"backend. This is ok but not recommended as some\n"
" This application is not running with a Postgres database backend. This is ok but not recommended as some\n"
" features only work with postgres databases.\n"
" "
msgstr ""
"\n"
"Deze applicatie draait niet met een Postgres database als backend. Dit is ok "
"maar wordt niet aanbevolen omdat sommige functies alleen werken met Postgres "
"databases."
"Deze applicatie draait niet met een Postgres database als backend. Dit is ok maar wordt niet aanbevolen omdat sommige functies alleen werken met Postgres databases."
#: .\cookbook\templates\url_import.html:5
msgid "URL Import"
@@ -1763,18 +1657,16 @@ msgstr "Informatie"
#: .\cookbook\templates\url_import.html:235
msgid ""
" Only websites containing ld+json or microdata information can currently\n"
" be imported. Most big recipe pages "
"support this. If you site cannot be imported but\n"
" be imported. Most big recipe pages support this. If you site cannot be imported but\n"
" you think\n"
" it probably has some kind of structured "
"data feel free to post an example in the\n"
" it probably has some kind of structured data feel free to post an example in the\n"
" github issues."
msgstr ""
"Alleen websites die Id+json of microdata informatie bevatten kunnen op dit "
"moment geïmporteerd worden. De meeste grote recepten websites ondersteunen "
"dit. Als jouw website niet geïmporteerd kan worden maar je denkt dat het "
"waarschijnlijk gestructureerde data bevat, voel je dan vrij om een foorbeeld "
"te posten in de GitHub issues."
"waarschijnlijk gestructureerde data bevat, voel je dan vrij om een foorbeeld"
" te posten in de GitHub issues."
#: .\cookbook\templates\url_import.html:243
msgid "Google ld+json Info"
@@ -1798,7 +1690,7 @@ msgstr "Voorkeur voor gebruiker bestaat al"
#: .\cookbook\views\api.py:416 .\cookbook\views\views.py:265
msgid "This feature is not available in the demo version!"
msgstr ""
msgstr "Deze optie is niet beschikbaar in de demo versie!"
#: .\cookbook\views\api.py:439
msgid "Sync successful!"
@@ -1888,7 +1780,7 @@ msgstr "Eenheden samengevoegd!"
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:317
msgid "Cannot merge with the same object!"
msgstr ""
msgstr "Kan niet met hetzelfde object samenvoegen!"
#: .\cookbook\views\edit.py:311
msgid "Foods merged!"
@@ -1896,11 +1788,11 @@ msgstr "Ingrediënten samengevoegd!"
#: .\cookbook\views\import_export.py:42
msgid "Importing is not implemented for this provider"
msgstr ""
msgstr "Importeren is voor deze provider niet geïmplementeerd"
#: .\cookbook\views\import_export.py:58
msgid "Exporting is not implemented for this provider"
msgstr ""
msgstr "Exporteren is voor deze provider niet geïmplementeerd"
#: .\cookbook\views\lists.py:42
msgid "Import Log"
@@ -1932,7 +1824,7 @@ msgstr "Opmerking opgeslagen!"
#: .\cookbook\views\views.py:152
msgid "This recipe is already linked to the book!"
msgstr ""
msgstr "Dit recept is al aan het boek gekoppeld!"
#: .\cookbook\views\views.py:158
msgid "Bookmark saved!"
@@ -1941,8 +1833,8 @@ msgstr "Bladwijzer opgeslagen!"
#: .\cookbook\views\views.py:380
msgid ""
"The setup page can only be used to create the first user! If you have "
"forgotten your superuser credentials please consult the django documentation "
"on how to reset passwords."
"forgotten your superuser credentials please consult the django documentation"
" on how to reset passwords."
msgstr ""
"De setup pagina kan alleen gebruikt worden om de eerste gebruiker aan te "
"maken! Indien je je superuser inloggegevens bent vergeten zal je de django "
@@ -1964,150 +1856,3 @@ msgstr "Onjuiste uitnodigingslink opgegeven!"
#: .\cookbook\views\views.py:470
msgid "Invite Link not valid or already used!"
msgstr "De uitnodigingslink is niet valide of al gebruikt!"
#~ msgid "Export Base64 encoded image?"
#~ msgstr "Base64-gecodeerde afbeelding exporteren?"
#~ msgid "Download export directly or show on page?"
#~ msgstr "De export direct downloaden of op de pagina weergeven?"
#~ msgid "Simply paste a JSON export into this textarea and click import."
#~ msgstr "Plak een JSON export in dit tekstveld en klik op importeren."
#~ msgid "Scaling factor for recipe."
#~ msgstr "Schaalfactor voor recept."
#~ msgid "Exported Recipe"
#~ msgstr "Geëxporteerd recept"
#~ msgid "Copy to clipboard"
#~ msgstr "Naar het klembord kopiëren"
#~ msgid "Copied!"
#~ msgstr "Gekopieerd!"
#~ msgid "Copy list to clipboard"
#~ msgstr "Lijst naar het klembord kopiëren"
#~ msgid "Error"
#~ msgstr "Error"
#~ msgid "There was an error loading the recipe!"
#~ msgstr "Er is een fout opgetreden bij het laden van het recept!"
#~ msgid "Updated"
#~ msgstr "Geüpdatet"
#~ msgid "Changes saved successfully!"
#~ msgstr "Wijzigingen succesvol opgeslagen!"
#~ msgid "There was an error updating the recipe!"
#~ msgstr "Er is een fout opgetreden bij het updaten van het recept!"
#~ msgid "Are you sure that you want to delete this ingredient?"
#~ msgstr "Weet je zeker dat je dit ingrediënt wil verwijderen?"
#~ msgid "Are you sure that you want to delete this step?"
#~ msgstr "Weet je zeker dat je deze stap wil verwijderen?"
#~ msgid "There was an error loading a resource!"
#~ msgstr "Er is een fout opgetreden bij het laden van een hulpbron!"
#~ msgid "Recipe Multiplier"
#~ msgstr "Recept vermenigvuldiger"
#~ msgid ""
#~ "When deleting a meal type all entries using that type will be deleted as "
#~ "well. Deletion will apply when configuration is saved. Do you want to "
#~ "proceed?"
#~ msgstr ""
#~ "Bij het verwijderen van een maaltijdsoort worden alle inzendingen die de "
#~ "maaltijdsoort gebruikt verwijderd. Verwijdering vindt plaats wanneer de "
#~ "configuratie opgeslagen wordt. Wil je doorgaan?"
#~ msgid "Add to Book"
#~ msgstr "Aan Boek toevoegen"
#~ msgid "Add to Plan"
#~ msgstr "Aan Plan toevoegen"
#~ msgid "Print"
#~ msgstr "Printen"
#~ msgid "Share"
#~ msgstr "Deel"
#~ msgid "in"
#~ msgstr "binnen"
#~ msgid "Preparation time ~"
#~ msgstr "Bereidingstijd"
#~ msgid "Minutes"
#~ msgstr "Minuten"
#~ msgid "View external recipe"
#~ msgstr "Extern recept bekijken"
#~ msgid "External recipe image"
#~ msgstr "Externe recept afbeelding"
#~ msgid "External recipe"
#~ msgstr "Extern recept"
#~ msgid ""
#~ "\n"
#~ " This is an external recipe, which "
#~ "means you can only view it by opening the link\n"
#~ " above.\n"
#~ " You can convert this recipe to a "
#~ "fancy recipe by pressing the convert button. The\n"
#~ " original\n"
#~ " file\n"
#~ " will still be accessible.\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "Dit is een extern recept, dat betekent dat je het dient te openen met de "
#~ "bovenstaande link.\n"
#~ "Je kan dit recept naar een flitsend recept omzetten door op de converteer "
#~ "knop te klikken.\n"
#~ "Het originele bestand blijft beschikbaar."
#~ msgid "Convert now!"
#~ msgstr "Nu converteren"
#~ msgid "Your username and password didn't match. Please try again."
#~ msgstr ""
#~ "Je gebruikersnaam en wachtwoord komen niet overeen. Probeer het opnieuw."
#~ msgid "There was an error updating a resource!"
#~ msgstr "Er is een fout opgetreden bij het updaten van een hulpbron!"
#~ msgid "Object created successfully!"
#~ msgstr "Object succesvol aangemaakt!"
#~ msgid "Please enter a valid food"
#~ msgstr "Geef een geldig ingrediënt op"
#~ msgid "Already importing the selected recipe, please wait!"
#~ msgstr "Het geselecteerde recept wordt geïmporteerd, even geduld!"
#~ msgid "An error occurred while trying to import this recipe!"
#~ msgstr "Er is een error opgetreden bij het importeren van dit recept!"
#~ msgid "Recipe imported successfully!"
#~ msgstr "Recept succesvol geïmporteerd!"
#~ msgid "Something went wrong during the import!"
#~ msgstr "Er is iets misgegaan tijdens het importeren!"
#~ msgid "Could not parse the supplied JSON!"
#~ msgstr "Er zit een fout in de opgegeven JSON!"
#~ msgid ""
#~ "External recipes cannot be exported, please share the file directly or "
#~ "select an internal recipe."
#~ msgstr ""
#~ "Het is niet mogelijk om externe recepten te exporteren. Deel het bestand "
#~ "zelf of selecteer een intern recept."

View File

@@ -1,20 +1,22 @@
# Generated by Django 3.0.2 on 2020-01-30 09:59
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_ingredient_units(apps, schema_editor):
Unit = apps.get_model('cookbook', 'Unit')
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
with scopes_disabled():
Unit = apps.get_model('cookbook', 'Unit')
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
for u in RecipeIngredients.objects.values('unit').distinct():
unit = Unit()
unit.name = u['unit']
unit.save()
for u in RecipeIngredients.objects.values('unit').distinct():
unit = Unit()
unit.name = u['unit']
unit.save()
for i in RecipeIngredients.objects.all():
i.unit_key = Unit.objects.get(name=i.unit)
i.save()
for i in RecipeIngredients.objects.all():
i.unit_key = Unit.objects.get(name=i.unit)
i.save()
class Migration(migrations.Migration):

View File

@@ -1,19 +1,21 @@
# Generated by Django 3.0.2 on 2020-02-16 22:09
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_ingredients(apps, schema_editor):
Ingredient = apps.get_model('cookbook', 'Ingredient')
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
with scopes_disabled():
Ingredient = apps.get_model('cookbook', 'Ingredient')
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
for u in RecipeIngredient.objects.values('name').distinct():
ingredient = Ingredient()
ingredient.name = u['name']
ingredient.save()
for u in RecipeIngredient.objects.values('name').distinct():
ingredient = Ingredient()
ingredient.name = u['name']
ingredient.save()
for i in RecipeIngredient.objects.all():
i.ingredient = Ingredient.objects.get(name=i.name)
i.save()
for i in RecipeIngredient.objects.all():
i.ingredient = Ingredient.objects.get(name=i.name)
i.save()
class Migration(migrations.Migration):

View File

@@ -1,15 +1,17 @@
# Generated by Django 3.0.5 on 2020-04-26 14:14
from django.db import migrations
from django_scopes import scopes_disabled
def apply_migration(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Group.objects.bulk_create([
Group(name=u'guest'),
Group(name=u'user'),
Group(name=u'admin'),
])
with scopes_disabled():
Group = apps.get_model('auth', 'Group')
Group.objects.bulk_create([
Group(name=u'guest'),
Group(name=u'user'),
Group(name=u'admin'),
])
class Migration(migrations.Migration):

View File

@@ -1,15 +1,17 @@
# Generated by Django 3.0.5 on 2020-04-27 16:00
from django.db import migrations
from django_scopes import scopes_disabled
def apply_migration(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
if u.groups.count() < 1:
u.groups.add(Group.objects.get(name='admin'))
u.save()
with scopes_disabled():
Group = apps.get_model('auth', 'Group')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
if u.groups.count() < 1:
u.groups.add(Group.objects.get(name='admin'))
u.save()
class Migration(migrations.Migration):

View File

@@ -2,43 +2,45 @@
from django.db import migrations
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
with scopes_disabled():
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
breakfast = MealType.objects.create(
name=_('Breakfast'),
order=0,
)
breakfast = MealType.objects.create(
name=_('Breakfast'),
order=0,
)
lunch = MealType.objects.create(
name=_('Lunch'),
order=0,
)
lunch = MealType.objects.create(
name=_('Lunch'),
order=0,
)
dinner = MealType.objects.create(
name=_('Dinner'),
order=0,
)
dinner = MealType.objects.create(
name=_('Dinner'),
order=0,
)
other = MealType.objects.create(
name=_('Other'),
order=0,
)
other = MealType.objects.create(
name=_('Other'),
order=0,
)
for m in MealPlan.objects.all():
if m.meal == 'BREAKFAST':
m.meal_type = breakfast
if m.meal == 'LUNCH':
m.meal_type = lunch
if m.meal == 'DINNER':
m.meal_type = dinner
if m.meal == 'OTHER':
m.meal_type = other
for m in MealPlan.objects.all():
if m.meal == 'BREAKFAST':
m.meal_type = breakfast
if m.meal == 'LUNCH':
m.meal_type = lunch
if m.meal == 'DINNER':
m.meal_type = dinner
if m.meal == 'OTHER':
m.meal_type = other
m.save()
m.save()
class Migration(migrations.Migration):

View File

@@ -2,22 +2,24 @@
from django.db import migrations
from django.db.models import Q
from django_scopes import scopes_disabled
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
User = apps.get_model('auth', 'User')
with scopes_disabled():
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
for t in MealType.objects.filter(created_by=None).all():
user_type = MealType.objects.create(
name=t.name,
created_by=u,
)
MealPlan.objects.filter(Q(created_by=u) and Q(meal_type=t)).update(meal_type=user_type)
for u in User.objects.all():
for t in MealType.objects.filter(created_by=None).all():
user_type = MealType.objects.create(
name=t.name,
created_by=u,
)
MealPlan.objects.filter(Q(created_by=u) and Q(meal_type=t)).update(meal_type=user_type)
MealType.objects.filter(created_by=None).delete()
MealType.objects.filter(created_by=None).delete()
class Migration(migrations.Migration):

View File

@@ -3,11 +3,14 @@
from django.db import migrations, models
import uuid
from django_scopes import scopes_disabled
def invalidate_shares(apps, schema_editor):
ShareLink = apps.get_model('cookbook', 'ShareLink')
with scopes_disabled():
ShareLink = apps.get_model('cookbook', 'ShareLink')
ShareLink.objects.all().delete()
ShareLink.objects.all().delete()
class Migration(migrations.Migration):

View File

@@ -1,16 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-25 19:37
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_ingredients(apps, schema_editor):
Recipe = apps.get_model('cookbook', 'Recipe')
Ingredient = apps.get_model('cookbook', 'Ingredient')
with scopes_disabled():
Recipe = apps.get_model('cookbook', 'Recipe')
Ingredient = apps.get_model('cookbook', 'Ingredient')
for r in Recipe.objects.all():
for i in Ingredient.objects.filter(recipe=r).all():
r.ingredients.add(i)
r.save()
for r in Recipe.objects.all():
for i in Ingredient.objects.filter(recipe=r).all():
r.ingredients.add(i)
r.save()
class Migration(migrations.Migration):

View File

@@ -1,21 +1,23 @@
# Generated by Django 3.0.7 on 2020-06-25 20:19
from django.db import migrations, models
from django_scopes import scopes_disabled
def create_default_step(apps, schema_editor):
Recipe = apps.get_model('cookbook', 'Recipe')
Step = apps.get_model('cookbook', 'Step')
with scopes_disabled():
Recipe = apps.get_model('cookbook', 'Recipe')
Step = apps.get_model('cookbook', 'Step')
for r in Recipe.objects.filter(internal=True).all():
s = Step.objects.create(
instruction=r.instructions
)
for i in r.ingredients.all():
s.ingredients.add(i)
s.save()
r.steps.add(s)
r.save()
for r in Recipe.objects.filter(internal=True).all():
s = Step.objects.create(
instruction=r.instructions
)
for i in r.ingredients.all():
s.ingredients.add(i)
s.save()
r.steps.add(s)
r.save()
class Migration(migrations.Migration):

View File

@@ -2,27 +2,29 @@
from django.db import migrations, models
import django.db.models.deletion
from django_scopes import scopes_disabled
def convert_old_specials(apps, schema_editor):
Ingredient = apps.get_model('cookbook', 'Ingredient')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
with scopes_disabled():
Ingredient = apps.get_model('cookbook', 'Ingredient')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
for i in Ingredient.objects.all():
if i.amount == 0:
i.no_amount = True
if i.unit.name == 'Special:Header':
i.header = True
i.unit = None
i.food = None
i.save()
for i in Ingredient.objects.all():
if i.amount == 0:
i.no_amount = True
if i.unit.name == 'Special:Header':
i.header = True
i.unit = None
i.food = None
i.save()
try:
Unit.objects.filter(name='Special:Header').delete()
Food.objects.filter(name='Header').delete()
except Exception:
pass
try:
Unit.objects.filter(name='Special:Header').delete()
Food.objects.filter(name='Header').delete()
except Exception:
pass
class Migration(migrations.Migration):

View File

@@ -0,0 +1,146 @@
# Generated by Django 3.1.6 on 2021-02-19 13:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0107_auto_20210128_1535'),
]
operations = [
migrations.AddField(
model_name='cooklog',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='food',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='invitelink',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='keyword',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='mealplan',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='mealtype',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='recipe',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='recipebook',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='recipebookentry',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='recipeimport',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='sharelink',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='shoppinglist',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='shoppinglistentry',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='shoppinglistrecipe',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='storage',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='supermarket',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='supermarketcategory',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='sync',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='synclog',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='unit',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='userpreference',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='viewlog',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 3.1.6 on 2021-02-21 11:04
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0108_auto_20210219_1410'),
]
operations = [
migrations.RemoveField(
model_name='recipebookentry',
name='space',
),
migrations.AlterField(
model_name='food',
name='name',
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
),
migrations.AlterField(
model_name='keyword',
name='name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='supermarket',
name='name',
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
),
migrations.AlterField(
model_name='supermarketcategory',
name='name',
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
),
migrations.AlterField(
model_name='unit',
name='name',
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
),
migrations.AlterUniqueTogether(
name='food',
unique_together={('space', 'name')},
),
migrations.AlterUniqueTogether(
name='keyword',
unique_together={('space', 'name')},
),
migrations.AlterUniqueTogether(
name='supermarket',
unique_together={('space', 'name')},
),
migrations.AlterUniqueTogether(
name='supermarketcategory',
unique_together={('space', 'name')},
),
migrations.AlterUniqueTogether(
name='unit',
unique_together={('space', 'name')},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.6 on 2021-02-21 13:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0109_auto_20210221_1204'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='space',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 3.1.6 on 2021-02-21 13:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django_scopes import scopes_disabled
def set_default_owner(apps, schema_editor):
Space = apps.get_model('cookbook', 'Space')
User = apps.get_model('auth', 'user')
with scopes_disabled():
for x in Space.objects.all():
x.created_by = User.objects.filter(is_superuser=True).first()
x.save()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0110_auto_20210221_1406'),
]
operations = [
migrations.AddField(
model_name='space',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.RunPython(set_default_owner),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-03-16 23:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0111_space_created_by'),
]
operations = [
migrations.RemoveField(
model_name='synclog',
name='space',
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.7 on 2021-03-17 19:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0112_remove_synclog_space'),
]
operations = [
migrations.RemoveField(
model_name='shoppinglistentry',
name='space',
),
migrations.RemoveField(
model_name='shoppinglistrecipe',
name='space',
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.7 on 2021-03-18 17:23
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0113_auto_20210317_2017'),
]
operations = [
migrations.CreateModel(
name='ImportLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(max_length=32)),
('running', models.BooleanField(default=True)),
('msg', models.TextField(default='')),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('keyword', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.keyword')),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.7 on 2021-03-18 21:12
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0114_importlog'),
]
operations = [
migrations.CreateModel(
name='TelegramBot',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=256)),
('name', models.CharField(blank=True, default='', max_length=128)),
('chat_id', models.CharField(blank=True, default='', max_length=128)),
('webhook_token', models.UUIDField(default=uuid.uuid4)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@@ -0,0 +1,41 @@
# Generated by Django 3.1.7 on 2021-03-18 23:12
from django.db import migrations
from django_scopes import scopes_disabled
def remove_empty_food_unit(apps, schema_editor):
with scopes_disabled():
Ingredient = apps.get_model('cookbook', 'Ingredient')
ShoppingListEntry = apps.get_model('cookbook', 'ShoppingListEntry')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
for f in Food.objects.filter(name='').all():
for o in Ingredient.objects.filter(food=f):
o.food = None
o.save()
for o in ShoppingListEntry.objects.filter(food=f):
o.delete()
f.delete()
for u in Unit.objects.filter(name='').all():
for o in Ingredient.objects.filter(unit=u):
o.unit = None
o.save()
for o in ShoppingListEntry.objects.filter(unit=u):
o.unit = None
o.save()
u.delete()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0115_telegrambot'),
]
operations = [
migrations.RunPython(remove_empty_food_unit),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-23 21:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0116_auto_20210319_0012'),
]
operations = [
migrations.AddField(
model_name='space',
name='max_recipes',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.7 on 2021-04-06 16:05
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_no_group_superusers(apps, schema_editor):
with scopes_disabled():
User = apps.get_model('auth', 'User')
Groups = apps.get_model('auth', 'Group')
for u in User.objects.filter(is_superuser=True).all():
if u.groups.count() == 0:
u.groups.add(Groups.objects.get(name='admin'))
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0117_space_max_recipes'),
]
operations = [
migrations.RunPython(migrate_no_group_superusers),
]

View File

@@ -9,7 +9,7 @@ from django.core.validators import MinLengthValidator
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
from django_random_queryset import RandomManager
from django_scopes import ScopedManager
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
STICKY_NAV_PREF_DEFAULT)
@@ -29,12 +29,45 @@ def get_model_name(model):
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
class PermissionModelMixin:
@staticmethod
def get_space_key():
return ('space',)
def get_space_kwarg(self):
return '__'.join(self.get_space_key())
def get_owner(self):
if getattr(self, 'created_by', None):
return self.created_by
if getattr(self, 'user', None):
return self.user
return None
def get_shared(self):
if getattr(self, 'shared', None):
return self.shared.all()
return []
def get_space(self):
p = '.'.join(self.get_space_key())
if getattr(self, p, None):
return getattr(self, p)
raise NotImplementedError('get space for method not implemented and standard fields not available')
class Space(models.Model):
name = models.CharField(max_length=128, default='Default')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
message = models.CharField(max_length=512, default='', blank=True)
max_recipes = models.IntegerField(default=0)
def __str__(self):
return self.name
class UserPreference(models.Model):
class UserPreference(models.Model, PermissionModelMixin):
# Themes
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
@@ -107,11 +140,14 @@ class UserPreference(models.Model):
shopping_auto_sync = models.IntegerField(default=5)
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
objects = ScopedManager(space='space')
def __str__(self):
return str(self.user)
class Storage(models.Model):
class Storage(models.Model, PermissionModelMixin):
DROPBOX = 'DB'
NEXTCLOUD = 'NEXTCLOUD'
LOCAL = 'LOCAL'
@@ -128,11 +164,14 @@ class Storage(models.Model):
path = models.CharField(blank=True, default='', max_length=256)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Sync(models.Model):
class Sync(models.Model, PermissionModelMixin):
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
path = models.CharField(max_length=512, default="")
active = models.BooleanField(default=True)
@@ -140,92 +179,138 @@ class Sync(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.path
class SupermarketCategory(models.Model):
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
class SupermarketCategory(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Meta:
unique_together = (('space', 'name'),)
class Supermarket(models.Model):
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
class Supermarket(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Meta:
unique_together = (('space', 'name'),)
class SupermarketCategoryRelation(models.Model):
class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
supermarket = models.ForeignKey(Supermarket, on_delete=models.CASCADE, related_name='category_to_supermarket')
category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket')
order = models.IntegerField(default=0)
objects = ScopedManager(space='supermarket__space')
@staticmethod
def get_space_key():
return 'supermarket', 'space'
class Meta:
ordering = ('order',)
class SyncLog(models.Model):
class SyncLog(models.Model, PermissionModelMixin):
sync = models.ForeignKey(Sync, on_delete=models.CASCADE)
status = models.CharField(max_length=32)
msg = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True)
objects = ScopedManager(space='sync__space')
def __str__(self):
return f"{self.created_at}:{self.sync} - {self.status}"
class Keyword(models.Model):
name = models.CharField(max_length=64, unique=True)
class Keyword(models.Model, PermissionModelMixin):
name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True)
description = models.TextField(default="", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
if self.icon:
return f"{self.icon} {self.name}"
else:
return f"{self.name}"
class Meta:
unique_together = (('space', 'name'),)
class Unit(models.Model):
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
class Unit(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Meta:
unique_together = (('space', 'name'),)
class Food(models.Model):
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
class Food(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
ignore_shopping = models.BooleanField(default=False)
description = models.TextField(default='', blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Meta:
unique_together = (('space', 'name'),)
class Ingredient(models.Model):
food = models.ForeignKey(
Food, on_delete=models.PROTECT, null=True, blank=True
)
unit = models.ForeignKey(
Unit, on_delete=models.PROTECT, null=True, blank=True
)
class Ingredient(models.Model, PermissionModelMixin):
food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
note = models.CharField(max_length=256, null=True, blank=True)
is_header = models.BooleanField(default=False)
no_amount = models.BooleanField(default=False)
order = models.IntegerField(default=0)
objects = ScopedManager(space='step__recipe__space')
@staticmethod
def get_space_key():
return 'step', 'recipe', 'space'
def get_space(self):
return self.step_set.first().recipe_set.first().space
def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
@@ -233,7 +318,7 @@ class Ingredient(models.Model):
ordering = ['order', 'pk']
class Step(models.Model):
class Step(models.Model, PermissionModelMixin):
TEXT = 'TEXT'
TIME = 'TIME'
@@ -249,6 +334,15 @@ class Step(models.Model):
order = models.IntegerField(default=0)
show_as_header = models.BooleanField(default=True)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe_set.first().space
def get_instruction_render(self):
from cookbook.helper.template_helper import render_instructions
return render_instructions(self)
@@ -257,7 +351,7 @@ class Step(models.Model):
ordering = ['order', 'pk']
class NutritionInformation(models.Model):
class NutritionInformation(models.Model, PermissionModelMixin):
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
carbohydrates = models.DecimalField(
default=0, decimal_places=16, max_digits=32
@@ -268,11 +362,20 @@ class NutritionInformation(models.Model):
max_length=512, default="", null=True, blank=True
)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe_set.first().space
def __str__(self):
return 'Nutrition'
class Recipe(models.Model):
class Recipe(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.CharField(max_length=512, blank=True, null=True)
servings = models.IntegerField(default=1)
@@ -297,51 +400,68 @@ class Recipe(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = RandomManager()
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Comment(models.Model):
class Comment(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
text = models.TextField()
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def __str__(self):
return self.text
class RecipeImport(models.Model):
class RecipeImport(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
file_uid = models.CharField(max_length=256, default="")
file_path = models.CharField(max_length=512, default="")
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class RecipeBook(models.Model):
class RecipeBook(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
icon = models.CharField(max_length=16, blank=True, null=True)
shared = models.ManyToManyField(
User, blank=True, related_name='shared_with'
)
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class RecipeBookEntry(models.Model):
class RecipeBookEntry(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE)
objects = ScopedManager(space='book__space')
@staticmethod
def get_space_key():
return 'book', 'space'
def __str__(self):
return self.recipe.name
@@ -355,29 +475,31 @@ class RecipeBookEntry(models.Model):
unique_together = (('recipe', 'book'),)
class MealType(models.Model):
class MealType(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class MealPlan(models.Model):
recipe = models.ForeignKey(
Recipe, on_delete=models.CASCADE, blank=True, null=True
)
class MealPlan(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
title = models.CharField(max_length=64, blank=True, default='')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
shared = models.ManyToManyField(
User, blank=True, related_name='plan_share'
)
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
note = models.TextField(blank=True)
date = models.DateField()
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def get_label(self):
if self.title:
return self.title
@@ -390,12 +512,19 @@ class MealPlan(models.Model):
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
class ShoppingListRecipe(models.Model):
recipe = models.ForeignKey(
Recipe, on_delete=models.CASCADE, null=True, blank=True
)
class ShoppingListRecipe(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe.space
def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}'
@@ -406,7 +535,7 @@ class ShoppingListRecipe(models.Model):
return None
class ShoppingListEntry(models.Model):
class ShoppingListEntry(models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
food = models.ForeignKey(Food, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
@@ -414,9 +543,21 @@ class ShoppingListEntry(models.Model):
order = models.IntegerField(default=0)
checked = models.BooleanField(default=False)
objects = ScopedManager(space='shoppinglist__space')
@staticmethod
def get_space_key():
return 'shoppinglist', 'space'
def get_space(self):
return self.shoppinglist_set.first().space
def __str__(self):
return f'Shopping list entry {self.id}'
def get_shared(self):
return self.shoppinglist_set.first().shared.all()
def get_owner(self):
try:
return self.shoppinglist_set.first().created_by
@@ -424,7 +565,7 @@ class ShoppingListEntry(models.Model):
return None
class ShoppingList(models.Model):
class ShoppingList(models.Model, PermissionModelMixin):
uuid = models.UUIDField(default=uuid.uuid4)
note = models.TextField(blank=True, null=True)
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
@@ -435,16 +576,22 @@ class ShoppingList(models.Model):
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'Shopping list {self.id}'
class ShareLink(models.Model):
class ShareLink(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.recipe} - {self.uuid}'
@@ -453,7 +600,7 @@ def default_valid_until():
return date.today() + timedelta(days=14)
class InviteLink(models.Model):
class InviteLink(models.Model, PermissionModelMixin):
uuid = models.UUIDField(default=uuid.uuid4)
username = models.CharField(blank=True, max_length=64)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
@@ -464,25 +611,63 @@ class InviteLink(models.Model):
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.uuid}'
class CookLog(models.Model):
class TelegramBot(models.Model, PermissionModelMixin):
token = models.CharField(max_length=256)
name = models.CharField(max_length=128, default='', blank=True)
chat_id = models.CharField(max_length=128, default='', blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
webhook_token = models.UUIDField(default=uuid.uuid4)
objects = ScopedManager(space='space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
def __str__(self):
return f"{self.name}"
class CookLog(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(default=timezone.now)
rating = models.IntegerField(null=True)
servings = models.IntegerField(default=0)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.recipe.name
class ViewLog(models.Model):
class ViewLog(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.recipe.name
class ImportLog(models.Model, PermissionModelMixin):
type = models.CharField(max_length=32)
running = models.BooleanField(default=True)
msg = models.TextField(default="")
keyword = models.ForeignKey(Keyword, null=True, blank=True, on_delete=models.SET_NULL)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
def __str__(self):
return f"{self.created_at}:{self.type}"

View File

@@ -35,14 +35,14 @@ class Dropbox(Provider):
# TODO check if has_more is set and import that as well
for recipe in recipes['entries']:
path = recipe['path_lower']
if not Recipe.objects.filter(file_path__iexact=path).exists() \
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
name = os.path.splitext(recipe['name'])[0]
new_recipe = RecipeImport(
name=name,
file_path=path,
storage=monitor.storage,
file_uid=recipe['id']
file_uid=recipe['id'],
space=monitor.space,
)
new_recipe.save()
import_count += 1
@@ -50,7 +50,7 @@ class Dropbox(Provider):
log_entry = SyncLog(
status='SUCCESS',
msg='Imported ' + str(import_count) + ' recipes',
sync=monitor
sync=monitor,
)
log_entry.save()
@@ -104,9 +104,7 @@ class Dropbox(Provider):
recipe.link = Dropbox.get_share_link(recipe)
recipe.save()
response = requests.get(
recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.')
)
response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
return io.BytesIO(response.content)

View File

@@ -18,13 +18,13 @@ class Local(Provider):
import_count = 0
for file in files:
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path__iexact=path).exists() \
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
name = os.path.splitext(file)[0]
new_recipe = RecipeImport(
name=name,
file_path=path,
storage=monitor.storage
storage=monitor.storage,
space=monitor.space,
)
new_recipe.save()
import_count += 1
@@ -32,7 +32,7 @@ class Local(Provider):
log_entry = SyncLog(
status='SUCCESS',
msg='Imported ' + str(import_count) + ' recipes',
sync=monitor
sync=monitor,
)
log_entry.save()

View File

@@ -34,13 +34,13 @@ class Nextcloud(Provider):
import_count = 0
for file in files:
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path__iexact=path).exists() \
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
name = os.path.splitext(file)[0]
new_recipe = RecipeImport(
name=name,
file_path=path,
storage=monitor.storage
storage=monitor.storage,
space=monitor.space,
)
new_recipe.save()
import_count += 1
@@ -48,7 +48,7 @@ class Nextcloud(Provider):
log_entry = SyncLog(
status='SUCCESS',
msg='Imported ' + str(import_count) + ' recipes',
sync=monitor
sync=monitor,
)
log_entry.save()
@@ -68,14 +68,7 @@ class Nextcloud(Provider):
data = {'path': recipe.file_path, 'shareType': 3}
r = requests.post(
url,
headers=headers,
auth=HTTPBasicAuth(
recipe.storage.username, recipe.storage.password
),
data=data
)
r = requests.post(url, headers=headers, auth=HTTPBasicAuth(recipe.storage.username, recipe.storage.password), data=data)
response_json = r.json()

View File

@@ -1,17 +1,20 @@
from decimal import Decimal
from django.contrib.auth.models import User
from django.db.models import QuerySet
from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer)
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import ValidationError, NotAuthenticated, NotFound, ParseError
from rest_framework.fields import ModelField
from rest_framework.serializers import BaseSerializer, Serializer
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport,
ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation)
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation, ImportLog)
from cookbook.templatetags.custom_tags import markdown
@@ -39,6 +42,38 @@ class CustomDecimalField(serializers.Field):
raise ValidationError('A valid number is required')
class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data):
if type(data) == QuerySet and data.query.is_sliced:
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
if self.child.Meta.model == User:
data = data.filter(userpreference__space=self.context['request'].space)
else:
data = data.filter(**{'__'.join(data.model.get_space_key()): self.context['request'].space})
return super().to_representation(data)
class SpacedModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class MealTypeSerializer(SpacedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
list_serializer_class = SpaceFilterSerializer
model = MealType
fields = ('id', 'name', 'order', 'created_by')
read_only_fields = ('created_by',)
class UserNameSerializer(WritableNestedModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
@@ -46,11 +81,18 @@ class UserNameSerializer(WritableNestedModelSerializer):
return obj.get_user_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username')
class UserPreferenceSerializer(serializers.ModelSerializer):
def create(self, validated_data):
if validated_data['user'] != self.context['request'].user:
raise NotFound()
return super().create(validated_data)
class Meta:
model = UserPreference
fields = (
@@ -58,10 +100,14 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'comments'
)
read_only_fields = ['user']
class StorageSerializer(serializers.ModelSerializer):
class StorageSerializer(SpacedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = Storage
fields = (
@@ -69,13 +115,15 @@ class StorageSerializer(serializers.ModelSerializer):
'token', 'created_by'
)
read_only_fields = ('created_by',)
extra_kwargs = {
'password': {'write_only': True},
'token': {'write_only': True},
}
class SyncSerializer(serializers.ModelSerializer):
class SyncSerializer(SpacedModelSerializer):
class Meta:
model = Sync
fields = (
@@ -84,7 +132,7 @@ class SyncSerializer(serializers.ModelSerializer):
)
class SyncLogSerializer(serializers.ModelSerializer):
class SyncLogSerializer(SpacedModelSerializer):
class Meta:
model = SyncLog
fields = ('id', 'sync', 'status', 'msg', 'created_at')
@@ -97,6 +145,7 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
return str(obj)
class Meta:
list_serializer_class = SpaceFilterSerializer
model = Keyword
fields = (
'id', 'label',
@@ -111,17 +160,13 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
return str(obj)
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = Keyword.objects.get_or_create(name=validated_data['name'])
obj, created = Keyword.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
return obj
class Meta:
list_serializer_class = SpaceFilterSerializer
model = Keyword
fields = (
'id', 'name', 'icon', 'label', 'description',
'created_at', 'updated_at'
)
fields = ('id', 'name', 'icon', 'label', 'description', 'created_at', 'updated_at')
read_only_fields = ('id',)
@@ -129,9 +174,7 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = Unit.objects.get_or_create(name=validated_data['name'])
obj, created = Unit.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
return obj
class Meta:
@@ -143,9 +186,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'])
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
return obj
def update(self, instance, validated_data):
@@ -156,7 +197,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
fields = ('id', 'name')
class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
class SupermarketCategoryRelationSerializer(SpacedModelSerializer):
category = SupermarketCategorySerializer()
class Meta:
@@ -164,7 +205,7 @@ class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
fields = ('id', 'category', 'supermarket', 'order')
class SupermarketSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
class Meta:
@@ -176,9 +217,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = Food.objects.get_or_create(name=validated_data['name'])
obj, created = Food.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
return obj
def update(self, instance, validated_data):
@@ -256,6 +295,7 @@ class RecipeSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
@@ -265,7 +305,7 @@ class RecipeImageSerializer(WritableNestedModelSerializer):
fields = ['image', ]
class RecipeImportSerializer(serializers.ModelSerializer):
class RecipeImportSerializer(SpacedModelSerializer):
class Meta:
model = RecipeImport
fields = '__all__'
@@ -277,26 +317,32 @@ class CommentSerializer(serializers.ModelSerializer):
fields = '__all__'
class RecipeBookSerializer(serializers.ModelSerializer):
class RecipeBookSerializer(SpacedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = RecipeBook
fields = '__all__'
read_only_fields = ['id', 'created_by']
fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by')
read_only_fields = ('created_by',)
class RecipeBookEntrySerializer(serializers.ModelSerializer):
def create(self, validated_data):
book = validated_data['book']
if not book.get_owner() == self.context['request'].user:
raise NotFound(detail=None, code=None)
return super().create(validated_data)
class Meta:
model = RecipeBookEntry
fields = '__all__'
fields = ('id', 'book', 'recipe',)
class MealTypeSerializer(serializers.ModelSerializer):
class Meta:
model = MealType
fields = '__all__'
class MealPlanSerializer(serializers.ModelSerializer):
class MealPlanSerializer(SpacedModelSerializer):
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
note_markdown = serializers.SerializerMethodField('get_note_markdown')
@@ -305,6 +351,10 @@ class MealPlanSerializer(serializers.ModelSerializer):
def get_note_markdown(self, obj):
return markdown(obj.note)
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = MealPlan
fields = (
@@ -312,6 +362,7 @@ class MealPlanSerializer(serializers.ModelSerializer):
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'meal_type_name'
)
read_only_fields = ('created_by',)
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
@@ -348,13 +399,18 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
shared = UserNameSerializer(many=True)
supermarket = SupermarketSerializer(allow_null=True)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = ShoppingList
fields = (
'id', 'uuid', 'note', 'recipes', 'entries',
'shared', 'finished', 'supermarket', 'created_by', 'created_at'
)
read_only_fields = ('id',)
read_only_fields = ('id', 'created_by',)
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
@@ -366,27 +422,48 @@ class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
read_only_fields = ('id',)
class ShareLinkSerializer(serializers.ModelSerializer):
class ShareLinkSerializer(SpacedModelSerializer):
class Meta:
model = ShareLink
fields = '__all__'
class CookLogSerializer(serializers.ModelSerializer):
def create(self, validated_data): # TODO make mixin
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = CookLog
fields = '__all__'
fields = ('id', 'recipe', 'servings', 'rating', 'created_by', 'created_at')
read_only_fields = ('id', 'created_by')
class ViewLogSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = ViewLog
fields = '__all__'
fields = ('id', 'recipe', 'created_by', 'created_at')
read_only_fields = ('created_by',)
class ImportLogSerializer(serializers.ModelSerializer):
keyword = KeywordSerializer(read_only=True)
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = ImportLog
fields = ('id', 'type', 'msg', 'running', 'keyword', 'created_by', 'created_at')
read_only_fields = ('created_by',)
# Export/Import Serializers
@@ -455,4 +532,5 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Logo" transform="matrix(0.637323,0,0,0.637323,-243.095,-716.725)">
<g id="Kreis" transform="matrix(1.44936,0,0,1.50279,387.258,1039.34)">
<ellipse cx="273.123" cy="324.015" rx="259.822" ry="250.584" style="fill:url(#_Linear1);"/>
@@ -22,7 +20,7 @@
<g transform="matrix(1.471,0,0,1.471,406.537,1149.69)">
<path d="M256.049,220C286.222,219.994 312.656,207.31 329.388,194.134C346.35,180.754 370.899,183.406 384.611,200.1C407.129,227.376 420.598,261.944 420.598,299.53C420.598,361.08 382.604,437.101 329.764,463.706C307.035,475.15 283.466,480.586 256.098,480.599L256.098,480.599L256.049,480.599L256,480.599L256,480.599C228.632,480.586 205.063,475.15 182.334,463.706C129.494,437.101 91.5,361.08 91.5,299.53C91.5,261.944 104.969,227.376 127.487,200.1C141.199,183.406 165.748,180.754 182.71,194.134C199.442,207.31 225.876,219.994 256.049,220Z" style="fill:rgb(255,203,118);"/>
</g>
<g id="Flame-2" serif:id="Flame 2" transform="matrix(0.965725,0,0,0.89175,164.497,436.391)">
<g id="Flame-2" transform="matrix(0.965725,0,0,0.89175,164.497,436.391)">
<path d="M604.408,844.314C601.981,840.845 601.962,836.056 604.362,832.565C606.763,829.074 611.005,827.721 614.769,829.246C633.87,836.869 658.833,848.629 678.207,864.452C718.526,897.381 729.55,919.407 738.552,942.091C749.208,968.943 750.785,996.68 748.515,1016.08C742.018,1071.61 700.355,1117.5 641.034,1117.5C581.713,1117.5 534.493,1072.05 533.553,1016.08C532.986,982.372 543.985,955.443 555.988,936.22C558.982,931.437 564.594,929.469 569.609,931.444C574.623,933.419 577.757,938.831 577.215,944.58C575.493,956.716 574.362,969.372 574.932,979.484C576.863,1013.7 597.171,1022.5 618.083,1022.29C640.371,1022.08 662.925,1003.17 654.797,954.895C647.69,912.681 622.362,870.194 604.408,844.314Z" style="fill:rgb(255,111,0);"/>
<clipPath id="_clip3">
<path d="M604.408,844.314C601.981,840.845 601.962,836.056 604.362,832.565C606.763,829.074 611.005,827.721 614.769,829.246C633.87,836.869 658.833,848.629 678.207,864.452C718.526,897.381 729.55,919.407 738.552,942.091C749.208,968.943 750.785,996.68 748.515,1016.08C742.018,1071.61 700.355,1117.5 641.034,1117.5C581.713,1117.5 534.493,1072.05 533.553,1016.08C532.986,982.372 543.985,955.443 555.988,936.22C558.982,931.437 564.594,929.469 569.609,931.444C574.623,933.419 577.757,938.831 577.215,944.58C575.493,956.716 574.362,969.372 574.932,979.484C576.863,1013.7 597.171,1022.5 618.083,1022.29C640.371,1022.08 662.925,1003.17 654.797,954.895C647.69,912.681 622.362,870.194 604.408,844.314Z"/>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2400 5114 c-489 -42 -875 -173 -1253 -424 -504 -335 -875 -838
-1042 -1411 -71 -244 -99 -448 -99 -719 0 -271 28 -475 99 -719 217 -746 779
-1364 1507 -1657 670 -269 1444 -237 2093 89 474 237 876 632 1120 1100 430
825 383 1810 -123 2582 -197 300 -433 538 -729 735 -330 219 -671 348 -1073
405 -104 15 -414 27 -500 19z m57 -205 c343 -135 623 -380 734 -640 109 -255
100 -528 -23 -737 -47 -80 -161 -192 -242 -237 -122 -68 -202 -88 -356 -89
-155 -1 -219 12 -345 73 -254 121 -388 361 -355 633 17 143 89 327 151 389 26
26 71 24 97 -4 21 -22 22 -29 16 -158 -9 -170 6 -232 70 -295 103 -103 305
-97 395 13 44 53 61 113 61 213 0 190 -95 429 -276 693 -41 59 -74 114 -74
122 0 19 36 55 55 55 8 0 49 -14 92 -31z m-760 -1664 c28 -8 102 -49 164 -92
336 -225 669 -287 1018 -187 114 33 286 118 389 193 114 82 154 96 277 96 93
0 105 -2 167 -33 179 -88 384 -439 463 -792 23 -102 31 -390 15 -517 -66 -533
-392 -1104 -775 -1358 -159 -106 -385 -190 -598 -221 -154 -23 -471 -15 -605
15 -494 110 -830 393 -1082 913 -81 168 -139 336 -177 515 -25 116 -27 148
-28 343 0 235 8 293 67 480 74 233 244 509 368 595 89 62 227 83 337 50z"/>
<path d="M2390 4867 c0 -2 26 -21 58 -41 31 -20 45 -27 30 -14 -30 26 -88 62
-88 55z"/>
<path d="M2530 4765 c13 -14 26 -25 28 -25 3 0 -5 11 -18 25 -13 14 -26 25
-28 25 -3 0 5 -11 18 -25z"/>
<path d="M2615 4690 c27 -27 51 -50 54 -50 3 0 -17 23 -44 50 -27 28 -51 50
-54 50 -3 0 17 -22 44 -50z"/>
<path d="M2721 4565 c61 -82 69 -91 44 -48 -11 18 -36 52 -57 75 -30 33 -27
27 13 -27z"/>
<path d="M2790 4460 c6 -11 13 -20 16 -20 2 0 0 9 -6 20 -6 11 -13 20 -16 20
-2 0 0 -9 6 -20z"/>
<path d="M2840 4365 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
-5 8 -10 11 -10 5z"/>
<path d="M2101 4224 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M2901 4194 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M2081 4154 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M2921 4114 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M2072 4100 c0 -14 2 -19 5 -12 2 6 2 18 0 25 -3 6 -5 1 -5 -13z"/>
<path d="M2932 4055 c0 -16 2 -22 5 -12 2 9 2 23 0 30 -3 6 -5 -1 -5 -18z"/>
<path d="M2064 3965 c0 -55 1 -76 3 -47 2 29 2 74 0 100 -2 26 -3 2 -3 -53z"/>
<path d="M2942 3960 c0 -14 2 -19 5 -12 2 6 2 18 0 25 -3 6 -5 1 -5 -13z"/>
<path d="M2932 3875 c0 -16 2 -22 5 -12 2 9 2 23 0 30 -3 6 -5 -1 -5 -18z"/>
<path d="M2071 3834 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M2921 3824 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M2892 3755 c-12 -25 -20 -45 -17 -45 7 0 47 81 43 86 -3 2 -14 -17
-26 -41z"/>
<path d="M2844 3678 l-19 -23 23 19 c12 11 22 21 22 23 0 8 -8 2 -26 -19z"/>
<path d="M2130 3686 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
13z"/>
<path d="M2175 3630 c10 -11 20 -20 23 -20 3 0 -3 9 -13 20 -10 11 -20 20 -23
20 -3 0 3 -9 13 -20z"/>
<path d="M2784 3618 l-19 -23 23 19 c21 18 27 26 19 26 -2 0 -12 -10 -23 -22z"/>
<path d="M2210 3606 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
13z"/>
<path d="M2734 3584 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M2264 3566 c11 -9 24 -16 30 -16 12 0 7 5 -24 19 -24 11 -24 11 -6
-3z"/>
<path d="M2680 3560 c-8 -5 -10 -10 -5 -10 6 0 17 5 25 10 8 5 11 10 5 10 -5
0 -17 -5 -25 -10z"/>
<path d="M2320 3540 c8 -5 22 -9 30 -9 10 0 8 3 -5 9 -27 12 -43 12 -25 0z"/>
<path d="M2608 3533 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
<path d="M2378 3523 c6 -2 18 -2 25 0 6 3 1 5 -13 5 -14 0 -19 -2 -12 -5z"/>
<path d="M2553 3523 c9 -2 23 -2 30 0 6 3 -1 5 -18 5 -16 0 -22 -2 -12 -5z"/>
<path d="M2463 3513 c9 -2 25 -2 35 0 9 3 1 5 -18 5 -19 0 -27 -2 -17 -5z"/>
<path d="M2441 2505 c-30 -9 -69 -24 -87 -35 -47 -30 -103 -95 -125 -148 -37
-86 -72 -109 -189 -122 -135 -15 -235 -85 -288 -199 -35 -75 -37 -182 -4 -255
39 -86 85 -132 178 -177 46 -22 91 -50 99 -62 11 -15 15 -48 15 -115 0 -100
12 -138 52 -166 20 -14 80 -16 468 -16 388 0 448 2 468 16 40 28 52 66 52 166
0 67 4 100 15 115 8 12 53 40 99 62 136 66 200 162 200 301 0 177 -125 308
-314 330 -120 14 -148 33 -194 132 -33 69 -107 140 -174 163 -67 24 -204 29
-271 10z"/>
<path d="M2126 1059 c-55 -43 -17 -120 72 -147 55 -17 669 -17 724 0 89 27
127 104 72 147 -26 20 -37 21 -434 21 -397 0 -408 -1 -434 -21z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/import_response_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -47,7 +47,7 @@ class RecipeTable(tables.Table):
model = Recipe
template_name = 'recipes_table.html'
fields = (
'id', 'name', 'all_tags', 'image', 'instructions',
'id', 'name', 'all_tags', 'description', 'image', 'instructions',
'working_time', 'waiting_time', 'internal'
)

View File

@@ -13,18 +13,22 @@
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
<link rel="shortcut icon" href="{% static 'assets/favicon.svg' %}">
<link rel="icon" type="image/png" href="{% static 'assets/favicon.svg' %}" sizes="32x32">
<link rel="icon" type="image/png" href="{% static 'assets/favicon.svg' %}" sizes="96x96">
<link rel="apple-touch-icon" href="{% static 'assets/favicon.svg' %}">
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'assets/favicon.svg' %}">
<link rel="apple-touch-icon" sizes="152x152" href="{% static 'assets/favicon.svg' %}">
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'assets/favicon.svg' %}">
<link rel="apple-touch-icon" sizes="167x167" href="{% static 'assets/favicon.svg' %}">
<link rel="icon" type="image/png" href="{% static 'assets/favicon-32x32.png' %}" sizes="32x32">
<link rel="icon" type="image/png" href="{% static 'assets/favicon-16x16.png' %}" sizes="16x16">
<link rel="mask-icon" href="{% static 'assets/safari-pinned-tab.svg' %}" color="#161616">
<link rel="apple-touch-icon" href="{% static 'assets/apple-touch-icon.png' %}" sizes="180x180">
<link rel="manifest" href="{% url 'web_manifest' %}">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161616">
<meta name="msapplication-TileColor" content="#161616">
<meta name="theme-color" content="#ffffff">
<!-- Bootstrap 4 -->
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
@@ -163,7 +167,8 @@
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'account_logout' %}">{% trans 'Login' %} <i class="fas fa-sign-in-alt"></i></a>
<a class="nav-link" href="{% url 'account_login' %}">{% trans 'Login' %} <i
class="fas fa-sign-in-alt"></i></a>
</li>
{% endif %}
</ul>
@@ -223,7 +228,7 @@
<script type="application/javascript">
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("{% url 'service_worker' %}", { scope: '/' }).then(function (reg) {
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: '/'}).then(function (reg) {
}).catch(function (err) {
console.warn('Error whilst registering service worker', err);

View File

@@ -46,6 +46,16 @@
<input class="form-control" id="id_name" v-model="recipe.name">
</div>
</div>
<div class="row">
<div class="col-12">
<label for="id_description">
{% trans 'Description' %}
</label>
<textarea id="id_description" class="form-control" v-model="recipe.description"
maxlength="512"></textarea>
</div>
</div>
<br/>
<div class="row">
@@ -77,6 +87,9 @@
:hide-selected="true"
:preserve-search="true"
placeholder="{% trans 'Select Keywords' %}"
tag-placeholder="{% trans 'Add Keyword' %}"
:taggable="true"
@tag="addKeyword"
label="label"
track-by="id"
id="id_keywords"
@@ -87,19 +100,6 @@
</div>
</div>
<div class="row">
<div class="col-12">
<label for="id_description">
{% trans 'Description' %}
</label>
<textarea id="id_description" class="form-control" v-model="recipe.description"
maxlength="512"></textarea>
</div>
</div>
<template v-if="recipe !== undefined">
<div class="row" v-if="recipe.nutrition" style="margin-top: 1vh">
<div class="col-md-12">
@@ -667,6 +667,10 @@
this.units.push(new_unit.unit)
this.recipe.steps[step].ingredients[id] = new_unit
},
addKeyword: function (tag) {
let new_keyword = {'label':tag,'name':tag}
this.recipe.keywords.push(new_keyword)
},
searchKeywords: function (query) {
this.keywords_loading = true
this.$http.get("{% url 'api:keyword-list' %}" + '?query=' + query + '&limit=10').then((response) => {

View File

@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Import' %}{% endblock %}
{% block content %}
<div id="app">
<import-response-view></import-response-view>
</div>
{% endblock %}
{% block script %}
<script src="{% url 'javascript-catalog' %}"></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.IMPORT_ID = {{pk}};
</script>
{% render_bundle 'chunk-vendors' %}
{% render_bundle 'import_response_view' %}
{% endblock %}

View File

@@ -2,7 +2,7 @@
{% load static %}
{% load i18n %}
{% block title %}{% trans "Offline" %}{% endblock %}
{% block title %}{% trans "No Permissions" %}{% endblock %}
{% block content %}
@@ -12,7 +12,12 @@
<h1 class="">{% trans 'No Permissions' %}</h1>
<br/>
<span>{% trans 'You do not have any groups and therefor cannot use this application. Please contact your administrator.' %}</span> <br/>
<span>
{% trans 'You do not have any groups and therefor cannot use this application.' %}
{% trans 'Please contact your administrator.' %}
</span>
<br/>
</div>

View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "No Permission" %}{% endblock %}
{% block content %}
<div style="text-align: center">
<h1 class="">{% trans 'No Permission' %}</h1>
<br/>
<span>{% trans 'You do not have the required permissions to view this page or perform this action.' %} {% trans 'Please contact your administrator.' %}</span> <br/>
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "No Space" %}{% endblock %}
{% block content %}
<div style="text-align: center">
<h1 class="">{% trans 'No Space' %}</h1>
<br/>
<span>{% trans 'You are not a member of any space.' %} {% trans 'Please contact your administrator.' %}</span> <br/>
</div>
{% endblock %}

View File

@@ -11,13 +11,13 @@
{% block table %}
<table {% render_attrs table.attrs class="table" %}>
{% for row in table.paginated_rows %}
<div class="card" style="margin-top: 2px;">
<div class="card" style="margin-top: 1px;">
<div class="row no-gutters">
<div class="col-md-4">
<a href="{% url 'view_recipe' row.cells.id %}">
{% if row.cells.image|length > 1 %}
<img src=" {{ row.cells.image }}" alt="{% trans 'Recipe Image' %}"
class="card-img" style="object-fit: cover;height: 130px">
class="card-img" style="object-fit:cover;height: 160px">
{% else %}
<img src="{% static 'assets/recipe_no_image.svg' %}"
alt="{% trans 'Recipe Image' %}"
@@ -31,10 +31,15 @@
<div class="card-body">
<div class="d-flex">
<div class="flex-fill">
<h5 class="card-title">{{ row.cells.name }}
<h5 class="card-title p-0 m-0">{{ row.cells.name }}
{% recipe_rating row.record request.user as rating %}
{{ rating|safe }}
</h5>
{%if row.record.description|length > 0 %}
<p class="card-subtitle p-0 m-0 text-muted" style="height:3em; overflow:hidden;">
{{ row.cells.description }}
</p>
{% endif %}
<p class="card-text{% if not row.record.keywords %} d-none d-lg-block{% endif %}">
{% for x in row.record.keywords.all %}
<span class="badge badge-pill badge-light">{{ x }}</span>
@@ -80,6 +85,10 @@
onclick="openCookLogModal({{ row.record.pk }})"><i
class="fas fa-clipboard-list fa-fw"></i> {% trans 'Log Cooking' %}
</button>
<a class="dropdown-item"
href="{% url 'delete_recipe' row.record.pk %}"><i
class="fas fa-trash fa-fw"></i> {% trans 'Delete' %}
</a>
</div>
</div>

View File

@@ -122,8 +122,9 @@
</tr>
</thead>
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries"
@change="dragChanged(c, $event)">
<tr v-for="(element, index) in c.entries" :key="element.id">
@change="dragChanged(c, $event)" handle=".handle">
<tr v-for="(element, index) in c.entries" :key="element.id"
v-bind:class="{ 'text-muted': element.checked }">
<td class="handle"><i class="fas fa-sort"></i></td>
<td>[[element.amount]]</td>
<td>[[element.unit.name]]</td>
@@ -154,7 +155,8 @@
<div class="input-group">
<input id="id_simple_entry" class="form-control" v-model="simple_entry">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><i class="fa fa-plus"></i>
<button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i
class="fa fa-plus"></i>
</button>
</div>
</div>
@@ -349,6 +351,8 @@
</div>
</div>
<br/>
<br/>
<b-modal id="id_modal_export" title="{% trans 'Copy/Export' %}">
<div class="row">
@@ -418,6 +422,8 @@
users_loading: false,
onLine: navigator.onLine,
simple_entry: '',
auto_sync_blocked: false,
auto_sync_running: false,
entry_mode_simple: $cookies.isKey('shopping_entry_mode_simple') ? ($cookies.get('shopping_entry_mode_simple') === 'true') : true,
},
directives: {
@@ -448,7 +454,7 @@
name: gettext('Uncategorized'),
id: -1,
entries: [],
order: 99999999
order: -1
}
}
@@ -542,6 +548,7 @@
this.loadShoppingList()
{% if recipes %}
this.loading = true
this.edit_mode = true
let loadingRecipes = []
@@ -556,7 +563,8 @@
{% if request.user.userpreference.shopping_auto_sync > 0 %}
setInterval(() => {
if ((this.shopping_list_id !== null) && !this.edit_mode && window.navigator.onLine) {
if ((this.shopping_list_id !== null) && !this.edit_mode && window.navigator.onLine && !this.auto_sync_blocked && !this.auto_sync_running) {
this.auto_sync_running = true
this.loadShoppingList(true)
}
}, {% widthratio request.user.userpreference.shopping_auto_sync 1 1000 %})
@@ -605,6 +613,7 @@
})
},
loadInitialRecipe: function (recipe, servings) {
servings = 1 //TODO temporary until i can actually fix the servings for this #453
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
this.addRecipeToList(response.data, servings)
}).catch((err) => {
@@ -620,16 +629,19 @@
this.shopping_list = response.body
this.loading = false
} else {
let check_map = {}
for (let e of response.body.entries) {
check_map[e.id] = {checked: e.checked}
}
if (!this.auto_sync_blocked) {
let check_map = {}
for (let e of response.body.entries) {
check_map[e.id] = {checked: e.checked}
}
for (let se of this.shopping_list.entries) {
if (check_map[se.id] !== undefined) {
se.checked = check_map[se.id].checked
for (let se of this.shopping_list.entries) {
if (check_map[se.id] !== undefined) {
se.checked = check_map[se.id].checked
}
}
}
this.auto_sync_running = false
}
if (this.shopping_list.entries.length === 0) {
this.edit_mode = true
@@ -743,18 +755,24 @@
}
},
entryChecked: function (entry) {
this.auto_sync_blocked = true
let updates = []
this.shopping_list.entries.forEach((item) => {
if (entry.entries.includes(item.id)) {
item.checked = entry.checked
this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
updates.push(this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
this.loading = false
})
}))
}
})
Promise.allSettled(updates).then(() => {
this.auto_sync_blocked = false
})
},
addEntry: function () {
if (this.new_entry.food !== undefined) {

File diff suppressed because one or more lines are too long

View File

@@ -22,23 +22,6 @@
<a href="{% url 'list_invite_link' %}" class="btn btn-success">{% trans 'Show Links' %}</a>
</div>
<!--
<div class="col-md-6">
<h3>{% trans 'Backup & Restore' %}</h3>
<a href="{% url 'api_backup' %}" class="btn btn-success">{% trans 'Download Backup' %}</a>
<br/> <br/>
<div class="alert alert-danger" role="alert">
⚠️ Backups simply create a so called fixture. Fixtures are json files containing all your data (WITHOUT
MEDIA FILES) <br>
They can be imported into django by running <code style="color: white">manage.py loaddata [fixture-name]</code> <br>
It is planned to provide a better way of backing up and restoring data but it is not yet implemented.<br><br>
⚠️<b>Please make sure to setup a solid backup strategy on your server to save the Database and the <code style="color: white">mediafiles</code>
directory</b>⚠️
</div>
</div>
-->
</div>
<br/>

View File

@@ -30,6 +30,19 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="input-group mb-3">
<input class="form-control" v-model="json_data" placeholder="{% trans 'Enter json directly' %}">
<div class="input-group-append">
<button @click="loadRecipeJson()" class="btn btn-primary shadow-none" type="button"
id="id_btn_search"><i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
<br/>
<div v-if="loading" class="text-center">
@@ -45,6 +58,12 @@
<input id="id_name" class="form-control" v-model="recipe_data.name">
</div>
<div class="form-group">
<label for="id_description">{% trans 'Recipe Description' %}</label>
<textarea id="id_description" class="form-control" v-model="recipe_data.description"
rows="4"></textarea>
</div>
<div class="row">
<div class="col col-md-6" v-if="recipe_data.image !== ''">
<img v-bind:src="recipe_data.image" alt="{% trans 'Recipe Image' %}"
@@ -190,6 +209,9 @@
:hide-selected="true"
:preserve-search="true"
placeholder="{% trans 'Select one' %}"
tag-placeholder="{% trans 'Add Keyword' %}"
:taggable="true"
@tag="addKeyword"
label="text"
track-by="id"
id="id_keywords"
@@ -280,6 +302,7 @@
loading: false,
all_keywords: false,
importing_recipe: false,
json_data: '',
},
directives: {
tabindex: {
@@ -316,10 +339,45 @@
this.error = err.data
this.loading = false
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
let msg = gettext('There was an error loading a resource!')
if (err.bodyText.length < 300) {
msg += err.bodyText
} else {
msg += ' ' + err.status + ' ' + err.statusText
}
this.makeToast(gettext('Error'), msg, 'danger')
})
},
loadRecipeJson: function () {
this.recipe_data = undefined
this.error = undefined
this.loading = true
this.$http.post("{% url 'api_recipe_from_json' %}", {'json': this.json_data}, {emulateJSON: true}).then((response) => {
console.log(response.data)
this.recipe_data = response.data;
this.loading = false
}).catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
let msg = gettext('There was an error loading a resource!')
if (err.bodyText.length < 300) {
msg += err.bodyText
} else {
msg += ' ' + err.status + ' ' + err.statusText
}
this.makeToast(gettext('Error'), msg, 'danger')
})
},
importRecipe: function () {
if (this.recipe_data.name.length > 128) {
this.makeToast(gettext('Error'), gettext('Recipe name is longer than 128 characters'), 'danger')
return;
}
if (this.recipe_data.description.length > 512) {
this.makeToast(gettext('Error'), gettext('Recipe description is longer than 512 characters'), 'danger')
return;
}
if (this.importing_recipe) {
this.makeToast(gettext('Error'), gettext('Already importing the selected recipe, please wait!'), 'danger')
return;
@@ -357,6 +415,10 @@
this.units.push(new_unit.unit)
this.recipe_data.recipeIngredient[index] = new_unit
},
addKeyword: function (tag) {
let new_keyword = {'text': tag, 'id': null}
this.recipe_data.keywords.push(new_keyword)
},
openUnitSelect: function (id) {
let index = id.replace('unit_', '')
if (this.recipe_data.recipeIngredient[index].unit !== null) {

View File

@@ -1,6 +1,6 @@
import bleach
import markdown as md
from bleach_whitelist import markdown_attrs, markdown_tags
from bleach_allowlist import markdown_attrs, markdown_tags
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name

View File

@@ -0,0 +1,7 @@
from django.test import utils
from django_scopes import scopes_disabled
# disables scoping error in all queries used inside the test FUNCTIONS
# FIXTURES need to have their own scopes_disabled!!
# This is done by hook pytest_fixture_setup in conftest.py for all non yield fixtures
utils.setup_databases = scopes_disabled()(utils.setup_databases)

View File

@@ -0,0 +1,116 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Keyword, CookLog
LIST_URL = 'api:cooklog-list'
DETAIL_URL = 'api:cooklog-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1, recipe_1_s1):
return CookLog.objects.create(recipe=recipe_1_s1, created_by=auth.get_user(u1_s1), space=space_1)
@pytest.fixture
def obj_2(space_1, u1_s1, recipe_1_s1):
return CookLog.objects.create(recipe=recipe_1_s1, created_by=auth.get_user(u1_s1), space=space_1)
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'servings': 2},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['servings'] == 2
# TODO disabled until https://github.com/vabene1111/recipes/issues/484
# @pytest.mark.parametrize("arg", [
# ['a_u', 403],
# ['g1_s1', 201],
# ['u1_s1', 201],
# ['a1_s1', 201],
# ])
# def test_add(arg, request, u1_s2, u2_s1, recipe_1_s1):
# c = request.getfixturevalue(arg[0])
# r = c.post(
# reverse(LIST_URL),
# {'recipe': recipe_1_s1.id},
# content_type='application/json'
# )
# response = json.loads(r.content)
# assert r.status_code == arg[1]
# if r.status_code == 201:
# assert response['recipe'] == recipe_1_s1.id
# r = c.get(reverse(DETAIL_URL, args={response['id']}))
# assert r.status_code == 200
# r = u2_s1.get(reverse(DETAIL_URL, args={response['id']}))
# assert r.status_code == 404
# r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
# assert r.status_code == 404
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert CookLog.objects.count() == 0

View File

@@ -1,69 +1,148 @@
import json
from cookbook.models import Food
from cookbook.tests.views.test_views import TestViews
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food
LIST_URL = 'api:food-list'
DETAIL_URL = 'api:food-detail'
class TestApiUnit(TestViews):
@pytest.fixture()
def obj_1(space_1):
return Food.objects.get_or_create(name='test_1', space=space_1)[0]
def setUp(self):
super(TestApiUnit, self).setUp()
self.food_1 = Food.objects.create(
name='Beef'
@pytest.fixture
def obj_2(space_1):
return Food.objects.get_or_create(name='test_2', space=space_1)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
def test_list_filter(obj_1, obj_2, u1_s1):
r = u1_s1.get(reverse(LIST_URL))
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 2
assert response[0]['name'] == obj_1.name
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
assert len(response) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_add_duplicate(u1_s1, u1_s2, obj_1):
r = u1_s1.post(
reverse(LIST_URL),
{'name': obj_1.name},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] == obj_1.id
r = u1_s2.post(
reverse(LIST_URL),
{'name': obj_1.name},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] != obj_1.id
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
self.food_2 = Food.objects.create(
name='Chicken'
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
def test_keyword_list(self):
# verify view permissions are applied accordingly
self.batch_requests(
[
(self.anonymous_client, 403),
(self.guest_client_1, 403),
(self.user_client_1, 200),
(self.admin_client_1, 200),
(self.superuser_client, 200)
],
reverse('api:food-list')
)
# verify storage is returned
r = self.user_client_1.get(reverse('api:food-list'))
self.assertEqual(r.status_code, 200)
response = json.loads(r.content)
self.assertEqual(len(response), 2)
self.assertEqual(response[0]['name'], self.food_1.name)
r = self.user_client_1.get(f'{reverse("api:food-list")}?limit=1')
response = json.loads(r.content)
self.assertEqual(len(response), 1)
r = self.user_client_1.get(f'{reverse("api:food-list")}?query=Pork')
response = json.loads(r.content)
self.assertEqual(len(response), 0)
r = self.user_client_1.get(f'{reverse("api:food-list")}?query=Beef')
response = json.loads(r.content)
self.assertEqual(len(response), 1)
def test_keyword_update(self):
r = self.user_client_1.patch(
reverse(
'api:food-detail',
args={self.food_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
self.assertEqual(r.status_code, 200)
self.assertEqual(response['name'], 'new')
def test_keyword_delete(self):
r = self.user_client_1.delete(
reverse('api:food-detail', args={self.food_1.id})
)
self.assertEqual(r.status_code, 204)
self.assertEqual(Food.objects.count(), 1)
assert r.status_code == 204
with scopes_disabled():
assert Food.objects.count() == 0

View File

@@ -0,0 +1,86 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Keyword, CookLog, ViewLog, ImportLog
LIST_URL = 'api:importlog-list'
DETAIL_URL = 'api:importlog-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1, recipe_1_s1):
return ImportLog.objects.create(type='test', created_by=auth.get_user(u1_s1), space=space_1)
@pytest.fixture
def obj_2(space_1, u1_s1, recipe_1_s1):
return ImportLog.objects.create(type='test', created_by=auth.get_user(u1_s1), space=space_1)
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'msg': 'new'},
content_type='application/json'
)
assert r.status_code == arg[1]
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert ImportLog.objects.count() == 0

View File

@@ -0,0 +1,106 @@
import json
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, Ingredient
LIST_URL = 'api:ingredient-list'
DETAIL_URL = 'api:ingredient-detail'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 10
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
with scopes_disabled():
recipe_1_s1.space = space_2
recipe_1_s1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, recipe_1_s1):
with scopes_disabled():
i = recipe_1_s1.steps.first().ingredients.first()
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={i.id}
),
{'note': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['note'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'food': {'name': 'test'}, 'unit': {'name': 'test'}, 'amount': 1},
content_type='application/json'
)
response = json.loads(r.content)
print(r)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['id'] == 1
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404 # ingredient is not linked to a recipe and therefore cannot be accessed
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(u1_s1, u1_s2, recipe_1_s1):
with scopes_disabled():
i = recipe_1_s1.steps.first().ingredients.first()
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={i.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={i.id}
)
)
assert r.status_code == 204
assert not Ingredient.objects.filter(pk=i.id).exists()

View File

@@ -1,74 +1,148 @@
import json
from cookbook.models import Keyword
from cookbook.tests.views.test_views import TestViews
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Keyword
LIST_URL = 'api:keyword-list'
DETAIL_URL = 'api:keyword-detail'
class TestApiKeyword(TestViews):
@pytest.fixture()
def obj_1(space_1):
return Keyword.objects.get_or_create(name='test_1', space=space_1)[0]
def setUp(self):
super(TestApiKeyword, self).setUp()
self.keyword_1 = Keyword.objects.create(
name='meat'
@pytest.fixture
def obj_2(space_1):
return Keyword.objects.get_or_create(name='test_2', space=space_1)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
def test_list_filter(obj_1, obj_2, u1_s1):
r = u1_s1.get(reverse(LIST_URL))
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 2
assert response[0]['name'] == obj_1.name
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
assert len(response) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_add_duplicate(u1_s1, u1_s2, obj_1):
r = u1_s1.post(
reverse(LIST_URL),
{'name': obj_1.name},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] == obj_1.id
r = u1_s2.post(
reverse(LIST_URL),
{'name': obj_1.name},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] != obj_1.id
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
self.keyword_2 = Keyword.objects.create(
name='veggies'
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
def test_keyword_list(self):
# verify view permissions are applied accordingly
self.batch_requests(
[
(self.anonymous_client, 403),
(self.guest_client_1, 403),
(self.user_client_1, 200),
(self.admin_client_1, 200),
(self.superuser_client, 200)
],
reverse('api:keyword-list')
)
# verify storage is returned
r = self.user_client_1.get(reverse('api:keyword-list'))
self.assertEqual(r.status_code, 200)
response = json.loads(r.content)
self.assertEqual(len(response), 2)
self.assertEqual(response[0]['name'], self.keyword_1.name)
r = self.user_client_1.get(f'{reverse("api:keyword-list")}?limit=1')
response = json.loads(r.content)
self.assertEqual(len(response), 1)
r = self.user_client_1.get(
f'{reverse("api:keyword-list")}?query=chicken'
)
response = json.loads(r.content)
self.assertEqual(len(response), 0)
r = self.user_client_1.get(f'{reverse("api:keyword-list")}?query=MEAT')
response = json.loads(r.content)
self.assertEqual(len(response), 1)
def test_keyword_update(self):
r = self.user_client_1.patch(
reverse(
'api:keyword-detail',
args={self.keyword_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
self.assertEqual(r.status_code, 200)
self.assertEqual(response['name'], 'new')
def test_keyword_delete(self):
r = self.user_client_1.delete(
reverse(
'api:keyword-detail',
args={self.keyword_1.id}
)
)
self.assertEqual(r.status_code, 204)
self.assertEqual(Keyword.objects.count(), 1)
assert r.status_code == 204
with scopes_disabled():
assert Keyword.objects.count() == 0

View File

@@ -0,0 +1,134 @@
import json
from datetime import datetime, timedelta
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, MealPlan, MealType
LIST_URL = 'api:mealplan-list'
DETAIL_URL = 'api:mealplan-detail'
@pytest.fixture()
def meal_type(space_1, u1_s1):
return MealType.objects.get_or_create(name='test', space=space_1, created_by=auth.get_user(u1_s1))[0]
@pytest.fixture()
def obj_1(space_1, recipe_1_s1, meal_type, u1_s1):
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), created_by=auth.get_user(u1_s1))
@pytest.fixture
def obj_2(space_1, recipe_1_s1, meal_type, u1_s1):
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), created_by=auth.get_user(u1_s1))
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
def test_list_filter(obj_1, u1_s1):
r = u1_s1.get(reverse(LIST_URL))
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?to_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'title': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['title'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'recipe': recipe_1_s1.id, 'meal_type': meal_type.id, 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['title'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert MealPlan.objects.count() == 0

View File

@@ -0,0 +1,132 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, MealType
LIST_URL = 'api:mealtype-list'
DETAIL_URL = 'api:mealtype-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
return MealType.objects.get_or_create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
return MealType.objects.get_or_create(name='test_2', created_by=auth.get_user(u1_s1), space=space_1)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
# TODO make name in space unique
# def test_add_duplicate(u1_s1, u1_s2, obj_1):
# r = u1_s1.post(
# reverse(LIST_URL),
# {'name': obj_1.name},
# content_type='application/json'
# )
# response = json.loads(r.content)
# assert r.status_code == 201
# assert response['id'] == obj_1.id
#
# r = u1_s2.post(
# reverse(LIST_URL),
# {'name': obj_1.name},
# content_type='application/json'
# )
# response = json.loads(r.content)
# assert r.status_code == 201
# assert response['id'] != obj_1.id
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert MealType.objects.count() == 0

View File

@@ -1,31 +1,104 @@
from cookbook.models import Recipe
from cookbook.tests.views.test_views import TestViews
from django.contrib import auth
import json
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, Ingredient, Step, Recipe
LIST_URL = 'api:recipe-list'
DETAIL_URL = 'api:recipe-detail'
class TestApiShopping(TestViews):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def setUp(self):
super(TestApiShopping, self).setUp()
self.internal_recipe = Recipe.objects.create(
name='Test',
internal=True,
created_by=auth.get_user(self.user_client_1)
)
def test_shopping_view_permissions(self):
self.batch_requests(
[
(self.anonymous_client, 403),
(self.guest_client_1, 200),
(self.user_client_1, 200),
(self.user_client_2, 200),
(self.admin_client_1, 200),
(self.superuser_client, 200)
],
def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
with scopes_disabled():
recipe_1_s1.space = space_2
recipe_1_s1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, recipe_1_s1):
with scopes_disabled():
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
'api:recipe-detail', args={self.internal_recipe.id})
DETAIL_URL,
args={recipe_1_s1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test', 'waiting_time': 0, 'working_time': 0, 'keywords': [], 'steps': []},
content_type='application/json'
)
response = json.loads(r.content)
print(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['id'] == 1
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(u1_s1, u1_s2, recipe_1_s1):
with scopes_disabled():
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={recipe_1_s1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={recipe_1_s1.id}
)
)
# TODO add tests for editing
assert r.status_code == 204
assert not Recipe.objects.filter(pk=recipe_1_s1.id).exists()

View File

@@ -0,0 +1,130 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import RecipeBook
LIST_URL = 'api:recipebook-list'
DETAIL_URL = 'api:recipebook-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
return RecipeBook.objects.get_or_create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
return RecipeBook.objects.get_or_create(name='test_2', created_by=auth.get_user(u1_s1), space=space_1)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
def test_list_filter(obj_1, obj_2, u1_s1):
r = u1_s1.get(reverse(LIST_URL))
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 2
assert response[0]['name'] == obj_1.name
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
assert len(response) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
print(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert RecipeBook.objects.count() == 0

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