Compare commits

..

384 Commits
1.4.8 ... 1.5.5

Author SHA1 Message Date
Miha Perpar
ba361a8a27 Translated using Weblate (Slovenian)
Currently translated at 59.0% (307 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2023-08-13 08:19:59 +00:00
Miha Perpar
fc2ce6e488 Translated using Weblate (Slovenian)
Currently translated at 15.9% (81 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/sl/
2023-08-13 08:19:59 +00:00
Tomasz Klimczak
d7f77a572a Translated using Weblate (Polish)
Currently translated at 100.0% (520 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2023-08-13 08:19:58 +00:00
Fabian Flodman
64e28fd01a Translated using Weblate (German)
Currently translated at 97.3% (506 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-08-13 08:19:58 +00:00
Thomas
714d5e5184 Translated using Weblate (German)
Currently translated at 97.3% (506 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-08-13 08:19:58 +00:00
Fabian Flodman
640500c82d Translated using Weblate (German)
Currently translated at 100.0% (490 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2023-08-13 08:19:58 +00:00
vabene1111
4b14a099df better logging 2023-08-05 12:00:03 +02:00
vabene1111
dae7cbfb85 version script updates and system page fix 2023-08-05 10:56:27 +02:00
vabene1111
0c62b80e3a Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2023-08-05 10:28:44 +02:00
vabene1111
678963e6dd more debug in version script 2023-08-05 10:28:39 +02:00
vabene1111
b8e1ed8967 Merge pull request #2570 from TandoorRecipes/dependabot/pip/cryptography-41.0.3
Bump cryptography from 41.0.2 to 41.0.3
2023-08-05 09:45:13 +02:00
Chen
d87633433a Translated using Weblate (Hebrew)
Currently translated at 90.5% (471 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/he/
2023-08-03 22:19:55 +00:00
Chen
fe33adbba0 Translated using Weblate (Hebrew)
Currently translated at 25.7% (134 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/he/
2023-08-02 15:51:50 +00:00
Chen
baa84cf481 Added translation using Weblate (Hebrew) 2023-08-02 15:26:57 +00:00
dependabot[bot]
2b8c607b78 Bump cryptography from 41.0.2 to 41.0.3
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.3)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-02 02:23:22 +00:00
Mára Štěpánek
eac059ca85 Translated using Weblate (Czech)
Currently translated at 100.0% (362 of 362 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/cs/
2023-07-31 14:19:56 +00:00
vabene1111
782dd4cb17 build stuff 2023-07-29 11:24:11 +02:00
vabene1111
f7b60f2c52 version script improvements 2023-07-29 10:55:18 +02:00
vabene1111
ca28e52698 keep git installed 2023-07-29 10:06:51 +02:00
vabene1111
0c2c12d536 improved version script 2023-07-29 08:43:17 +02:00
vabene1111
113c40c243 changed version command order 2023-07-29 08:38:13 +02:00
vabene1111
0688f46d8b new version script 2023-07-29 08:32:10 +02:00
vabene1111
2fdcdba889 base pasth pdf viewer 2023-07-29 07:48:27 +02:00
vabene1111
6a39148e5f fixed try catch and added git to permanent dependency 2023-07-28 15:59:48 +02:00
vabene1111
22dfb40fd5 improved system info even more 2023-07-27 20:48:51 +02:00
vabene1111
2b5a86ce53 improved system page 2023-07-27 20:40:25 +02:00
vabene1111
e77016ea9b playing around 2023-07-27 18:49:39 +02:00
vabene1111
9988a61da7 added version number to system screen 2023-07-27 18:39:21 +02:00
vabene1111
f34fb8eec3 Merge pull request #2563 from smilerz/test_fixes
fixed rating sort order and updated tests
2023-07-26 06:21:54 +02:00
smilerz
7853357065 fix error when filtering on rating in saved filters 2023-07-25 17:48:19 -05:00
smilerz
6f1befc43c fixed rating sort order and updated tests 2023-07-25 11:37:48 -05:00
vabene1111
c18386b9b5 fixed copied ingredients being linked together 2023-07-22 12:59:31 +02:00
vabene1111
d5ba2e6716 improved multi url import 2023-07-22 11:18:06 +02:00
vabene1111
b30f8c245e added option to set URL on food 2023-07-22 09:12:45 +02:00
vabene1111
74c86f1b6b Merge pull request #2541 from titilambert/patch-1
Expose food description in food form
2023-07-22 08:28:39 +02:00
smilerz
cf9d599536 fixed sort by rating so that unrated are always last 2023-07-20 15:39:35 -05:00
vabene1111
14a67fd6c2 improved spinner rendering 2023-07-20 16:24:25 +02:00
vabene1111
413da01c5c Merge pull request #2554 from TandoorRecipes/dependabot/npm_and_yarn/vue/word-wrap-1.2.4
Bump word-wrap from 1.2.3 to 1.2.4 in /vue
2023-07-19 09:05:55 +02:00
dependabot[bot]
a73d231bd4 Bump word-wrap from 1.2.3 to 1.2.4 in /vue
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-19 07:05:07 +00:00
vabene1111
4f2392faac updated pyyaml to be compatible with cython 3 2023-07-19 09:04:01 +02:00
vabene1111
2321dcec6c Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-07-18 16:40:42 +02:00
vabene1111
c2cf7ba758 fixed test 2023-07-18 16:40:38 +02:00
vabene1111
239dd4aa60 Merge pull request #2481 from TandoorRecipes/dependabot/pip/pytube-15.0.0
Bump pytube from 12.1.0 to 15.0.0
2023-07-18 15:35:04 +02:00
vabene1111
a653b2e777 Merge pull request #2525 from TandoorRecipes/dependabot/pip/whitenoise-6.5.0
Bump whitenoise from 6.2.0 to 6.5.0
2023-07-18 15:34:52 +02:00
vabene1111
d8faee7e93 Merge pull request #2545 from TandoorRecipes/dependabot/pip/cryptography-41.0.2
Bump cryptography from 41.0.0 to 41.0.2
2023-07-18 15:32:29 +02:00
vabene1111
69417425e9 Merge branch 'develop' into dependabot/pip/cryptography-41.0.2 2023-07-18 15:32:23 +02:00
vabene1111
e8574a49a7 Merge pull request #2544 from TandoorRecipes/dependabot/npm_and_yarn/vue/semver-5.7.2
Bump semver from 5.7.1 to 5.7.2 in /vue
2023-07-18 15:31:59 +02:00
vabene1111
fe624cd218 Merge pull request #2536 from TandoorRecipes/dependabot/pip/django-4.1.10
Bump django from 4.1.9 to 4.1.10
2023-07-18 15:31:34 +02:00
vabene1111
1f10a66c74 added base unit to unit editor 2023-07-18 13:54:35 +02:00
vabene1111
a8f1cd26cd change guest recipe permission 2023-07-18 10:54:20 +02:00
vabene1111
a497a6b7f5 space api read for all users in space 2023-07-15 13:57:25 +02:00
dependabot[bot]
9dc144f2b5 Bump cryptography from 41.0.0 to 41.0.2
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.0 to 41.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.0...41.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-15 01:23:46 +00:00
Eirik Skarding
7d50f3cf21 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.9% (344 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-07-12 21:19:57 +00:00
dependabot[bot]
315af4911c Bump semver from 5.7.1 to 5.7.2 in /vue
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-11 17:57:36 +00:00
vabene1111
35704c69c7 added option to pass recipe to recipe view 2023-07-11 17:50:48 +02:00
vabene1111
a24628c771 fixed userspace tetsts 2023-07-11 17:25:43 +02:00
vabene1111
e9748a160a addded paginated user space endpoint 2023-07-11 17:01:56 +02:00
Thibault Cohen
7bc78e104f Expose food description in food form 2023-07-10 21:26:26 -04:00
Mára Štěpánek
6f0dccfec9 Translated using Weblate (Czech)
Currently translated at 97.5% (487 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2023-07-06 21:19:59 +00:00
Rubens
76d6981dab Translated using Weblate (Catalan)
Currently translated at 85.1% (417 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ca/
2023-07-06 21:19:59 +00:00
dependabot[bot]
5df37c52dd Bump django from 4.1.9 to 4.1.10
Bumps [django](https://github.com/django/django) from 4.1.9 to 4.1.10.
- [Commits](https://github.com/django/django/compare/4.1.9...4.1.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-06 00:10:14 +00:00
vabene1111
7a2ccc075c improved shopping entry api endpoint performance 2023-07-04 16:49:56 +02:00
vabene1111
237054c23e improved commonly used administrative admin fields 2023-07-03 22:30:27 +02:00
vabene1111
ac1d641bd5 added RO DRF permission and internal_note filters for invite/userspace 2023-07-03 21:59:15 +02:00
vabene1111
3545b6e98a plugin loader improvements 2023-07-03 17:56:05 +02:00
vabene1111
d3a56e00ea allow disabling plugins 2023-07-03 07:41:56 +02:00
vabene1111
e9f8578c25 re added path to plugin check 2023-07-03 07:02:56 +02:00
vabene1111
dccfc436be Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-07-03 05:55:17 +02:00
vabene1111
1e85c8587b fixed plugin error message 2023-07-03 05:55:12 +02:00
vabene1111
b8f92ab054 Merge pull request #2531 from michael-genson/feature/add-source-url-to-recipe-export
Add source URL to recipe export
2023-07-03 05:47:31 +02:00
Mára Štěpánek
766ed31f8e Translated using Weblate (Czech)
Currently translated at 79.7% (398 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2023-07-02 21:19:57 +00:00
Michael Genson
cad78e115d added source url to recipe export 2023-07-02 10:42:41 -05:00
dependabot[bot]
ad7ebf1cd5 Bump whitenoise from 6.2.0 to 6.5.0
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 6.2.0 to 6.5.0.
- [Changelog](https://github.com/evansd/whitenoise/blob/main/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/6.2.0...6.5.0)

---
updated-dependencies:
- dependency-name: whitenoise
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 00:03:23 +00:00
vabene1111
b599c4f6a9 added internal notes and improved invite link form 2023-06-30 23:09:22 +02:00
vabene1111
439539f56d show optional fields in generic forms 2023-06-30 23:09:01 +02:00
vabene1111
237bcb92c9 fixed food editor default properties unit 2023-06-29 17:26:49 +02:00
vabene1111
ce02a23dbb fixed quick ingredient import in recipe editor 2023-06-29 17:13:53 +02:00
vabene1111
8e81512735 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-06-29 17:05:37 +02:00
vabene1111
c69f0394a8 possibly fixed bug with food editor ingredient delete page reload 2023-06-29 17:05:32 +02:00
vabene1111
d7ca9e05de Merge pull request #2521 from gloriousDan/improve-docs
add note to docker-compose files and update postgres tag
2023-06-29 17:04:31 +02:00
vabene1111
64534ff810 fixed navbar color for non logged in users 2023-06-29 17:03:05 +02:00
vabene1111
d0164a6c28 Merge pull request #2522 from gloriousDan/fix-raspi
Fix Raspi build and consolidate with normal build and image
2023-06-27 16:10:39 +02:00
Daniel Schulz
0f898ddf4a unify raspi and normal build again 2023-06-27 00:51:55 +02:00
Daniel Schulz
e903382034 update alpine to v3.18 2023-06-27 00:51:22 +02:00
Daniel Schulz
0d225450da add note to docker-compose files and update postgres tag 2023-06-27 00:33:29 +02:00
vabene1111
c077a64484 further improvements 2023-06-26 20:57:51 +02:00
vabene1111
6c16094b42 added initial version of tandoor dark theme 2023-06-26 20:43:50 +02:00
vabene1111
5aa80746f9 Merge branch 'develop' 2023-06-26 20:25:58 +02:00
vabene1111
cc64717818 auto add schema attrs in json importer 2023-06-26 20:22:59 +02:00
vabene1111
6acd892116 fixed broken image would fail default importer 2023-06-26 20:18:36 +02:00
vabene1111
3955408aa4 dont show properties view if no properties are present in DB 2023-06-26 20:03:25 +02:00
vabene1111
3de2468df3 fixed to light nav color in some themes 2023-06-26 19:57:38 +02:00
vabene1111
b1d983fbc3 fixed required field in food 2023-06-26 17:08:45 +02:00
vabene1111
5f443d2593 fixed issue when creating food with properties 2023-06-26 16:48:50 +02:00
vabene1111
436158f596 fixed allow decimals in food property amount 2023-06-26 15:47:44 +02:00
vabene1111
dcc56fc138 added new docs entry to nav 2023-06-26 15:21:05 +02:00
vabene1111
0eef10079b Merge pull request #2517 from 16cdlogan/patch-1
Create Truenas-Portainer
2023-06-26 15:19:03 +02:00
16cdlogan
2b839dfb19 Create Truenas-Portainer
Install Tandoor Recipes on TrueNAS Core and Portainer
2023-06-25 21:32:54 -04:00
sweeney
491b678d6e Translated using Weblate (Greek)
Currently translated at 1.4% (7 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/
2023-06-25 14:19:55 +00:00
sweeney
151dce006d Translated using Weblate (Greek)
Currently translated at 54.7% (287 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-06-25 14:19:55 +00:00
sweeney
d4f538b4aa Translated using Weblate (Greek)
Currently translated at 35.4% (186 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-06-24 13:32:57 +00:00
sweeney
a727439c57 Added translation using Weblate (Greek) 2023-06-24 13:32:57 +00:00
vabene1111
f779107749 Merge branch 'develop' 2023-06-24 12:17:48 +02:00
vabene1111
4a5c8f41fa fixed open data slug uniqueness check 2023-06-24 12:15:47 +02:00
vabene1111
bf458e22e8 fixed merging deleting food properties 2023-06-24 11:52:42 +02:00
sweeney
9b8088fca2 Translated using Weblate (Greek)
Currently translated at 25.5% (134 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-06-23 09:19:56 +00:00
Thomas
68435aa335 Translated using Weblate (German)
Currently translated at 99.7% (498 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-06-23 09:19:56 +00:00
vabene1111
afe5465044 added styling options to several components 2023-06-22 15:58:32 +02:00
vabene1111
9decf3cf14 Merge branch 'develop' 2023-06-22 11:30:19 +02:00
vabene1111
b31c3cfd2f updated CI node version 2023-06-22 10:15:45 +02:00
vabene1111
1306c7381c fixed keyword import error 2023-06-22 10:11:00 +02:00
vabene1111
dbd2025e71 updated lock file 2023-06-22 08:55:31 +02:00
vabene1111
f19f4abe0c fixed yarn lock? 2023-06-21 21:41:27 +02:00
vabene1111
7c4a854bfd update lock file
might still be broken because the stupid build does not work
2023-06-21 21:21:15 +02:00
vabene1111
04322b56a4 fixed recipe view component 2023-06-21 21:12:14 +02:00
vabene1111
45b4ac3e9e fixed broken mealplan 2023-06-21 21:06:19 +02:00
vabene1111
362ed9b088 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-06-21 17:06:06 +02:00
vabene1111
8bf347dd09 moved recipe view to component (currently broken) 2023-06-21 17:05:59 +02:00
John Doe
d449f0c2fc Translated using Weblate (Czech)
Currently translated at 10.8% (54 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2023-06-21 14:20:01 +00:00
Tobias Huppertz
6dab514817 Translated using Weblate (German)
Currently translated at 99.5% (497 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-06-21 14:20:01 +00:00
Tobias Huppertz
8ce0d416c2 Translated using Weblate (German)
Currently translated at 100.0% (490 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2023-06-21 14:20:01 +00:00
vabene1111
dd88641763 improved plugin nav capabilities 2023-06-21 16:07:32 +02:00
vabene1111
fb52f34ef9 Merge branch 'master' into develop 2023-06-21 14:52:39 +02:00
vabene1111
561c2f2d1f Merge branch 'develop' 2023-06-21 14:48:11 +02:00
vabene1111
4b48c1046e rezeptsuite enhancements 2023-06-20 16:49:00 +02:00
vabene1111
3e0f2fbddc fixed recipesage servings and time 2023-06-20 16:31:40 +02:00
vabene1111
c5eb025186 fixed nextcloud import how to step 2023-06-20 16:22:02 +02:00
vabene1111
23bfc3c3b0 re-added property imports for open data importer 2023-06-20 15:42:25 +02:00
vabene1111
813c7a46f1 added additonal verification of imported images 2023-06-20 13:35:34 +02:00
vabene1111
6b475468fc added some more validation 2023-06-20 13:22:44 +02:00
vabene1111
053ff9506a fixed open data import store error 2023-06-20 13:03:18 +02:00
John Doe
11a699ed47 Translated using Weblate (Czech)
Currently translated at 7.0% (35 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2023-06-19 10:16:11 +00:00
vabene1111
b3c6cacdad added better edge case handling to recipe card 2023-06-13 17:33:12 +02:00
vabene1111
4875b158fd fixed open data importer 2023-06-13 13:23:57 +02:00
vabene1111
6bb04dc56d improved property functions 2023-06-12 17:20:00 +02:00
vabene1111
2dc038edc7 fixed url import array in name 2023-06-12 16:18:24 +02:00
vabene1111
8597c3e95d Merge pull request #2489 from TandoorRecipes/dependabot/pip/cryptography-41.0.0
Bump cryptography from 39.0.1 to 41.0.0
2023-06-08 16:30:20 +02:00
vabene1111
5c0094fd43 Merge pull request #2478 from jwr1/develop
Fix bottom navigation not hiding in print mode
2023-06-08 14:43:51 +02:00
vabene1111
23d67a5bd3 compile messages and added norwegian to language option 2023-06-08 14:40:55 +02:00
vabene1111
3a26f09307 Merge pull request #2488 from smilerz/delete_empty
add admin command to delete unattached ingredients and steps
2023-06-08 14:38:34 +02:00
Eirik Skarding
2592e606cc Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.0% (220 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-06-08 00:19:55 +00:00
Eirik Skarding
11f2b95b4d Translated using Weblate (Norwegian Bokmål)
Currently translated at 35.8% (179 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-06-06 12:19:55 +00:00
Eirik Skarding
c171a01b7d Translated using Weblate (Norwegian Bokmål)
Currently translated at 33.8% (169 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-06-03 14:19:56 +00:00
dependabot[bot]
2671519386 Bump cryptography from 39.0.1 to 41.0.0
Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.1 to 41.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/39.0.1...41.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-02 20:15:48 +00:00
smilerz
19750cf499 add admin command to delete unattached ingredients and steps 2023-06-02 08:58:08 -05:00
vabene1111
711f80b1fb Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2023-06-01 21:44:48 +02:00
vabene1111
1ffa0f396a fixed fuzzy filter mixing not working without login 2023-06-01 21:44:44 +02:00
dependabot[bot]
991a51d55e Bump pytube from 12.1.0 to 15.0.0
Bumps [pytube](https://github.com/pytube/pytube) from 12.1.0 to 15.0.0.
- [Release notes](https://github.com/pytube/pytube/releases)
- [Commits](https://github.com/pytube/pytube/compare/v12.1.0...v15.0.0)

---
updated-dependencies:
- dependency-name: pytube
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-01 00:57:43 +00:00
sweeney
e052a7869d Translated using Weblate (Greek)
Currently translated at 11.0% (58 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-05-31 17:19:57 +00:00
Tomasz Klimczak
d57f35e4e8 Translated using Weblate (Polish)
Currently translated at 100.0% (499 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2023-05-31 17:19:57 +00:00
John Wesley
2cb7030b04 Fix bottom navigation not hiding in print mode 2023-05-29 11:46:07 -04:00
vabene1111
a53f17c1b9 default properties food unit 2023-05-29 17:37:09 +02:00
vabene1111
326549568f added unit conversion editor to food editor 2023-05-29 17:16:22 +02:00
vabene1111
c0577abb89 fixed generic modal form error (merge conflict) 2023-05-29 15:40:25 +02:00
vabene1111
a65e93a9b3 fixed property helper bug with non food ingredients 2023-05-29 12:43:14 +02:00
Luis Cacho
cadf14c338 Translated using Weblate (Spanish)
Currently translated at 74.3% (357 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/es/
2023-05-26 16:19:57 +00:00
Luis Cacho
7b49f1f437 Translated using Weblate (Spanish)
Currently translated at 56.1% (275 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/es/
2023-05-26 16:19:57 +00:00
vabene1111
2214540a51 Merge branch 'feature/unit-conversion' into develop 2023-05-26 16:11:20 +02:00
vabene1111
256b7b1543 Merge branch 'develop' into feature/unit-conversion
# Conflicts:
#	cookbook/helper/recipe_url_import.py
2023-05-26 16:11:11 +02:00
vabene1111
ebc213395d added property api test 2023-05-26 16:06:49 +02:00
vabene1111
7af581f0ff allow users to choose between food and recipe properties 2023-05-26 15:59:49 +02:00
vabene1111
aeb944b281 dont show properties if no reference amout is given 2023-05-26 15:32:55 +02:00
vabene1111
43105ddd2f fixed onhand test (cache) and fixed shared recipe properties 2023-05-26 10:57:08 +02:00
vabene1111
f2b3cfb8f0 Merge branch 'develop' 2023-05-26 09:56:17 +02:00
vabene1111
3302dacdc3 properties structure imporioved 2023-05-25 16:13:16 +02:00
sardigital
5f07ef04d2 Translated using Weblate (Russian)
Currently translated at 71.6% (344 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ru/
2023-05-25 06:19:56 +00:00
vabene1111
4c69a0b721 fixed json import missing source url attribute 2023-05-24 20:32:47 +02:00
vabene1111
2a538abf80 test work 2023-05-24 15:59:25 +02:00
vabene1111
3236b65d9e food editor and property view improvements 2023-05-24 13:49:29 +02:00
vabene1111
79cd17a5ba Merge branch 'develop' into feature/unit-conversion
# Conflicts:
#	vue/src/components/Modals/GenericModalForm.vue
2023-05-24 08:53:49 +02:00
vabene1111
06a08dcf6e allow plugins to add navs 2023-05-23 16:05:12 +02:00
vabene1111
de29b44c0d Merge branch 'develop' 2023-05-23 15:32:57 +02:00
vabene1111
dc4ca81270 updated generic modal form to always show errors 2023-05-23 15:32:53 +02:00
vabene1111
dd3dc0a058 Merge branch 'develop' 2023-05-23 14:49:06 +02:00
vabene1111
30c6389382 added force show error parameter to standard toast 2023-05-23 14:18:59 +02:00
vabene1111
45effbbcde Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-05-23 13:35:11 +02:00
vabene1111
ffa06ca75e emit additonal hidden event from generic modal form 2023-05-23 13:35:06 +02:00
vabene1111
903a4c93eb Merge pull request #2470 from gloriousDan/enable_gunicorn_debug_log
Enable gunicorn debug log
2023-05-23 13:13:21 +02:00
vabene1111
a8ae6c86e2 Merge pull request #2474 from TandoorRecipes/dependabot/pip/requests-2.31.0
Bump requests from 2.28.2 to 2.31.0
2023-05-23 13:12:25 +02:00
vabene1111
976445c1f0 Merge pull request #2435 from screendriver/noindex
Prevent indexing content by search engines
2023-05-23 13:11:12 +02:00
dependabot[bot]
9cf1141794 Bump requests from 2.28.2 to 2.31.0
Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 06:26:41 +00:00
vabene1111
b095bee229 added FDA key .env (temporarily) and plugin loader catch block 2023-05-22 17:47:13 +02:00
vabene1111
3c3ecc5342 comonent in generic form trial
# Conflicts:
#	vue/src/components/FoodEditor.vue
#	vue/src/components/Modals/GenericModalForm.vue
#	vue/src/utils/models.js
2023-05-21 20:26:00 +02:00
Daniel Schulz
8b50b99977 make gunicorn log level setting available 2023-05-21 14:06:54 +02:00
vabene1111
f369b74c94 fixed plugin bundle path 2023-05-21 12:00:20 +02:00
vabene1111
7b11f276a8 added - before prefix 2023-05-21 11:39:04 +02:00
vabene1111
fe35173ab5 open data tag suffix 2023-05-21 11:05:59 +02:00
vabene1111
4bd879c787 added link creation to open data workflow 2023-05-21 10:57:47 +02:00
vabene1111
fcbc5ed5d0 changed sub repo path 2023-05-21 10:55:07 +02:00
vabene1111
2bdc541183 fixed repo install 2023-05-21 10:52:12 +02:00
vabene1111
4b08eea39d test plugin container release 2023-05-21 10:47:42 +02:00
vabene1111
c777cfe5b9 improved plugin functionality
- added abiulity to extend default api router from plugion
- added dability to pass custom model definition to generic model/api functions
- added ability to pass custom API clients to generic API function
2023-05-20 12:53:14 +02:00
vabene1111
e860d0aa83 Merge branch 'develop' into feature/unit-conversion 2023-05-18 14:29:39 +02:00
vabene1111
b5681a0255 Merge branch 'develop' 2023-05-18 14:29:31 +02:00
vabene1111
ddd2f96b85 updated translations 2023-05-18 14:29:19 +02:00
vabene1111
b56b778573 Merge pull request #2458 from ambroisie/fix-multiple-files-field
Fix multiple file field
2023-05-18 14:26:41 +02:00
vabene1111
cf7fc906bb Merge pull request #2463 from TandoorRecipes/dependabot/pip/django-4.1.9
Bump django from 4.1.7 to 4.1.9
2023-05-18 14:26:25 +02:00
vabene1111
0539e1ea15 food edit modal done 2023-05-11 17:13:35 +02:00
dependabot[bot]
c5c37296e9 Bump django from 4.1.7 to 4.1.9
Bumps [django](https://github.com/django/django) from 4.1.7 to 4.1.9.
- [Commits](https://github.com/django/django/compare/4.1.7...4.1.9)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-09 22:47:01 +00:00
vabene1111
6030fa1d68 property scaling and ui 2023-05-08 12:09:55 +02:00
vabene1111
2a5cba0178 improvements to property calculation 2023-05-07 00:30:32 +02:00
vabene1111
9a77089c6d improved unit conversion and tests 2023-05-06 23:57:45 +02:00
vabene1111
5f79895a97 added migration for existing nutrition information 2023-05-06 23:21:09 +02:00
vabene1111
19f5da77b2 cleanup migrations, remove pint to speed up base conversion and calculate properties on converted ingredients 2023-05-06 22:21:27 +02:00
vabene1111
2cc7278865 extremly innefficent WIP 2023-05-06 20:54:36 +02:00
vabene1111
60f31608b9 added recipe properties 2023-05-06 19:14:25 +02:00
vabene1111
763f71a05c cleanup views 2023-05-06 17:40:39 +02:00
vabene1111
e3921cd6a8 Merge branch 'develop' into feature/unit-conversion 2023-05-06 17:01:06 +02:00
vabene1111
54a5c145cc comonent in generic form trial 2023-05-05 17:04:20 +02:00
vabene1111
86fd0dcab1 made the open data importer its own component 2023-05-05 16:33:30 +02:00
Bruno BELANYI
6b04c92297 Fix multiple file field
Due to [1], the previous solution does not work on recent Django releases.

See [2] for the documented work-around, as applied in this commit.

Closes #2457.

[1]: https://docs.djangoproject.com/en/4.2/releases/4.2.1/
[2]: https://docs.djangoproject.com/en/4.2/topics/http/file-uploads/#uploading-multiple-files
2023-05-04 22:41:19 +01:00
vabene1111
12da77f037 beser response and stuff 2023-05-04 17:12:49 +02:00
vabene1111
071926aada improved importer merging behavior 2023-05-04 15:29:06 +02:00
vabene1111
33d048e623 improve importer 2023-05-04 08:43:36 +02:00
vabene1111
135640dd58 Merge pull request #2454 from smilerz/pytest_fixes
reload food objects to resolve inconsistent behavior
2023-05-03 19:24:46 +02:00
smilerz
d8ddf66921 reload food objects to resolve inconsistent behavior 2023-05-03 10:17:59 -05:00
vabene1111
274fce5236 fixed importer 2023-05-02 16:27:03 +02:00
vabene1111
1046065f46 fixed load bulk 2023-05-01 22:36:39 +02:00
vabene1111
60243ad901 load bulk 2023-05-01 13:39:09 +02:00
vabene1111
d62c49eb2f not yet fully working food import 2023-05-01 13:29:14 +02:00
vabene1111
7e3313f48c added automation to docs TOC 2023-05-01 12:59:55 +02:00
axeron2036
ea4c16cc2a Translated using Weblate (Russian)
Currently translated at 9.6% (48 of 496 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ru/
2023-05-01 07:55:47 +00:00
vabene1111
1bb6eb7141 import open data content 2023-04-30 22:30:56 +02:00
vabene1111
89e3e85d1e Merge branch 'develop' into feature/unit-conversion
# Conflicts:
#	requirements.txt
2023-04-30 21:51:56 +02:00
vabene1111
dfde340447 basic import working 2023-04-30 21:51:28 +02:00
Oliver Cervera
e7239c7c68 Translated using Weblate (Italian)
Currently translated at 91.4% (448 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/it/
2023-04-29 07:55:49 +00:00
vabene1111
f7ef2ed4f5 Merge pull request #2437 from gabe565/ci-fix-stable-notify
Fix stable release Discord notification
2023-04-27 13:18:59 +02:00
noxonad
56f6de3510 Translated using Weblate (Romanian)
Currently translated at 100.0% (480 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ro/
2023-04-27 08:55:57 +00:00
noxonad
cf86af7a23 Translated using Weblate (Romanian)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ro/
2023-04-27 08:55:57 +00:00
Mike Miller
fe32a743db Translated using Weblate (German)
Currently translated at 100.0% (480 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-04-27 08:55:57 +00:00
Sebastian Krug
93b750dbf1 Translated using Weblate (German)
Currently translated at 100.0% (480 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-04-27 08:55:57 +00:00
Sebastian Krug
4337f594f6 Translated using Weblate (German)
Currently translated at 100.0% (489 of 489 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2023-04-27 08:55:57 +00:00
Gabe Cook
17fc24fc1b Fix stable release Discord notification 2023-04-26 11:19:38 -05:00
Christian Rackerseder
44771bde71 Prevent indexing content by search engines 2023-04-26 12:19:30 +02:00
vabene1111
018e9ef88f Merge branch 'develop' 2023-04-26 09:04:16 +02:00
vabene1111
7d99a9a9c3 updated translation files 2023-04-26 07:46:48 +02:00
vabene1111
c4078800e3 Merge pull request #2434 from ssams/ldap-auth-starttls
ldap auth: allow connecting using StartTLS
2023-04-26 07:43:44 +02:00
ssams
d87f0f3c15 ldap auth: allow connecting using StartTLS 2023-04-25 21:30:47 +02:00
vabene1111
6e9b504a9d Merge pull request #2407 from TandoorRecipes/dependabot/pip/django-allauth-0.54.0
Bump django-allauth from 0.52.0 to 0.54.0
2023-04-25 16:03:14 +02:00
dependabot[bot]
7397210729 Bump django-allauth from 0.52.0 to 0.54.0
Bumps [django-allauth](https://github.com/pennersr/django-allauth) from 0.52.0 to 0.54.0.
- [Release notes](https://github.com/pennersr/django-allauth/releases)
- [Changelog](https://github.com/pennersr/django-allauth/blob/master/ChangeLog.rst)
- [Commits](https://github.com/pennersr/django-allauth/compare/0.52.0...0.54.0)

---
updated-dependencies:
- dependency-name: django-allauth
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-25 13:59:53 +00:00
vabene1111
7830ddd4e9 Merge pull request #2431 from smilerz/pytest_fixes
Pytest fixes
2023-04-25 15:59:15 +02:00
smilerz
b711ee5257 update User factory to make username unique 2023-04-24 07:33:47 -05:00
smilerz
c7b6253e04 fixed food tree search filter tests 2023-04-24 07:01:28 -05:00
Michael
85dcb6c61f Translated using Weblate (German)
Currently translated at 99.5% (478 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-04-23 15:55:48 +00:00
smilerz
323ded1814 fix touchable 2023-04-22 17:57:15 -05:00
smilerz
595de0c5a1 fixed pytests 2023-04-22 17:56:01 -05:00
smilerz
8c3195937b fix food tests 2023-04-21 13:14:23 -05:00
Espen Sellevåg
8a8be7fb2d Translated using Weblate (Norwegian Bokmål)
Currently translated at 16.6% (80 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-04-17 20:55:48 +00:00
axeron2036
ec68da051d Translated using Weblate (Russian)
Currently translated at 71.6% (344 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ru/
2023-04-17 20:55:48 +00:00
Espen Sellevåg
29f13d687c Translated using Weblate (Norwegian Bokmål)
Currently translated at 52.8% (196 of 371 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nb_NO/
2023-04-17 20:55:47 +00:00
Espen Sellevåg
980e83b23c Translated using Weblate (Norwegian Bokmål)
Currently translated at 7.9% (38 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-04-15 22:55:48 +00:00
Espen Sellevåg
668ccf89fd Translated using Weblate (Norwegian Bokmål)
Currently translated at 28.8% (107 of 371 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nb_NO/
2023-04-15 22:55:47 +00:00
vabene1111
3ec02db2f6 working food property editor 2023-04-15 11:19:20 +02:00
Espen Sellevåg
93f7da3ed9 Added translation using Weblate (Norwegian Bokmål) 2023-04-14 21:41:01 +00:00
vabene1111
3f88778013 Merge pull request #2421 from noxonad/patch-2
Fixed another typo
2023-04-13 13:44:22 +02:00
vabene1111
499d026b5c Merge pull request #2422 from noxonad/patch-3
Fixed typo in json
2023-04-13 13:43:56 +02:00
noxonad
e0a1189430 Translated using Weblate (Ukrainian)
Currently translated at 0.3% (2 of 528 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/uk/
2023-04-12 11:55:59 +00:00
noxonad
e2905eb999 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (528 of 528 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/bg/
2023-04-12 11:55:59 +00:00
noxonad
8b6f2c1e70 Translated using Weblate (Danish)
Currently translated at 100.0% (528 of 528 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/da/
2023-04-12 11:55:59 +00:00
noxonad
b7e4e53519 Translated using Weblate (Portuguese (Brazil))
Currently translated at 5.8% (33 of 562 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pt_BR/
2023-04-12 11:55:59 +00:00
noxonad
80eee255f7 Translated using Weblate (Slovenian)
Currently translated at 15.5% (79 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/sl/
2023-04-12 11:55:59 +00:00
noxonad
60a4a63f56 Translated using Weblate (Romanian)
Currently translated at 100.0% (480 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ro/
2023-04-12 11:55:59 +00:00
noxonad
82b80e60e6 Translated using Weblate (Romanian)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ro/
2023-04-12 11:55:59 +00:00
noxonad
915d0359bf Translated using Weblate (Russian)
Currently translated at 9.4% (47 of 496 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ru/
2023-04-12 11:55:59 +00:00
noxonad
10f1a77c1c Translated using Weblate (Russian)
Currently translated at 71.0% (341 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ru/
2023-04-12 11:55:59 +00:00
noxonad
ea141577d0 Translated using Weblate (Italian)
Currently translated at 92.1% (444 of 482 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/it/
2023-04-12 11:55:59 +00:00
noxonad
d0a1151a33 Translated using Weblate (Hungarian)
Currently translated at 87.5% (422 of 482 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/hu/
2023-04-12 11:55:59 +00:00
noxonad
bc461997f8 Translated using Weblate (French)
Currently translated at 91.9% (443 of 482 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2023-04-12 11:55:59 +00:00
noxonad
d8051203c1 Translated using Weblate (German)
Currently translated at 99.5% (487 of 489 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2023-04-12 11:55:59 +00:00
noxonad
af71407ca6 Translated using Weblate (Catalan)
Currently translated at 86.3% (416 of 482 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ca/
2023-04-12 11:55:58 +00:00
vabene1111
b275c53e5a first ideas of property editor 2023-04-11 17:27:10 +02:00
vabene1111
7d9fcac0c7 basic food property viewer in recipe view 2023-04-11 16:48:38 +02:00
noxonad
cb9a90d1e7 Fixed typo in json 2023-04-11 16:40:16 +03:00
vabene1111
ec083214ef Merge branch 'develop' into feature/unit-conversion 2023-04-11 14:46:58 +02:00
noxonad
4a4e4719b3 Fixed another typo 2023-04-11 14:25:29 +03:00
vabene1111
a0ff489be0 Merge pull request #2415 from smilerz/import_automation
added keyword automation to url import
2023-04-08 12:01:08 +02:00
vabene1111
147aae318a Merge pull request #2416 from dnlmlr/patch-1
Small fix for german language
2023-04-08 11:59:40 +02:00
dnlmlr
d8e61a485e Small fix for german language 2023-04-06 17:43:02 +02:00
vabene1111
c2be329495 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-04-06 17:22:06 +02:00
vabene1111
2552d27f6f added plugin architecture 2023-04-06 17:22:02 +02:00
smilerz
9db5e45c26 added keyword automation to url import 2023-04-05 15:46:10 -05:00
vabene1111
2a6fc723d0 first food property UI prototype 2023-04-04 13:13:51 +02:00
vabene1111
74f88eb952 Merge pull request #2414 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue-cli-plugin-i18n-2.3.2
Bump vue-cli-plugin-i18n from 2.3.1 to 2.3.2 in /vue
2023-04-03 02:09:34 +02:00
vabene1111
9a0a99a21f Merge pull request #2413 from TandoorRecipes/dependabot/npm_and_yarn/vue/typescript-eslint/parser-5.57.0
Bump @typescript-eslint/parser from 5.56.0 to 5.57.0 in /vue
2023-04-03 02:09:18 +02:00
vabene1111
b35b731b6d Merge pull request #2412 from TandoorRecipes/dependabot/npm_and_yarn/vue/popperjs/core-2.11.7
Bump @popperjs/core from 2.11.6 to 2.11.7 in /vue
2023-04-03 02:09:06 +02:00
vabene1111
e8d2b95aaa Merge pull request #2409 from TandoorRecipes/dependabot/pip/markdown-3.4.3
Bump markdown from 3.4.1 to 3.4.3
2023-04-03 02:08:43 +02:00
vabene1111
6be1ddfe87 Merge pull request #2406 from TandoorRecipes/dependabot/pip/django-auth-ldap-4.2.0
Bump django-auth-ldap from 4.1.0 to 4.2.0
2023-04-03 02:08:14 +02:00
vabene1111
5b518d4a4c Merge pull request #2408 from TandoorRecipes/dependabot/pip/recipe-scrapers-14.35.0
Bump recipe-scrapers from 14.30.0 to 14.35.0
2023-04-03 02:07:42 +02:00
vabene1111
1c6db468e1 Merge pull request #2410 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue-cookies-1.8.3
Bump vue-cookies from 1.8.2 to 1.8.3 in /vue
2023-04-03 02:07:11 +02:00
vabene1111
25c914606e improved converters and helpers 2023-04-02 10:54:57 +02:00
vabene1111
44cb2d9807 improved tests and limited conversion to existing units 2023-04-02 10:05:28 +02:00
vabene1111
f90a66af1e Merge branch 'develop' into feature/unit-conversion 2023-04-02 09:17:38 +02:00
dependabot[bot]
519e36379b Bump vue-cli-plugin-i18n from 2.3.1 to 2.3.2 in /vue
Bumps [vue-cli-plugin-i18n](https://github.com/intlify/vue-cli-plugin-i18n) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/intlify/vue-cli-plugin-i18n/releases)
- [Changelog](https://github.com/intlify/vue-cli-plugin-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-cli-plugin-i18n/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: vue-cli-plugin-i18n
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:59:18 +00:00
dependabot[bot]
4726598deb Bump @typescript-eslint/parser from 5.56.0 to 5.57.0 in /vue
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.56.0 to 5.57.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.57.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:58:57 +00:00
dependabot[bot]
1e57e7e70b Bump @popperjs/core from 2.11.6 to 2.11.7 in /vue
Bumps [@popperjs/core](https://github.com/popperjs/popper-core) from 2.11.6 to 2.11.7.
- [Release notes](https://github.com/popperjs/popper-core/releases)
- [Commits](https://github.com/popperjs/popper-core/compare/v2.11.6...v2.11.7)

---
updated-dependencies:
- dependency-name: "@popperjs/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:58:33 +00:00
dependabot[bot]
6f1777d37d Bump vue-cookies from 1.8.2 to 1.8.3 in /vue
Bumps [vue-cookies](https://github.com/cmp-cc/vue-cookies) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/cmp-cc/vue-cookies/releases)
- [Commits](https://github.com/cmp-cc/vue-cookies/compare/v1.8.2...v1.8.3)

---
updated-dependencies:
- dependency-name: vue-cookies
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:57:53 +00:00
dependabot[bot]
d6d9066eea Bump markdown from 3.4.1 to 3.4.3
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.4.1 to 3.4.3.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/change_log/release-2.6.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.4.1...3.4.3)

---
updated-dependencies:
- dependency-name: markdown
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:57:13 +00:00
dependabot[bot]
2c7e7f859b Bump recipe-scrapers from 14.30.0 to 14.35.0
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 14.30.0 to 14.35.0.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/14.30.0...14.35.0)

---
updated-dependencies:
- dependency-name: recipe-scrapers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:57:06 +00:00
dependabot[bot]
97e5d23d98 Bump django-auth-ldap from 4.1.0 to 4.2.0
Bumps [django-auth-ldap](https://github.com/django-auth-ldap/django-auth-ldap) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/django-auth-ldap/django-auth-ldap/releases)
- [Changelog](https://github.com/django-auth-ldap/django-auth-ldap/blob/master/docs/changes.rst)
- [Commits](https://github.com/django-auth-ldap/django-auth-ldap/compare/4.1.0...4.2.0)

---
updated-dependencies:
- dependency-name: django-auth-ldap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 00:56:58 +00:00
vabene1111
a8cbef7bd4 Merge pull request #2403 from smilerz/debug_toolbar
enable toolbar seperate from debug logging
2023-03-30 07:37:56 +02:00
smilerz
569143a7ee enable toolbar seperate from debug logging 2023-03-29 15:52:46 -05:00
vabene1111
3e8f0c3aae Merge pull request #2379 from thomaspreece/develop
Improve parsing of OpenEats imports
2023-03-29 16:04:07 +02:00
vabene1111
11620ba2b6 Update openeats.py 2023-03-29 16:03:33 +02:00
vabene1111
073ee7e963 Update openeats.py 2023-03-29 16:03:08 +02:00
vabene1111
9620689bd0 disable space creation for demo user on hosted instance 2023-03-28 23:22:14 +02:00
vabene1111
b8cbda10f1 disable space creation for demo user on hosted instance 2023-03-28 23:21:57 +02:00
Thomas Preece
5a145d7f8e PR feedback changes 2023-03-28 17:30:58 +01:00
vabene1111
0ba2fa296a added token auth for share link endpoint 2023-03-28 15:42:34 +02:00
Matěj Kubla
2a5fc22dd7 Translated using Weblate (Czech)
Currently translated at 100.0% (362 of 362 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/cs/
2023-03-25 11:32:52 +00:00
Matěj Kubla
e739c4d627 Added translation using Weblate (Czech) 2023-03-25 11:32:51 +00:00
vabene1111
7e350b2f90 improved property calculator 2023-03-25 07:59:07 +01:00
vabene1111
6d5592c1be basic food property calculation 2023-03-25 07:46:06 +01:00
vabene1111
87066c5d93 Merge pull request #2397 from gabe565/ci-skip-push-dependabot
Skip Docker push for Dependabot PRs
2023-03-25 07:22:43 +01:00
Gabe Cook
39ab2eb10f Skip Docker push for Dependabot PRs 2023-03-25 01:02:28 -05:00
vabene1111
9241638686 Merge branch 'develop' into feature/unit-conversion 2023-03-25 06:23:56 +01:00
vabene1111
048f12948d added WSL docs to docs nav 2023-03-25 06:23:48 +01:00
vabene1111
ed4a46d585 Merge pull request #2372 from nco34/patch-1
Create WSL-Docker-Installation.md
2023-03-25 05:56:16 +01:00
vabene1111
4857a853b3 Merge pull request #2351 from ocnattie/develop
added a couple of typo corrections
2023-03-25 05:54:27 +01:00
vabene1111
cfda0a17b1 Merge pull request #2326 from alexbarcelo/k8s_update
Updating/improving kubernetes deployment and its documentation
2023-03-25 05:52:13 +01:00
vabene1111
84a1c560cc Merge pull request #2270 from MarcusWolschon/features/upstream/1552_Import_Recipes_from_Cookidoo
#1552 import recipes from cookidoo
2023-03-25 05:51:18 +01:00
vabene1111
cf8e130bb8 Merge pull request #2394 from TandoorRecipes/dependabot/npm_and_yarn/vue/typescript-eslint/parser-5.56.0
Bump @typescript-eslint/parser from 5.51.0 to 5.56.0 in /vue
2023-03-25 05:50:38 +01:00
vabene1111
33070e3c51 Merge pull request #2297 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue-template-compiler-2.7.14
Bump vue-template-compiler from 2.6.14 to 2.7.14 in /vue
2023-03-25 05:50:30 +01:00
dependabot[bot]
d4a646c973 Bump @typescript-eslint/parser from 5.51.0 to 5.56.0 in /vue
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.51.0 to 5.56.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.56.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-25 04:49:53 +00:00
vabene1111
38ad546634 Merge pull request #2340 from TandoorRecipes/dependabot/pip/django-cleanup-7.0.0
Bump django-cleanup from 6.0.0 to 7.0.0
2023-03-25 05:49:32 +01:00
vabene1111
001094afa5 Merge pull request #2341 from TandoorRecipes/dependabot/pip/requests-2.28.2
Bump requests from 2.28.1 to 2.28.2
2023-03-25 05:49:26 +01:00
vabene1111
dd4e170fb0 Merge pull request #2342 from TandoorRecipes/dependabot/pip/django-webpack-loader-1.8.1
Bump django-webpack-loader from 1.8.0 to 1.8.1
2023-03-25 05:49:15 +01:00
vabene1111
10b3b6fe1e Merge pull request #2347 from TandoorRecipes/dependabot/npm_and_yarn/vue/webpack-bundle-tracker-1.8.1
Bump webpack-bundle-tracker from 1.8.0 to 1.8.1 in /vue
2023-03-25 05:49:09 +01:00
vabene1111
c15e88a3d3 Merge pull request #2395 from TandoorRecipes/dependabot/npm_and_yarn/vue/babel/eslint-parser-7.21.3
Bump @babel/eslint-parser from 7.19.1 to 7.21.3 in /vue
2023-03-25 05:48:44 +01:00
vabene1111
673ccb5024 Merge pull request #2393 from TandoorRecipes/dependabot/npm_and_yarn/vue/typescript-eslint/eslint-plugin-5.56.0
Bump @typescript-eslint/eslint-plugin from 4.33.0 to 5.56.0 in /vue
2023-03-25 05:48:35 +01:00
vabene1111
7297cb5c3f Merge pull request #2388 from TandoorRecipes/dependabot/github_actions/actions/setup-python-4
Bump actions/setup-python from 2 to 4
2023-03-25 05:48:28 +01:00
dependabot[bot]
f995c44d0b Bump django-cleanup from 6.0.0 to 7.0.0
Bumps [django-cleanup](https://github.com/un1t/django-cleanup) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/un1t/django-cleanup/releases)
- [Changelog](https://github.com/un1t/django-cleanup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un1t/django-cleanup/compare/6.0.0...7.0.0)

---
updated-dependencies:
- dependency-name: django-cleanup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 07:04:18 +00:00
dependabot[bot]
f81a7479c7 Bump @babel/eslint-parser from 7.19.1 to 7.21.3 in /vue
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.19.1 to 7.21.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.3/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 07:00:46 +00:00
dependabot[bot]
f5ab723ac2 Bump actions/setup-python from 2 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:59:24 +00:00
dependabot[bot]
2dddc79a47 Bump @typescript-eslint/eslint-plugin from 4.33.0 to 5.56.0 in /vue
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.33.0 to 5.56.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.56.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:59:04 +00:00
vabene1111
b28cd881de Merge pull request #2392 from TandoorRecipes/dependabot/npm_and_yarn/vue/core-js-3.29.1
Bump core-js from 3.27.2 to 3.29.1 in /vue
2023-03-24 07:58:51 +01:00
vabene1111
b013efadda Merge pull request #2391 from TandoorRecipes/dependabot/pip/pytest-7.2.2
Bump pytest from 7.2.0 to 7.2.2
2023-03-24 07:58:42 +01:00
vabene1111
a61566063b Merge pull request #2390 from TandoorRecipes/dependabot/github_actions/github/codeql-action-2
Bump github/codeql-action from 1 to 2
2023-03-24 07:58:26 +01:00
vabene1111
9f6ec38ac5 Merge pull request #2389 from TandoorRecipes/dependabot/pip/django-tables2-2.5.3
Bump django-tables2 from 2.4.1 to 2.5.3
2023-03-24 07:58:18 +01:00
vabene1111
5110b975e9 Merge pull request #2387 from TandoorRecipes/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 1 to 3
2023-03-24 07:58:09 +01:00
dependabot[bot]
56ad93cdb3 Bump core-js from 3.27.2 to 3.29.1 in /vue
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.27.2 to 3.29.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.29.1/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:56:02 +00:00
dependabot[bot]
fb7d1d94ab Bump pytest from 7.2.0 to 7.2.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.0 to 7.2.2.
- [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/7.2.0...7.2.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:55:06 +00:00
dependabot[bot]
78de1c2bc2 Bump github/codeql-action from 1 to 2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:54:44 +00:00
dependabot[bot]
8ad21b68ef Bump django-tables2 from 2.4.1 to 2.5.3
Bumps [django-tables2](https://github.com/jieter/django-tables2) from 2.4.1 to 2.5.3.
- [Release notes](https://github.com/jieter/django-tables2/releases)
- [Changelog](https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jieter/django-tables2/compare/v2.4.1...v2.5.3)

---
updated-dependencies:
- dependency-name: django-tables2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:54:40 +00:00
dependabot[bot]
58f7d02460 Bump actions/checkout from 1 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 1 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 06:54:35 +00:00
vabene1111
7c641af280 Merge pull request #2377 from gabe565/enable-dependabot-gha
Enable Dependabot updates for GitHub Actions
2023-03-24 07:54:12 +01:00
vabene1111
8ebb62188b Merge pull request #2376 from gabe565/refactor-ci
Refactor Docker CI: Consolidate, add cache, GHCR Repository, major/minor tags
2023-03-24 07:53:38 +01:00
Thomas Preece
a1b8f736c2 Improve parsing of OpenEats imports 2023-03-20 18:18:43 +00:00
Gabe Cook
0347ff5304 Enable Dependabot updates for GitHub Actions 2023-03-17 17:15:06 -05:00
Gabe Cook
037f38ac6b Add GHCR repository 2023-03-17 16:12:23 -05:00
Gabe Cook
5370e67444 Consolidate Docker build CI workflows 2023-03-17 14:02:45 -05:00
vabene1111
cb518a0cca many more unit conversions 2023-03-16 17:07:46 +01:00
Diogo Cardoso
87db9124d0 Translated using Weblate (Portuguese)
Currently translated at 63.5% (305 of 480 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt/
2023-03-16 11:55:48 +00:00
vabene1111
6efe4ab08d base unit conversions 2023-03-15 17:30:23 +01:00
vabene1111
8476b5e01f added Hungarian langauge to language switcher 2023-03-15 14:59:30 +01:00
vabene1111
27c5749b21 Merge branch 'develop' into feature/unit-conversion 2023-03-15 14:57:00 +01:00
nco34
f1eb553487 Create WSL-Docker-Installation.md 2023-03-15 01:25:53 -04:00
Ohaneje Natalie
99fc0d1f81 added a couple of typo corrections 2023-03-02 11:39:04 +01:00
dependabot[bot]
19fe7ce214 Bump webpack-bundle-tracker from 1.8.0 to 1.8.1 in /vue
Bumps [webpack-bundle-tracker](https://github.com/django-webpack/webpack-bundle-tracker) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/django-webpack/webpack-bundle-tracker/releases)
- [Commits](https://github.com/django-webpack/webpack-bundle-tracker/compare/1.8.0...1.8.1)

---
updated-dependencies:
- dependency-name: webpack-bundle-tracker
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 01:04:03 +00:00
dependabot[bot]
ca26588c32 Bump django-webpack-loader from 1.8.0 to 1.8.1
Bumps [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/django-webpack/django-webpack-loader/releases)
- [Changelog](https://github.com/django-webpack/django-webpack-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/django-webpack/django-webpack-loader/compare/1.8.0...1.8.1)

---
updated-dependencies:
- dependency-name: django-webpack-loader
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 00:57:46 +00:00
dependabot[bot]
e2c807e303 Bump requests from 2.28.1 to 2.28.2
Bumps [requests](https://github.com/psf/requests) from 2.28.1 to 2.28.2.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.1...v2.28.2)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 00:57:39 +00:00
vabene1111
b10be8d321 playing around with pint 2023-02-26 11:56:48 +01:00
vabene1111
8a648a5e41 unit conversion cleanups 2023-02-26 09:12:16 +01:00
vabene1111
fcf861f5eb cleaner caching function 2023-02-26 08:49:44 +01:00
vabene1111
1efcf386e2 ingredient related recipes performance 2023-02-26 08:27:20 +01:00
vabene1111
38010117e5 optimized unit conversion queries
using filter breaks prefetch related
2023-02-26 08:22:07 +01:00
vabene1111
c217bf2445 improved recipe detail API performance
by properly using prefetch related
from 600 queries in 280ms to 290 in ~100 ms
2023-02-25 23:34:35 +01:00
vabene1111
671269dca7 Merge branch 'develop' into feature/unit-conversion 2023-02-25 22:15:42 +01:00
vabene1111
2e013e7b43 add api endpoints and genereic views 2023-02-24 23:17:12 +01:00
vabene1111
ff6c8d5822 added unique constraints 2023-02-24 22:27:35 +01:00
vabene1111
a2b987352f user nutrition types + ingredient nutrtion calculation 2023-02-24 22:12:52 +01:00
vabene1111
5651beffb2 Merge branch 'develop' into feature/unit-conversion
# Conflicts:
#	vue/yarn.lock
2023-02-24 20:41:01 +01:00
Alex Barcelo
d05b894d69 Updating/improving kubernetes deployment and its documentation 2023-02-20 14:14:53 +01:00
dependabot[bot]
1cbc74761a Bump vue-template-compiler from 2.6.14 to 2.7.14 in /vue
Bumps [vue-template-compiler](https://github.com/vuejs/vue) from 2.6.14 to 2.7.14.
- [Release notes](https://github.com/vuejs/vue/releases)
- [Changelog](https://github.com/vuejs/vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue/compare/v2.6.14...v2.7.14)

---
updated-dependencies:
- dependency-name: vue-template-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-16 22:44:06 +00:00
Marcus Wolschon
bce44866c2 add translations, go back to using scraper.instructions() 2023-01-30 16:49:38 +01:00
vabene1111
29bb391bfe Merge branch 'develop' into feature/unit-conversion 2023-01-15 17:44:46 +01:00
Marcus Wolschon
f43ef3ad59 another typo 2023-01-15 13:04:29 +01:00
Marcus Wolschon
4c71c5b088 fix typo 2023-01-15 13:03:51 +01:00
Marcus Wolschon
54d0b70f01 #1552 use instructions_list() instead of instructions() and move Thermomix-Handling to clean_instruction_string() 2023-01-15 12:55:03 +01:00
Marcus Wolschon
5a0f07a6b2 handle steps 2023-01-13 22:21:31 +01:00
Marcus Wolschon
a4bf967f65 #1552 Import Recipes from Cookidoo 2023-01-13 21:33:01 +01:00
Marcus Wolschon
77feb0db3a #1552 Import Recipes from Cookidoo 2023-01-13 21:31:49 +01:00
Marcus Wolschon
33c634c0e2 #1552 Import Recipes from Cookidoo 2023-01-13 21:22:17 +01:00
Marcus Wolschon
be24e25ae4 #1552 Import Recipes from Cookidoo 2023-01-13 21:19:49 +01:00
vabene1111
3ced8c7a1e first conversions working 2023-01-09 17:42:53 +01:00
192 changed files with 32092 additions and 8543 deletions

View File

@@ -3,7 +3,6 @@ npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE

View File

@@ -2,6 +2,10 @@
# when unset: 1 (true) - dont unset this, just for development
DEBUG=0
SQL_DEBUG=0
DEBUG_TOOLBAR=0
# Gunicorn log level for debugging (default value is "info" when unset)
# (see https://docs.gunicorn.org/en/stable/settings.html#loglevel for available settings)
# GUNICORN_LOG_LEVEL="debug"
# HTTP port to bind to
# TANDOOR_PORT=8080
@@ -157,6 +161,7 @@ REVERSE_PROXY_AUTH=0
#AUTH_LDAP_BIND_PASSWORD=
#AUTH_LDAP_USER_SEARCH_BASE_DN=
#AUTH_LDAP_TLS_CACERTFILE=
#AUTH_LDAP_START_TLS=
# Enables exporting PDF (see export docs)
# Disabled by default, uncomment to enable

View File

@@ -14,3 +14,8 @@ updates:
directory: "/vue/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -0,0 +1,110 @@
name: Build Docker Container with open data plugin installed
on: push
jobs:
build-container:
name: Build ${{ matrix.name }} Container
runs-on: ubuntu-latest
if: github.repository_owner == 'TandoorRecipes'
continue-on-error: ${{ matrix.continue-on-error }}
permissions:
contents: read
packages: write
strategy:
matrix:
include:
# Standard build config
- name: Standard
dockerfile: Dockerfile
platforms: linux/amd64,linux/arm64
suffix: ""
continue-on-error: false
steps:
- uses: actions/checkout@v3
- name: Get version number
id: get_version
run: |
if [[ "$GITHUB_REF" = refs/tags/* ]]; then
echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then
echo VERSION=beta >> $GITHUB_OUTPUT
else
echo VERSION=develop >> $GITHUB_OUTPUT
fi
# clone open data plugin
- name: clone open data plugin repo
uses: actions/checkout@master
with:
repository: TandoorRecipes/open_data_plugin
ref: master
path: ./recipes/plugins/open_data_plugin
# Build Vue frontend
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: yarn
cache-dependency-path: vue/yarn.lock
- name: Install dependencies
working-directory: ./vue
run: yarn install --frozen-lockfile
- name: Build dependencies
working-directory: ./vue
run: yarn build
- name: Setup Open Data Plugin Links
working-directory: ./recipes/plugins/open_data_plugin
run: python setup_repo.py
- name: Build Open Data Frontend
working-directory: ./recipes/plugins/open_data_plugin/vue
run: yarn build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
if: github.secret_source == 'Actions'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
if: github.secret_source == 'Actions'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
vabene1111/recipes
ghcr.io/TandoorRecipes/recipes
flavor: |
latest=false
suffix=${{ matrix.suffix }}
tags: |
type=raw,value=latest,suffix=-open-data-plugin,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=semver,suffix=-open-data-plugin,pattern={{version}}
type=semver,suffix=-open-data-plugin,pattern={{major}}.{{minor}}
type=semver,suffix=-open-data-plugin,pattern={{major}}
type=ref,suffix=-open-data-plugin,event=branch
- name: Build and Push
uses: docker/build-push-action@v4
with:
context: .
file: ${{ matrix.dockerfile }}
pull: true
push: ${{ github.secret_source == 'Actions' }}
platforms: ${{ matrix.platforms }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

126
.github/workflows/build-docker.yml vendored Normal file
View File

@@ -0,0 +1,126 @@
name: Build Docker Container
on: push
jobs:
build-container:
name: Build ${{ matrix.name }} Container
runs-on: ubuntu-latest
if: github.repository_owner == 'TandoorRecipes'
continue-on-error: ${{ matrix.continue-on-error }}
permissions:
contents: read
packages: write
strategy:
matrix:
include:
# Standard build config
- name: Standard
dockerfile: Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
suffix: ""
continue-on-error: false
steps:
- uses: actions/checkout@v3
- name: Get version number
id: get_version
run: |
if [[ "$GITHUB_REF" = refs/tags/* ]]; then
echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then
echo VERSION=beta >> $GITHUB_OUTPUT
else
echo VERSION=develop >> $GITHUB_OUTPUT
fi
# Build Vue frontend
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: yarn
cache-dependency-path: vue/yarn.lock
- name: Install dependencies
working-directory: ./vue
run: yarn install --frozen-lockfile
- name: Build dependencies
working-directory: ./vue
run: yarn build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
if: github.secret_source == 'Actions'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
if: github.secret_source == 'Actions'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
vabene1111/recipes
ghcr.io/TandoorRecipes/recipes
flavor: |
latest=false
suffix=${{ matrix.suffix }}
tags: |
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
- name: Build and Push
uses: docker/build-push-action@v4
with:
context: .
file: ${{ matrix.dockerfile }}
pull: true
push: ${{ github.secret_source == 'Actions' }}
platforms: ${{ matrix.platforms }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
notify-stable:
name: Notify Stable
runs-on: ubuntu-latest
needs: build-container
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Set tag name
run: |
# Strip "refs/tags/" prefix
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
# Send stable discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
notify-beta:
name: Notify Beta
runs-on: ubuntu-latest
needs: build-container
if: github.ref == 'refs/heads/beta'
steps:
# Send beta discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 The BETA Image has been updated! 🥳'

View File

@@ -12,7 +12,7 @@ jobs:
python-version: ['3.10']
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
@@ -20,7 +20,7 @@ jobs:
# Build Vue frontend
- uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '18'
- name: Install Vue dependencies
working-directory: ./vue
run: yarn install

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -25,7 +25,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages
with:
languages: python, javascript
@@ -47,6 +47,6 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
with:
languages: javascript, python

View File

@@ -1,48 +0,0 @@
name: publish beta raspi image docker
on:
push:
branches:
- 'beta'
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = 'beta'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: beta-raspi
dockerFile: Dockerfile-raspi
platform: linux/arm/v7
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 The BETA Image has been updated! 🥳'

View File

@@ -1,47 +0,0 @@
name: publish beta image docker
on:
push:
branches:
- 'beta'
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = 'beta'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: beta
platform: linux/amd64,linux/arm64
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 The BETA Image has been updated! 🥳'

View File

@@ -1,42 +0,0 @@
name: publish dev image docker
on:
push:
branches:
- '*'
- '*/*'
- '!master'
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = 'develop'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Clear Cache
working-directory: ./vue
run: yarn cache clean --all
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@2.13
with:
name: vabene1111/recipes
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,45 +0,0 @@
name: publish latest raspi image docker
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}-raspi
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}-raspi'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
dockerFile: Dockerfile-raspi
platform: linux/arm/v7
tag: latest-raspi
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,44 +0,0 @@
name: publish latest image docker
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
platform: linux/amd64,linux/arm64
tag: latest
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,47 +0,0 @@
name: publish tagged raspi release docker
on:
release:
types: [published]
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
dockerFile: Dockerfile-raspi
platform: linux/arm/v7
tag: ${{ steps.get_version.outputs.VERSION }}-raspi
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,53 +0,0 @@
name: publish tagged release docker
on:
release:
types: [published]
jobs:
build:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
working-directory: ./vue
run: yarn install
- name: Build dependencies
working-directory: ./vue
run: yarn build
# Build container
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
platform: linux/amd64,linux/arm64
tag: ${{ steps.get_version.outputs.VERSION }}
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'

View File

@@ -9,8 +9,8 @@ jobs:
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-include-markdown-plugin

1
.gitignore vendored
View File

@@ -78,6 +78,7 @@ postgresql/
/docker-compose.override.yml
vue/node_modules
plugins
.vscode/
vetur.config.js
cookbook/static/vue

2
.idea/recipes.iml generated
View File

@@ -18,7 +18,7 @@
<excludeFolder url="file://$MODULE_DIR$/staticfiles" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.11 (recipes)" jdkType="Python SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">

View File

@@ -1,7 +1,7 @@
FROM python:3.10-alpine3.15
FROM python:3.10-alpine3.18
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
@@ -15,7 +15,11 @@ WORKDIR /opt/recipes
COPY requirements.txt ./
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev git && \
RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
@@ -26,5 +30,11 @@ RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-de
#Copy project and execute it.
COPY . ./
# collect information from git repositories
RUN /opt/recipes/venv/bin/python version.py
# delete git repositories to reduce image size
RUN find . -type d -name ".git" | xargs rm -rf
RUN chmod +x boot.sh
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@@ -1,33 +0,0 @@
# builds of cryptography for raspberry pi (or better arm v7) fail for some
FROM python:3.9-alpine3.15
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap gcompat
#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 requirements.txt ./
RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev zlib-dev jpeg-dev libwebp-dev python3-dev git && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
venv/bin/pip install wheel==0.37.1 && \
venv/bin/pip install -r requirements.txt --no-cache-dir --no-binary=Pillow && \
apk --purge del .build-deps
#Copy project and execute it.
COPY . ./
RUN chmod +x boot.sh
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@@ -4,6 +4,7 @@ source venv/bin/activate
TANDOOR_PORT="${TANDOOR_PORT:-8080}"
GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}"
GUNICORN_THREADS="${GUNICORN_THREADS:-2}"
GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"
NGINX_CONF_FILE=/opt/recipes/nginx/conf.d/Recipes.conf
display_warning() {
@@ -65,4 +66,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level INFO recipes.wsgi
exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi

View File

@@ -10,12 +10,13 @@ from treebeard.forms import movenodeform_factory
from cookbook.managers import DICTIONARY
from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField, ImportLog,
Ingredient, InviteLink, Keyword, MealPlan, MealType, NutritionInformation,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace)
from .models import (Automation, BookmarkletImport, Comment, CookLog, Food, FoodInheritField,
ImportLog, Ingredient, InviteLink, Keyword, MealPlan, MealType,
NutritionInformation, Property, PropertyType, Recipe, RecipeBook,
RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, TelegramBot,
Unit, UnitConversion, UserFile, UserPreference, UserSpace, ViewLog)
class CustomUserAdmin(UserAdmin):
@@ -38,6 +39,8 @@ def delete_space_action(modeladmin, request, queryset):
class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
search_fields = ('name', 'created_by__username')
autocomplete_fields = ('created_by',)
filter_horizontal = ('food_inherit',)
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
date_hierarchy = 'created_at'
actions = [delete_space_action]
@@ -49,6 +52,8 @@ admin.site.register(Space, SpaceAdmin)
class UserSpaceAdmin(admin.ModelAdmin):
list_display = ('user', 'space',)
search_fields = ('user__username', 'space__name',)
filter_horizontal = ('groups',)
autocomplete_fields = ('user', 'space',)
admin.site.register(UserSpace, UserSpaceAdmin)
@@ -59,6 +64,7 @@ class UserPreferenceAdmin(admin.ModelAdmin):
search_fields = ('user__username',)
list_filter = ('theme', 'nav_color', 'default_page',)
date_hierarchy = 'created_at'
filter_horizontal = ('plan_share', 'shopping_share',)
@staticmethod
def name(obj):
@@ -150,9 +156,16 @@ class KeywordAdmin(TreeAdmin):
admin.site.register(Keyword, KeywordAdmin)
@admin.action(description='Delete Steps not part of a Recipe.')
def delete_unattached_steps(modeladmin, request, queryset):
with scopes_disabled():
Step.objects.filter(recipe=None).delete()
class StepAdmin(admin.ModelAdmin):
list_display = ('name', 'order',)
search_fields = ('name',)
actions = [delete_unattached_steps]
admin.site.register(Step, StepAdmin)
@@ -201,9 +214,24 @@ class FoodAdmin(TreeAdmin):
admin.site.register(Food, FoodAdmin)
class UnitConversionAdmin(admin.ModelAdmin):
list_display = ('base_amount', 'base_unit', 'food', 'converted_amount', 'converted_unit')
search_fields = ('food__name', 'unit__name')
admin.site.register(UnitConversion, UnitConversionAdmin)
@admin.action(description='Delete Ingredients not part of a Recipe.')
def delete_unattached_ingredients(modeladmin, request, queryset):
with scopes_disabled():
Ingredient.objects.filter(step__recipe=None).delete()
class IngredientAdmin(admin.ModelAdmin):
list_display = ('food', 'amount', 'unit')
search_fields = ('food__name', 'unit__name')
actions = [delete_unattached_ingredients]
admin.site.register(Ingredient, IngredientAdmin)
@@ -286,6 +314,7 @@ admin.site.register(InviteLink, InviteLinkAdmin)
class CookLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
search_fields = ('recipe__name', 'space__name',)
admin.site.register(CookLog, CookLogAdmin)
@@ -319,6 +348,20 @@ class ShareLinkAdmin(admin.ModelAdmin):
admin.site.register(ShareLink, ShareLinkAdmin)
class PropertyTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
admin.site.register(PropertyType, PropertyTypeAdmin)
class PropertyAdmin(admin.ModelAdmin):
list_display = ('property_amount', 'property_type')
admin.site.register(Property, PropertyAdmin)
class NutritionInformationAdmin(admin.ModelAdmin):
list_display = ('id',)

View File

@@ -167,8 +167,25 @@ class ImportExportBase(forms.Form):
))
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = single_file_clean(data, initial)
return result
class ImportForm(ImportExportBase):
files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True}))
files = MultipleFileField(required=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)

View File

@@ -0,0 +1,11 @@
class CacheHelper:
space = None
BASE_UNITS_CACHE_KEY = None
PROPERTY_TYPE_CACHE_KEY = None
def __init__(self, space):
self.space = space
self.BASE_UNITS_CACHE_KEY = f'SPACE_{space.id}_BASE_UNITS'
self.PROPERTY_TYPE_CACHE_KEY = f'SPACE_{space.id}_PROPERTY_TYPES'

View File

@@ -40,7 +40,12 @@ def get_filetype(name):
# TODO also add env variable to define which images sizes should be compressed
# filetype argument can not be optional, otherwise this function will treat all images as if they were a jpeg
# Because it's no longer optional, no reason to return it
def handle_image(request, image_object, filetype):
def handle_image(request, image_object, filetype):
try:
Image.open(image_object).verify()
except Exception:
return None
if (image_object.size / 1000) > 500: # if larger than 500 kb compress
if filetype == '.jpeg' or filetype == '.jpg':
return rescale_image_jpeg(image_object)

View File

@@ -0,0 +1,214 @@
from django.db.models import Q
from cookbook.models import Unit, SupermarketCategory, Property, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion, FoodProperty
class OpenDataImporter:
request = None
data = {}
slug_id_cache = {}
update_existing = False
use_metric = True
def __init__(self, request, data, update_existing=False, use_metric=True):
self.request = request
self.data = data
self.update_existing = update_existing
self.use_metric = use_metric
def _update_slug_cache(self, object_class, datatype):
self.slug_id_cache[datatype] = dict(object_class.objects.filter(space=self.request.space, open_data_slug__isnull=False).values_list('open_data_slug', 'id', ))
def import_units(self):
datatype = 'unit'
insert_list = []
for u in list(self.data[datatype].keys()):
insert_list.append(Unit(
name=self.data[datatype][u]['name'],
plural_name=self.data[datatype][u]['plural_name'],
base_unit=self.data[datatype][u]['base_unit'] if self.data[datatype][u]['base_unit'] != '' else None,
open_data_slug=u,
space=self.request.space
))
if self.update_existing:
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
else:
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',))
def import_category(self):
datatype = 'category'
insert_list = []
for k in list(self.data[datatype].keys()):
insert_list.append(SupermarketCategory(
name=self.data[datatype][k]['name'],
open_data_slug=k,
space=self.request.space
))
return SupermarketCategory.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',))
def import_property(self):
datatype = 'property'
insert_list = []
for k in list(self.data[datatype].keys()):
insert_list.append(PropertyType(
name=self.data[datatype][k]['name'],
unit=self.data[datatype][k]['unit'],
open_data_slug=k,
space=self.request.space
))
return PropertyType.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',))
def import_supermarket(self):
datatype = 'store'
self._update_slug_cache(SupermarketCategory, 'category')
insert_list = []
for k in list(self.data[datatype].keys()):
insert_list.append(Supermarket(
name=self.data[datatype][k]['name'],
open_data_slug=k,
space=self.request.space
))
# always add open data slug if matching supermarket is found, otherwise relation might fail
supermarkets = Supermarket.objects.bulk_create(insert_list, unique_fields=('space', 'name',), update_conflicts=True, update_fields=('open_data_slug',))
self._update_slug_cache(Supermarket, 'store')
insert_list = []
for k in list(self.data[datatype].keys()):
relations = []
order = 0
for c in self.data[datatype][k]['categories']:
relations.append(
SupermarketCategoryRelation(
supermarket_id=self.slug_id_cache[datatype][k],
category_id=self.slug_id_cache['category'][c],
order=order,
)
)
order += 1
SupermarketCategoryRelation.objects.bulk_create(relations, ignore_conflicts=True, unique_fields=('supermarket', 'category',))
return supermarkets
def import_food(self):
identifier_list = []
datatype = 'food'
for k in list(self.data[datatype].keys()):
identifier_list.append(self.data[datatype][k]['name'])
identifier_list.append(self.data[datatype][k]['plural_name'])
existing_objects_flat = []
existing_objects = {}
for f in Food.objects.filter(space=self.request.space).filter(name__in=identifier_list).values_list('id', 'name', 'plural_name'):
existing_objects_flat.append(f[1])
existing_objects_flat.append(f[2])
existing_objects[f[1]] = f
existing_objects[f[2]] = f
self._update_slug_cache(Unit, 'unit')
self._update_slug_cache(PropertyType, 'property')
# pref_unit_key = 'preferred_unit_metric'
# pref_shopping_unit_key = 'preferred_packaging_unit_metric'
# if not self.use_metric:
# pref_unit_key = 'preferred_unit_imperial'
# pref_shopping_unit_key = 'preferred_packaging_unit_imperial'
insert_list = []
update_list = []
update_field_list = []
for k in list(self.data[datatype].keys()):
if not (self.data[datatype][k]['name'] in existing_objects_flat or self.data[datatype][k]['plural_name'] in existing_objects_flat):
insert_list.append({'data': {
'name': self.data[datatype][k]['name'],
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
# 'preferred_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
# 'preferred_shopping_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
'open_data_slug': k,
'space': self.request.space.id,
}})
else:
if self.data[datatype][k]['name'] in existing_objects:
existing_food_id = existing_objects[self.data[datatype][k]['name']][0]
else:
existing_food_id = existing_objects[self.data[datatype][k]['plural_name']][0]
if self.update_existing:
update_field_list = ['name', 'plural_name', 'preferred_unit_id', 'preferred_shopping_unit_id', 'supermarket_category_id', 'fdc_id', 'open_data_slug', ]
update_list.append(Food(
id=existing_food_id,
name=self.data[datatype][k]['name'],
plural_name=self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
# preferred_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
# preferred_shopping_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
supermarket_category_id=self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
fdc_id=self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
open_data_slug=k,
))
else:
update_field_list = ['open_data_slug', ]
update_list.append(Food(id=existing_food_id, open_data_slug=k, ))
Food.load_bulk(insert_list, None)
if len(update_list) > 0:
Food.objects.bulk_update(update_list, update_field_list)
self._update_slug_cache(Food, 'food')
food_property_list = []
alias_list = []
for k in list(self.data[datatype].keys()):
for fp in self.data[datatype][k]['properties']['type_values']:
food_property_list.append(Property(
property_type_id=self.slug_id_cache['property'][fp['property_type']],
property_amount=fp['property_value'],
import_food_id=self.slug_id_cache['food'][k],
space=self.request.space,
))
# for a in self.data[datatype][k]['alias']:
# alias_list.append(Automation(
# param_1=a,
# param_2=self.data[datatype][k]['name'],
# space=self.request.space,
# created_by=self.request.user,
# ))
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
property_food_relation_list = []
for p in Property.objects.filter(space=self.request.space, import_food_id__isnull=False).values_list('import_food_id', 'id', ):
property_food_relation_list.append(Food.properties.through(food_id=p[0], property_id=p[1]))
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
# Automation.objects.bulk_create(alias_list, ignore_conflicts=True, unique_fields=('space', 'param_1', 'param_2',))
return insert_list + update_list
def import_conversion(self):
datatype = 'conversion'
insert_list = []
for k in list(self.data[datatype].keys()):
insert_list.append(UnitConversion(
base_amount=self.data[datatype][k]['base_amount'],
base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
converted_amount=self.data[datatype][k]['converted_amount'],
converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
open_data_slug=k,
space=self.request.space,
created_by=self.request.user,
))
return UnitConversion.objects.bulk_create(insert_list, ignore_conflicts=True, unique_fields=('space', 'base_unit', 'converted_unit', 'food', 'open_data_slug'))

View File

@@ -322,7 +322,7 @@ class CustomRecipePermission(permissions.BasePermission):
def has_permission(self, request, view): # user is either at least a guest or a share link is given and the request is safe
share = request.query_params.get('share', None)
return has_group_permission(request.user, ['guest']) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
def has_object_permission(self, request, view, obj):
share = request.query_params.get('share', None)
@@ -332,7 +332,7 @@ class CustomRecipePermission(permissions.BasePermission):
if obj.private:
return ((obj.created_by == request.user) or (request.user in obj.shared.all())) and obj.space == request.space
else:
return has_group_permission(request.user, ['guest']) and obj.space == request.space
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) and obj.space == request.space
class CustomUserPermission(permissions.BasePermission):
@@ -434,3 +434,10 @@ def switch_user_active_space(user, space):
return us
except ObjectDoesNotExist:
return None
class IsReadOnlyDRF(permissions.BasePermission):
message = 'You cannot interact with this object as it is not owned by you!'
def has_permission(self, request, view):
return request.method in SAFE_METHODS

View File

@@ -0,0 +1,71 @@
from django.core.cache import caches
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import PropertyType, Unit, Food, Property, Recipe, Step
class FoodPropertyHelper:
space = None
def __init__(self, space):
"""
Helper to perform food property calculations
:param space: space to limit scope to
"""
self.space = space
def calculate_recipe_properties(self, recipe):
"""
Calculate all food properties for a given recipe.
:param recipe: recipe to calculate properties for
:return: dict of with property keys and total/food values for each property available
"""
ingredients = []
computed_properties = {}
for s in recipe.steps.all():
ingredients += s.ingredients.all()
property_types = caches['default'].get(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, None)
if not property_types:
property_types = PropertyType.objects.filter(space=self.space).all()
caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60) # cache is cleared on property type save signal so long duration is fine
for fpt in property_types:
computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0, 'missing_value': False}
uch = UnitConversionHelper(self.space)
for i in ingredients:
if i.food is not None:
conversions = uch.get_conversions(i)
for pt in property_types:
found_property = False
if i.food.properties_food_amount == 0 or i.food.properties_food_unit is None:
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}
computed_properties[pt.id]['missing_value'] = i.food.properties_food_unit is None
else:
for p in i.food.properties.all():
if p.property_type == pt:
for c in conversions:
if c.unit == i.food.properties_food_unit:
found_property = True
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
computed_properties[pt.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
if not found_property:
computed_properties[pt.id]['missing_value'] = True
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}
return computed_properties
# small dict helper to add to existing key or create new, probably a better way of doing this
# TODO move to central helper ?
@staticmethod
def add_or_create(d, key, value, food):
if key in d:
d[key]['value'] += value
else:
d[key] = {'id': food.id, 'food': food.name, 'value': value}
return d

View File

@@ -3,9 +3,9 @@ from collections import Counter
from datetime import date, timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
from django.core.cache import cache
from django.core.cache import caches
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value, When, FilteredRelation)
from django.core.cache import cache, caches
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
When)
from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation
from django.utils.translation import gettext as _
@@ -20,7 +20,8 @@ from recipes import settings
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
class RecipeSearch():
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
_postgres = settings.DATABASES['default']['ENGINE'] in [
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
def __init__(self, request, **params):
self._request = request
@@ -31,6 +32,9 @@ class RecipeSearch():
if custom_filter:
self._params = {**json.loads(custom_filter.search)}
self._original_params = {**(params or {})}
# json.loads casts rating as an integer, expecting string
if isinstance(self._params.get('rating', None), int):
self._params['rating'] = str(self._params['rating'])
else:
self._params = {**(params or {})}
else:
@@ -45,7 +49,8 @@ class RecipeSearch():
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
else:
self._search_prefs = SearchPreference()
self._string = self._params.get('query').strip() if self._params.get('query', None) else None
self._string = self._params.get('query').strip(
) if self._params.get('query', None) else None
self._rating = self._params.get('rating', None)
self._keywords = {
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
@@ -74,7 +79,8 @@ class RecipeSearch():
self._random = str2bool(self._params.get('random', False))
self._new = str2bool(self._params.get('new', False))
self._num_recent = int(self._params.get('num_recent', 0))
self._include_children = str2bool(self._params.get('include_children', None))
self._include_children = str2bool(
self._params.get('include_children', None))
self._timescooked = self._params.get('timescooked', None)
self._cookedon = self._params.get('cookedon', None)
self._createdon = self._params.get('createdon', None)
@@ -82,9 +88,9 @@ class RecipeSearch():
self._viewedon = self._params.get('viewedon', None)
self._makenow = self._params.get('makenow', None)
# this supports hidden feature to find recipes missing X ingredients
if type(self._makenow) == bool and self._makenow == True:
if isinstance(self._makenow, bool) and self._makenow == True:
self._makenow = 0
elif type(self._makenow) == str and self._makenow in ["yes", "true"]:
elif isinstance(self._makenow, str) and self._makenow in ["yes", "true"]:
self._makenow = 0
else:
try:
@@ -95,18 +101,24 @@ class RecipeSearch():
self._search_type = self._search_prefs.search or 'plain'
if self._string:
if self._postgres:
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
self._unaccent_include = self._search_prefs.unaccent.values_list(
'field', flat=True)
else:
self._unaccent_include = []
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._icontains_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._trigram_include = None
self._fulltext_include = None
self._trigram = False
if self._postgres and self._string:
self._language = DICTIONARY.get(translation.get_language(), 'simple')
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
self._language = DICTIONARY.get(
translation.get_language(), 'simple')
self._trigram_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list(
'field', flat=True) or None
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
self._trigram = True
@@ -141,7 +153,7 @@ class RecipeSearch():
self.unit_filters(units=self._units)
self._makenow_filter(missing=self._makenow)
self.string_filters(string=self._string)
return self._queryset.filter(space=self._request.space).distinct().order_by(*self.orderby)
return self._queryset.filter(space=self._request.space).order_by(*self.orderby)
def _sort_includes(self, *args):
for x in args:
@@ -182,8 +194,10 @@ class RecipeSearch():
# otherwise sort by the remaining order_by attributes or favorite by default
else:
order += default_order
order[:] = [Lower('name').asc() if x == 'name' else x for x in order]
order[:] = [Lower('name').desc() if x == '-name' else x for x in order]
order[:] = [Lower('name').asc() if x ==
'name' else x for x in order]
order[:] = [Lower('name').desc() if x ==
'-name' else x for x in order]
self.orderby = order
def string_filters(self, string=None):
@@ -200,21 +214,28 @@ class RecipeSearch():
for f in self._filters:
query_filter |= f
self._queryset = self._queryset.filter(query_filter).distinct() # this creates duplicate records which can screw up other aggregates, see makenow for workaround
# this creates duplicate records which can screw up other aggregates, see makenow for workaround
self._queryset = self._queryset.filter(query_filter).distinct()
if self._fulltext_include:
if self._fuzzy_match is None:
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
self._queryset = self._queryset.annotate(
score=Coalesce(Max(self.search_rank), 0.0))
else:
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
self._queryset = self._queryset.annotate(
rank=Coalesce(Max(self.search_rank), 0.0))
if self._fuzzy_match is not None:
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
simularity = self._fuzzy_match.filter(
pk=OuterRef('pk')).values('simularity')
if not self._fulltext_include:
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
self._queryset = self._queryset.annotate(
score=Coalesce(Subquery(simularity), 0.0))
else:
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
self._queryset = self._queryset.annotate(
simularity=Coalesce(Subquery(simularity), 0.0))
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
self._queryset = self._queryset.annotate(
score=F('rank') + F('simularity'))
else:
query_filter = Q()
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
@@ -223,7 +244,8 @@ class RecipeSearch():
def _cooked_on_filter(self, cooked_date=None):
if self._sort_includes('lastcooked') or cooked_date:
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
lessthan = self._sort_includes(
'-lastcooked') or '-' in (cooked_date or [])[:1]
if lessthan:
default = timezone.now() - timedelta(days=100000)
else:
@@ -233,32 +255,41 @@ class RecipeSearch():
if cooked_date is None:
return
cooked_date = date(*[int(x) for x in cooked_date.split('-') if x != ''])
cooked_date = date(*[int(x)
for x in cooked_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
self._queryset = self._queryset.filter(
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
else:
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
self._queryset = self._queryset.filter(
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
def _created_on_filter(self, created_date=None):
if created_date is None:
return
lessthan = '-' in created_date[:1]
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
created_date = date(*[int(x)
for x in created_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
self._queryset = self._queryset.filter(
created_at__date__lte=created_date)
else:
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
self._queryset = self._queryset.filter(
created_at__date__gte=created_date)
def _updated_on_filter(self, updated_date=None):
if updated_date is None:
return
lessthan = '-' in updated_date[:1]
updated_date = date(*[int(x) for x in updated_date.split('-') if x != ''])
updated_date = date(*[int(x)
for x in updated_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
self._queryset = self._queryset.filter(
updated_at__date__lte=updated_date)
else:
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
self._queryset = self._queryset.filter(
updated_at__date__gte=updated_date)
def _viewed_on_filter(self, viewed_date=None):
if self._sort_includes('lastviewed') or viewed_date:
@@ -268,12 +299,15 @@ class RecipeSearch():
if viewed_date is None:
return
lessthan = '-' in viewed_date[:1]
viewed_date = date(*[int(x) for x in viewed_date.split('-') if x != ''])
viewed_date = date(*[int(x)
for x in viewed_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
self._queryset = self._queryset.filter(
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
else:
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
self._queryset = self._queryset.filter(
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
def _new_recipes(self, new_days=7):
# TODO make new days a user-setting
@@ -293,27 +327,32 @@ class RecipeSearch():
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked:
less_than = '-' in (times_cooked or []) or not self._sort_includes('-favorite')
less_than = '-' in (times_cooked or []
) and not self._sort_includes('-favorite')
if less_than:
default = 1000
else:
default = 0
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
self._queryset = self._queryset.annotate(
favorite=Coalesce(Subquery(favorite_recipes), default))
if times_cooked is None:
return
if times_cooked == '0':
self._queryset = self._queryset.filter(favorite=0)
elif less_than:
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked[1:])).exclude(favorite=0)
self._queryset = self._queryset.filter(favorite__lte=int(
times_cooked.replace('-', ''))).exclude(favorite=0)
else:
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
self._queryset = self._queryset.filter(
favorite__gte=int(times_cooked))
def keyword_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -346,7 +385,8 @@ class RecipeSearch():
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in kw_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
def food_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -360,7 +400,8 @@ class RecipeSearch():
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
if 'or' in fd_filter:
if self._include_children:
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
f_or = Q(
steps__ingredients__food__in=Food.include_descendants(foods))
else:
f_or = Q(steps__ingredients__food__in=foods)
@@ -372,7 +413,8 @@ class RecipeSearch():
recipes = Recipe.objects.all()
for food in foods:
if self._include_children:
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
f_and = Q(
steps__ingredients__food__in=food.get_descendants_and_self())
else:
f_and = Q(steps__ingredients__food=food)
if 'not' in fd_filter:
@@ -380,7 +422,8 @@ class RecipeSearch():
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in fd_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
def unit_filters(self, units=None, operator=True):
if operator != True:
@@ -389,12 +432,14 @@ class RecipeSearch():
return
if not isinstance(units, list):
units = [units]
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
self._queryset = self._queryset.filter(
steps__ingredients__unit__in=units)
def rating_filter(self, rating=None):
if rating or self._sort_includes('rating'):
lessthan = self._sort_includes('-rating') or '-' in (rating or [])
if lessthan:
lessthan = '-' in (rating or [])
reverse = 'rating' in (self._sort_order or []) and '-rating' not in (self._sort_order or [])
if lessthan or reverse:
default = 100
else:
default = 0
@@ -434,11 +479,14 @@ class RecipeSearch():
recipes = Recipe.objects.all()
for book in kwargs[bk_filter]:
if 'not' in bk_filter:
recipes = recipes.filter(recipebookentry__book__id=book)
recipes = recipes.filter(
recipebookentry__book__id=book)
else:
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
self._queryset = self._queryset.filter(
recipebookentry__book__id=book)
if 'not' in bk_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
def step_filters(self, steps=None, operator=True):
if operator != True:
@@ -446,7 +494,7 @@ class RecipeSearch():
if not steps:
return
if not isinstance(steps, list):
steps = [unistepsts]
steps = [steps]
self._queryset = self._queryset.filter(steps__id__in=steps)
def build_fulltext_filters(self, string=None):
@@ -457,20 +505,25 @@ class RecipeSearch():
rank = []
if 'name' in self._fulltext_include:
vectors.append('name_search_vector')
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
rank.append(SearchRank('name_search_vector',
self.search_query, cover_density=True))
if 'description' in self._fulltext_include:
vectors.append('desc_search_vector')
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
rank.append(SearchRank('desc_search_vector',
self.search_query, cover_density=True))
if 'steps__instruction' in self._fulltext_include:
vectors.append('steps__search_vector')
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
rank.append(SearchRank('steps__search_vector',
self.search_query, cover_density=True))
if 'keywords__name' in self._fulltext_include:
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
vectors.append('keywords__name__unaccent')
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
rank.append(SearchRank('keywords__name__unaccent',
self.search_query, cover_density=True))
if 'steps__ingredients__food__name' in self._fulltext_include:
vectors.append('steps__ingredients__food__name__unaccent')
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
rank.append(SearchRank('steps__ingredients__food__name',
self.search_query, cover_density=True))
for r in rank:
if self.search_rank is None:
@@ -478,7 +531,8 @@ class RecipeSearch():
else:
self.search_rank += r
# modifying queryset will annotation creates duplicate results
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
self._filters.append(Q(id__in=Recipe.objects.annotate(
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
def build_text_filters(self, string=None):
if not string:
@@ -508,25 +562,32 @@ class RecipeSearch():
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
def _makenow_filter(self, missing=None):
if missing is None or (type(missing) == bool and missing == False):
if missing is None or (isinstance(missing, bool) and missing == False):
return
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
shopping_users = [
*self._request.user.get_shopping_share(), self._request.user]
onhand_filter = (
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users) # or substitute food onhand
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
# or substitute food onhand
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
)
makenow_recipes = Recipe.objects.annotate(
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
count_food=Count('steps__ingredients__food__pk', filter=Q(
steps__ingredients__food__isnull=False), distinct=True),
count_onhand=Count('steps__ingredients__food__pk',
filter=onhand_filter, distinct=True),
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
steps__ingredients__food__recipe__isnull=True), distinct=True),
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
shopping_users), then=Value(1)), default=Value(0)),
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
shopping_users), then=Value(1)), default=Value(0))
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
self._queryset = self._queryset.distinct().filter(
id__in=makenow_recipes.values('id'))
@staticmethod
def __children_substitute_filter(shopping_users=None):
@@ -547,7 +608,8 @@ class RecipeSearch():
@staticmethod
def __sibling_substitute_filter(shopping_users=None):
sibling_onhand_subquery = Food.objects.filter(
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
path__startswith=Substr(
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
depth=OuterRef('depth'),
onhand_users__in=shopping_users
)
@@ -586,7 +648,8 @@ class RecipeFacet():
self.Recent = self._cache.get('Recent', None)
if self._queryset is not None:
self._recipe_list = list(self._queryset.values_list('id', flat=True))
self._recipe_list = list(
self._queryset.values_list('id', flat=True))
self._search_params = {
'keyword_list': self._request.query_params.getlist('keywords', []),
'food_list': self._request.query_params.getlist('foods', []),
@@ -618,7 +681,8 @@ class RecipeFacet():
'Books': self.Books
}
caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
caches['default'].set(self._SEARCH_CACHE_KEY,
self._cache, self._cache_timeout)
def get_facets(self, from_cache=False):
if from_cache:
@@ -655,13 +719,16 @@ class RecipeFacet():
def get_keywords(self):
if self.Keywords is None:
if self._search_params['search_keywords_or']:
keywords = Keyword.objects.filter(space=self._request.space).distinct()
keywords = Keyword.objects.filter(
space=self._request.space).distinct()
else:
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
depth=1)).filter(space=self._request.space).distinct()
# set keywords to root objects only
keywords = self._keyword_queryset(keywords)
self.Keywords = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
self.Keywords = [{**x, 'children': None}
if x['numchild'] > 0 else x for x in list(keywords)]
self.set_cache('Keywords', self.Keywords)
return self.Keywords
@@ -669,28 +736,28 @@ class RecipeFacet():
if self.Foods is None:
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
if self._search_params['search_foods_or']:
foods = Food.objects.filter(space=self._request.space).distinct()
foods = Food.objects.filter(
space=self._request.space).distinct()
else:
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
depth=1)).filter(space=self._request.space).distinct()
# set keywords to root objects only
foods = self._food_queryset(foods)
self.Foods = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
self.Foods = [{**x, 'children': None}
if x['numchild'] > 0 else x for x in list(foods)]
self.set_cache('Foods', self.Foods)
return self.Foods
def get_books(self):
if self.Books is None:
self.Books = []
return self.Books
def get_ratings(self):
if self.Ratings is None:
if not self._request.space.demo and self._request.space.show_facet_count:
if self._queryset is None:
self._queryset = Recipe.objects.filter(id__in=self._recipe_list)
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
self._queryset = Recipe.objects.filter(
id__in=self._recipe_list)
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
self.Ratings = dict(Counter(r.rating for r in rating_qs))
else:
self.Rating = {}
@@ -715,10 +782,13 @@ class RecipeFacet():
foods = self._food_queryset(food.get_children(), food)
deep_search = self.Foods
for node in nodes:
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
index = next((i for i, x in enumerate(
deep_search) if x["id"] == node.id), None)
deep_search = deep_search[index]['children']
index = next((i for i, x in enumerate(deep_search) if x["id"] == food.id), None)
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
index = next((i for i, x in enumerate(
deep_search) if x["id"] == food.id), None)
deep_search[index]['children'] = [
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
self.set_cache('Foods', self.Foods)
return self.get_facets()
@@ -731,10 +801,13 @@ class RecipeFacet():
keywords = self._keyword_queryset(keyword.get_children(), keyword)
deep_search = self.Keywords
for node in nodes:
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
index = next((i for i, x in enumerate(
deep_search) if x["id"] == node.id), None)
deep_search = deep_search[index]['children']
index = next((i for i, x in enumerate(deep_search) if x["id"] == keyword.id), None)
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
index = next((i for i, x in enumerate(deep_search)
if x["id"] == keyword.id), None)
deep_search[index]['children'] = [
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
self.set_cache('Keywords', self.Keywords)
return self.get_facets()

View File

@@ -1,8 +1,9 @@
import random
# import random
import re
import traceback
from html import unescape
from unicodedata import decomposition
from django.core.cache import caches
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from isodate import parse_duration as iso_parse_duration
@@ -10,9 +11,12 @@ from isodate.isoerror import ISO8601Error
from pytube import YouTube
from recipe_scrapers._utils import get_host_name, get_minutes
from cookbook.helper import recipe_url_import as helper
# from cookbook.helper import recipe_url_import as helper
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Keyword, Automation
from cookbook.models import Automation, Keyword, PropertyType
# from unicodedata import decomposition
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
@@ -31,6 +35,9 @@ def get_from_scraper(scrape, request):
except Exception:
recipe_json['name'] = ''
if isinstance(recipe_json['name'], list) and len(recipe_json['name']) > 0:
recipe_json['name'] = recipe_json['name'][0]
try:
description = scrape.description() or None
except Exception:
@@ -127,7 +134,7 @@ def get_from_scraper(scrape, request):
try:
if scrape.author():
keywords.append(scrape.author())
except:
except Exception:
pass
try:
@@ -191,7 +198,14 @@ def get_from_scraper(scrape, request):
except Exception:
pass
if recipe_json['source_url']:
try:
recipe_json['properties'] = get_recipe_properties(request.space, scrape.schema.nutrients())
print(recipe_json['properties'])
except Exception:
traceback.print_exc()
pass
if 'source_url' in recipe_json and recipe_json['source_url']:
automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512]
for a in automations:
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
@@ -201,6 +215,30 @@ def get_from_scraper(scrape, request):
return recipe_json
def get_recipe_properties(space, property_data):
# {'servingSize': '1', 'calories': '302 kcal', 'proteinContent': '7,66g', 'fatContent': '11,56g', 'carbohydrateContent': '41,33g'}
properties = {
"property-calories": "calories",
"property-carbohydrates": "carbohydrateContent",
"property-proteins": "proteinContent",
"property-fats": "fatContent",
}
recipe_properties = []
for pt in PropertyType.objects.filter(space=space, open_data_slug__in=list(properties.keys())).all():
for p in list(properties.keys()):
if pt.open_data_slug == p:
if properties[p] in property_data:
recipe_properties.append({
'property_type': {
'id': pt.id,
'name': pt.name,
},
'property_amount': parse_servings(property_data[properties[p]]) / float(property_data['servingSize']),
})
return recipe_properties
def get_from_youtube_scraper(url, request):
"""A YouTube Information Scraper."""
kw, created = Keyword.objects.get_or_create(name='YouTube', space=request.space)
@@ -247,10 +285,27 @@ def parse_description(description):
def clean_instruction_string(instruction):
normalized_string = normalize_string(instruction)
# handle HTML tags that can be converted to markup
normalized_string = instruction \
.replace("<nobr>", "**") \
.replace("</nobr>", "**") \
.replace("<strong>", "**") \
.replace("</strong>", "**")
normalized_string = normalize_string(normalized_string)
normalized_string = normalized_string.replace('\n', ' \n')
normalized_string = normalized_string.replace(' \n \n', '\n\n')
return normalized_string
# handle unsupported, special UTF8 character in Thermomix-specific instructions,
# that happen in nearly every recipe on Cookidoo, Zaubertopf Club, Rezeptwelt
# and in Thermomix-specific recipes on many other sites
return normalized_string \
.replace("", _('reverse rotation')) \
.replace("", _('careful rotation')) \
.replace("", _('knead')) \
.replace("Andicken ", _('thicken')) \
.replace("Erwärmen ", _('warm up')) \
.replace("Fermentieren ", _('ferment')) \
.replace("Sous-vide ", _("sous-vide"))
def parse_instructions(instructions):
@@ -350,10 +405,28 @@ def parse_time(recipe_time):
def parse_keywords(keyword_json, space):
keywords = []
keyword_aliases = {}
# retrieve keyword automation cache if it exists, otherwise build from database
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{space.pk}'
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
keyword_aliases = c
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
keyword_aliases[a.param_1] = a.param_2
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
# keywords as list
for kw in keyword_json:
kw = normalize_string(kw)
# if alias exists use that instead
if len(kw) != 0:
if keyword_aliases:
try:
kw = keyword_aliases[kw]
except KeyError:
pass
if k := Keyword.objects.filter(name=kw, space=space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
else:

View File

@@ -0,0 +1,141 @@
from django.core.cache import caches
from decimal import Decimal
from cookbook.helper.cache_helper import CacheHelper
from cookbook.models import Ingredient, Unit
CONVERSION_TABLE = {
'weight': {
'g': 1000,
'kg': 1,
'ounce': 35.274,
'pound': 2.20462
},
'volume': {
'ml': 1000,
'l': 1,
'fluid_ounce': 33.814,
'pint': 2.11338,
'quart': 1.05669,
'gallon': 0.264172,
'tbsp': 67.628,
'tsp': 202.884,
'imperial_fluid_ounce': 35.1951,
'imperial_pint': 1.75975,
'imperial_quart': 0.879877,
'imperial_gallon': 0.219969,
'imperial_tbsp': 56.3121,
'imperial_tsp': 168.936,
},
}
BASE_UNITS_WEIGHT = list(CONVERSION_TABLE['weight'].keys())
BASE_UNITS_VOLUME = list(CONVERSION_TABLE['volume'].keys())
class ConversionException(Exception):
pass
class UnitConversionHelper:
space = None
def __init__(self, space):
"""
Initializes unit conversion helper
:param space: space to perform conversions on
"""
self.space = space
@staticmethod
def convert_from_to(from_unit, to_unit, amount):
"""
Convert from one base unit to another. Throws ConversionException if trying to convert between different systems (weight/volume) or if units are not supported.
:param from_unit: str unit to convert from
:param to_unit: str unit to convert to
:param amount: amount to convert
:return: Decimal converted amount
"""
system = None
if from_unit in BASE_UNITS_WEIGHT and to_unit in BASE_UNITS_WEIGHT:
system = 'weight'
if from_unit in BASE_UNITS_VOLUME and to_unit in BASE_UNITS_VOLUME:
system = 'volume'
if not system:
raise ConversionException('Trying to convert units not existing or not in one unit system (weight/volume)')
return Decimal(amount / Decimal(CONVERSION_TABLE[system][from_unit] / CONVERSION_TABLE[system][to_unit]))
def base_conversions(self, ingredient_list):
"""
Calculates all possible base unit conversions for each ingredient give.
Converts to all common base units IF they exist in the unit database of the space.
For useful results all ingredients passed should be of the same food, otherwise filtering afterwards might be required.
:param ingredient_list: list of ingredients to convert
:return: ingredient list with appended conversions
"""
base_conversion_ingredient_list = ingredient_list.copy()
for i in ingredient_list:
try:
conversion_unit = i.unit.name
if i.unit.base_unit:
conversion_unit = i.unit.base_unit
# TODO allow setting which units to convert to? possibly only once conversions become visible
units = caches['default'].get(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, None)
if not units:
units = Unit.objects.filter(space=self.space, base_unit__in=(BASE_UNITS_VOLUME + BASE_UNITS_WEIGHT)).all()
caches['default'].set(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, units, 60 * 60) # cache is cleared on unit save signal so long duration is fine
for u in units:
try:
ingredient = Ingredient(amount=self.convert_from_to(conversion_unit, u.base_unit, i.amount), unit=u, food=ingredient_list[0].food, )
if not any((x.unit.name == ingredient.unit.name or x.unit.base_unit == ingredient.unit.name) for x in base_conversion_ingredient_list):
base_conversion_ingredient_list.append(ingredient)
except ConversionException:
pass
except Exception:
pass
return base_conversion_ingredient_list
def get_conversions(self, ingredient):
"""
Converts an ingredient to all possible conversions based on the custom unit conversion database.
After that passes conversion to UnitConversionHelper.base_conversions() to get all base conversions possible.
:param ingredient: Ingredient object
:return: list of ingredients with all possible custom and base conversions
"""
conversions = [ingredient]
if ingredient.unit:
for c in ingredient.unit.unit_conversion_base_relation.all():
if c.space == self.space:
r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
if r and r not in conversions:
conversions.append(r)
for c in ingredient.unit.unit_conversion_converted_relation.all():
if c.space == self.space:
r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
if r and r not in conversions:
conversions.append(r)
conversions = self.base_conversions(conversions)
return conversions
def _uc_convert(self, uc, amount, unit, food):
"""
Helper to calculate values for custom unit conversions.
Converts given base values using the passed UnitConversion object into a converted Ingredient
:param uc: UnitConversion object
:param amount: base amount
:param unit: base unit
:param food: base food
:return: converted ingredient object from base amount/unit/food
"""
if uc.food is None or uc.food == food:
if unit == uc.base_unit:
return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space)
else:
return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space)

View File

@@ -1,4 +1,5 @@
import json
import traceback
from io import BytesIO, StringIO
from re import match
from zipfile import ZipFile
@@ -19,7 +20,10 @@ class Default(Integration):
recipe = self.decode_recipe(recipe_string)
images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist()))
if images:
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
try:
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
except AttributeError as e:
traceback.print_exc()
return recipe
def decode_recipe(self, string):

View File

@@ -51,9 +51,15 @@ class NextcloudCookbook(Integration):
ingredients_added = False
for s in recipe_json['recipeInstructions']:
step = Step.objects.create(
instruction=s, space=self.request.space,
)
instruction_text = ''
if 'text' in s:
step = Step.objects.create(
instruction=s['text'], name=s['name'], space=self.request.space,
)
else:
step = Step.objects.create(
instruction=s, space=self.request.space,
)
if not ingredients_added:
if len(recipe_json['description'].strip()) > 500:
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
@@ -98,11 +104,10 @@ class NextcloudCookbook(Integration):
return recipe
def formatTime(self, min):
h = min//60
h = min // 60
m = min % 60
return f'PT{h}H{m}M0S'
def get_file_from_recipe(self, recipe):
export = {}
@@ -111,7 +116,7 @@ class NextcloudCookbook(Integration):
export['url'] = recipe.source_url
export['prepTime'] = self.formatTime(recipe.working_time)
export['cookTime'] = self.formatTime(recipe.waiting_time)
export['totalTime'] = self.formatTime(recipe.working_time+recipe.waiting_time)
export['totalTime'] = self.formatTime(recipe.working_time + recipe.waiting_time)
export['recipeYield'] = recipe.servings
export['image'] = f'/Recipes/{recipe.name}/full.jpg'
export['imageUrl'] = f'/Recipes/{recipe.name}/full.jpg'
@@ -133,7 +138,6 @@ class NextcloudCookbook(Integration):
export['recipeIngredient'] = recipeIngredient
export['recipeInstructions'] = recipeInstructions
return "recipe.json", json.dumps(export)
def get_files_from_recipes(self, recipes, el, cookie):
@@ -163,7 +167,7 @@ class NextcloudCookbook(Integration):
export_zip_obj.close()
return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
return [[self.get_export_file_name(), export_zip_stream.getvalue()]]
def getJPEG(self, imageByte):
image = Image.open(BytesIO(imageByte))
@@ -172,14 +176,14 @@ class NextcloudCookbook(Integration):
bytes = BytesIO()
image.save(bytes, "JPEG")
return bytes.getvalue()
def getThumb(self, size, imageByte):
image = Image.open(BytesIO(imageByte))
w, h = image.size
m = min(w, h)
m = min(w, h)
image = image.crop(((w-m)//2, (h-m)//2, (w+m)//2, (h+m)//2))
image = image.crop(((w - m) // 2, (h - m) // 2, (w + m) // 2, (h + m) // 2))
image = image.resize([size, size], Image.Resampling.LANCZOS)
image = image.convert('RGB')

View File

@@ -2,24 +2,54 @@ import json
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Recipe, Step
from cookbook.models import Ingredient, Recipe, Step, Keyword, Comment, CookLog
from django.utils.translation import gettext as _
class OpenEats(Integration):
def get_recipe_from_file(self, file):
recipe = Recipe.objects.create(name=file['name'].strip(), created_by=self.request.user, internal=True,
description = file['info']
description_max_length = Recipe._meta.get_field('description').max_length
if len(description) > description_max_length:
description = description[0:description_max_length]
recipe = Recipe.objects.create(name=file['name'].strip(), description=description, created_by=self.request.user, internal=True,
servings=file['servings'], space=self.request.space, waiting_time=file['cook_time'], working_time=file['prep_time'])
instructions = ''
if file["info"] != '':
instructions += file["info"]
if file["directions"] != '':
instructions += file["directions"]
if file["source"] != '':
instructions += file["source"]
instructions += '\n' + _('Recipe source:') + f'[{file["source"]}]({file["source"]})'
cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
if file["cuisine"] != '':
keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
if created:
keyword.move(cuisine_keyword, pos="last-child")
recipe.keywords.add(keyword)
course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
if file["course"] != '':
keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
if created:
keyword.move(course_keyword, pos="last-child")
recipe.keywords.add(keyword)
for tag in file["tags"]:
keyword, created = Keyword.objects.get_or_create(name=tag.strip(), space=self.request.space)
recipe.keywords.add(keyword)
for comment in file['comments']:
Comment.objects.create(recipe=recipe, text=comment['text'], created_by=self.request.user)
CookLog.objects.create(recipe=recipe, rating=comment['rating'], created_by=self.request.user, space=self.request.space)
if file["photo"] != '':
recipe.image = f'recipes/openeats-import/{file["photo"]}'
recipe.save()
step = Step.objects.create(instruction=instructions, space=self.request.space,)
@@ -38,6 +68,9 @@ class OpenEats(Integration):
recipe_json = json.loads(file.read())
recipe_dict = {}
ingredient_group_dict = {}
cuisine_group_dict = {}
course_group_dict = {}
tag_group_dict = {}
for o in recipe_json:
if o['model'] == 'recipe.recipe':
@@ -50,11 +83,27 @@ class OpenEats(Integration):
'cook_time': o['fields']['cook_time'],
'servings': o['fields']['servings'],
'ingredients': [],
'photo': o['fields']['photo'],
'cuisine': o['fields']['cuisine'],
'course': o['fields']['course'],
'tags': o['fields']['tags'],
'comments': [],
}
if o['model'] == 'ingredient.ingredientgroup':
ingredient_group_dict[o['pk']] = o['fields']['recipe']
if o['model'] == 'recipe_groups.cuisine':
cuisine_group_dict[o['pk']] = o['fields']['title']
if o['model'] == 'recipe_groups.course':
course_group_dict[o['pk']] = o['fields']['title']
if o['model'] == 'recipe_groups.tag':
tag_group_dict[o['pk']] = o['fields']['title']
for o in recipe_json:
if o['model'] == 'rating.rating':
recipe_dict[o['fields']['recipe']]["comments"].append({
"text": o['fields']['comment'],
"rating": o['fields']['rating']
})
if o['model'] == 'ingredient.ingredient':
ingredient = {
'food': o['fields']['title'],
@@ -63,6 +112,15 @@ class OpenEats(Integration):
}
recipe_dict[ingredient_group_dict[o['fields']['ingredient_group']]]['ingredients'].append(ingredient)
for k, r in recipe_dict.items():
if r["cuisine"] in cuisine_group_dict:
r["cuisine"] = cuisine_group_dict[r["cuisine"]]
if r["course"] in course_group_dict:
r["course"] = course_group_dict[r["course"]]
for index in range(len(r["tags"])):
if r["tags"][index] in tag_group_dict:
r["tags"][index] = tag_group_dict[r["tags"][index]]
return list(recipe_dict.values())
def get_file_from_recipe(self, recipe):

View File

@@ -1,6 +1,7 @@
from io import BytesIO
import requests
import validators
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
@@ -67,8 +68,9 @@ class Plantoeat(Integration):
if image_url:
try:
response = requests.get(image_url)
self.import_recipe_image(recipe, BytesIO(response.content))
if validators.url(image_url, public=True):
response = requests.get(image_url)
self.import_recipe_image(recipe, BytesIO(response.content))
except Exception as e:
print('failed to import image ', str(e))

View File

@@ -5,6 +5,7 @@ import requests
import validators
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Recipe, Step
@@ -18,19 +19,21 @@ class RecipeSage(Integration):
created_by=self.request.user, internal=True,
space=self.request.space)
if file['recipeYield'] != '':
recipe.servings = parse_servings(file['recipeYield'])
recipe.servings_text = parse_servings_text(file['recipeYield'])
try:
if file['recipeYield'] != '':
recipe.servings = int(file['recipeYield'])
if 'totalTime' in file and file['totalTime'] != '':
recipe.working_time = parse_time(file['totalTime'])
if file['totalTime'] != '':
recipe.waiting_time = int(file['totalTime']) - int(file['timePrep'])
if file['prepTime'] != '':
recipe.working_time = int(file['timePrep'])
recipe.save()
if 'timePrep' in file and file['prepTime'] != '':
recipe.working_time = parse_time(file['timePrep'])
recipe.waiting_time = parse_time(file['totalTime']) - parse_time(file['timePrep'])
except Exception as e:
print('failed to parse yield or time ', str(e))
print('failed to parse time ', str(e))
recipe.save()
ingredient_parser = IngredientParser(self.request, True)
ingredients_added = False

View File

@@ -22,9 +22,12 @@ class Rezeptsuitede(Integration):
name=recipe_xml.find('head').attrib['title'].strip(),
created_by=self.request.user, internal=True, space=self.request.space)
if recipe_xml.find('head').attrib['servingtype']:
recipe.servings = parse_servings(recipe_xml.find('head').attrib['servingtype'].strip())
recipe.servings_text = parse_servings_text(recipe_xml.find('head').attrib['servingtype'].strip())
try:
if recipe_xml.find('head').attrib['servingtype']:
recipe.servings = parse_servings(recipe_xml.find('head').attrib['servingtype'].strip())
recipe.servings_text = parse_servings_text(recipe_xml.find('head').attrib['servingtype'].strip())
except KeyError:
pass
if recipe_xml.find('remark') is not None: # description is a list of <li>'s with text
if recipe_xml.find('remark').find('line') is not None:
@@ -50,7 +53,9 @@ class Rezeptsuitede(Integration):
for ingredient in recipe_xml.find('part').findall('ingredient'):
f = ingredient_parser.get_food(ingredient.attrib['item'])
u = ingredient_parser.get_unit(ingredient.attrib['unit'])
amount, unit, note = ingredient_parser.parse_amount(ingredient.attrib['qty'])
amount = 0
if ingredient.attrib['qty'].strip() != '':
amount, unit, note = ingredient_parser.parse_amount(ingredient.attrib['qty'])
ingredient_step.ingredients.add(Ingredient.objects.create(food=f, unit=u, amount=amount, space=self.request.space, ))
try:

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2022-05-10 15:32+0000\n"
"Last-Translator: zeon <zeonbg@gmail.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Bulgarian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/bg/>\n"
"Language: bg\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.10.1\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
@@ -1433,7 +1433,7 @@ msgstr ""
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
msgstr "Търсете рецепта..."
msgstr "Търсете рецепта ..."
#: .\cookbook\templates\index.html:44
msgid "New Recipe"
@@ -1818,7 +1818,7 @@ msgid ""
msgstr ""
" \n"
" Пълнотекстови търсения се опитват да нормализират предоставените "
"думи, за да съответстват на често срещани варианти. Например: 'вили, "
"думи, за да съответстват на често срещани варианти. Например: 'вили, "
"'вилица', 'вилици' всички ще се нормализират до 'вилиц'.\n"
" Има няколко налични метода, описани по-долу, които ще "
"контролират как поведението при търсене трябва да реагира, когато се търсят "

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
"PO-Revision-Date: 2023-01-08 17:55+0000\n"
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
"PO-Revision-Date: 2023-07-31 14:19+0000\n"
"Last-Translator: Mára Štěpánek <stepanekm7@gmail.com>\n"
"Language-Team: Czech <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/cs/>\n"
"Language: cs\n"
@@ -36,7 +36,7 @@ msgid ""
"try them out!"
msgstr ""
"Barva horního navigačního menu. Některé barvy neladí se všemi tématy a je "
"třeba je vyzkoušet."
"třeba je vyzkoušet!"
#: .\cookbook\forms.py:45
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
@@ -50,7 +50,7 @@ msgid ""
"to fractions automatically)"
msgstr ""
"Povolit podporu zlomků u množství ingrediencí (desetinná čísla budou "
"automaticky převedena na zlomky)."
"automaticky převedena na zlomky)"
#: .\cookbook\forms.py:47
msgid ""
@@ -553,7 +553,7 @@ msgstr "Cesta musí být v následujícím formátu"
#: .\cookbook\templates\batch\monitor.html:27
msgid "Sync Now!"
msgstr "Zahájit synchronizaci"
msgstr "Zahájit synchronizaci!"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
@@ -1036,7 +1036,7 @@ msgstr "Tento text je kurzívou"
#: .\cookbook\templates\markdown_info.html:61
#: .\cookbook\templates\markdown_info.html:77
msgid "Blockquotes are also possible"
msgstr "Lze použít i kvotace "
msgstr "Lze použít i kvotace"
#: .\cookbook\templates\markdown_info.html:84
msgid "Lists"
@@ -1106,8 +1106,8 @@ msgid ""
"rel=\"noreferrer noopener\" target=\"_blank\">this one.</a>"
msgstr ""
"Ruční vytváření tabulek pomocí značek je složité. Doporučujeme použít "
"například <a href=\"https://www.tablesgenerator.com/markdown_tables\" "
"rel=\"noreferrer noopener\" target=\"_blank\">tento tabulkový editor</a>."
"například <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
"\"noreferrer noopener\" target=\"_blank\">tento tabulkový editor.</a>"
#: .\cookbook\templates\markdown_info.html:155
#: .\cookbook\templates\markdown_info.html:157
@@ -1256,22 +1256,36 @@ msgid ""
" "
msgstr ""
"\n"
" <p>Modul jídelníčku umožňuje plánovat jídlo pomocí receptů i poznámek.</p>\n"
" <p>Jednoduše vyberte recept ze seznamu naposledy navštívených receptů, nebo ho vyhledejte\n"
" s přetáhněte na požadovaný den v rozvrhu. Můžete také přidat poznámku s popiskem\n"
" a poté přetáhnout recept pro vytvoření plánu s vlatními popisky. Vytvořením samotné poznámky\n"
" je možné přetažením pole poznámky do rozvrhu.</p>\n"
" <p>Kliknutím na recept zobrazíte detailní náhled. Odtud lze také přidat položky\n"
" do nákupního seznamu. Do nákupního seznamu můžete také přidat všechny recepty na daný den\n"
" kliknutím na ikonu nákupního košíku na horní straně tabulky.</p>\n"
" <p>V běžnémípadě se jídelníček plánuje hromadně, proto můžete v nastavení definovat\n"
" se kterými uživateli si přejete jídelníčky sdílet.\n"
" <p>Modul jídelníčku umožňuje plánovat jídlo "
"pomocí receptů i poznámek.</p>\n"
" <p>Jednoduše vyberte recept ze seznamu naposledy "
"navštívených receptů, nebo ho vyhledejte\n"
" s přetáhněte na požadovaný den v rozvrhu. "
"Můžete také přidat poznámku s popiskem\n"
" a poté přetáhnout recept pro vytvoření plánu "
"s vlatními popisky. Vytvořem samotné poznámky\n"
" je možné přetažením pole poznámky do "
"rozvrhu.</p>\n"
" <p>Kliknutím na recept zobrazíte detailní "
"náhled. Odtud lze také přidat položky\n"
" do nákupního seznamu. Do nákupního seznamu "
"můžete také přidat všechny recepty na daný den\n"
" kliknutím na ikonu nákupního košíku na horní "
"straně tabulky.</p>\n"
" <p>V běžném případě se jídelníček plánuje "
"hromadně, proto můžete v nastavení definovat\n"
" se kterými uživateli si přejete jídelníčky "
"sdílet.\n"
" </p>\n"
" <p>Můžete také upravovat typy jídel, které si přejete naplánovat. Pokud budete sdílet jídelníček \n"
" <p>Můžete také upravovat typy jídel, které si "
"přejete naplánovat. Pokud budete sdílet jídelníček \n"
" s někým, kdo\n"
" má přidána jiná jídla, jeho typy jídel se objeví i ve vašem seznamu. Pro předcházení\n"
" má přidána jiná jídla, jeho typy jídel se "
"objeví i ve vašem seznamu. Pro předcházení\n"
" duplicitám (např. Ostatní, Jiná)\n"
" pojmenujte váš typ jídla stejně, jako uživatel se kterým své seznamy sdílíte. Tím budou seznamy sloučeny.</p>\n"
" pojmenujte váš typ jídla stejně, jako "
"uživatel se kterým své seznamy sdílíte. Tím budou seznamy\n"
" sloučeny.</p>\n"
" "
#: .\cookbook\templates\meal_plan_entry.html:6
@@ -1333,12 +1347,12 @@ msgstr "Obrázek receptu"
#: .\cookbook\templates\recipes_table.html:46
#: .\cookbook\templates\url_import.html:55
msgid "Preparation time ca."
msgstr "Doba přípravy cca"
msgstr "Doba přípravy cca."
#: .\cookbook\templates\recipes_table.html:52
#: .\cookbook\templates\url_import.html:60
msgid "Waiting time ca."
msgstr "Doba čekání cca"
msgstr "Doba čekání cca."
#: .\cookbook\templates\recipes_table.html:55
msgid "External"
@@ -1386,7 +1400,7 @@ msgid ""
" in the following examples:"
msgstr ""
"Použijte tajný klíč jako autorizační hlavičku definovanou slovním klíčem, "
"jak je uvedeno v následujících příkladech."
"jak je uvedeno v následujících příkladech:"
#: .\cookbook\templates\settings.html:94
msgid "or"
@@ -1808,7 +1822,7 @@ msgstr "Import není pro tohoto poskytovatele implementován!"
#: .\cookbook\views\import_export.py:58
msgid "Exporting is not implemented for this provider"
msgstr "Eport není pro tohoto poskytovatele implementován!"
msgstr "Export není pro tohoto poskytovatele implementován!"
#: .\cookbook\views\lists.py:42
msgid "Import Log"
@@ -1840,7 +1854,7 @@ msgstr "Komentář uložen!"
#: .\cookbook\views\views.py:152
msgid "This recipe is already linked to the book!"
msgstr "Tento recept už v kuchařce existuje."
msgstr "Tento recept už v kuchařce existuje!"
#: .\cookbook\views\views.py:158
msgid "Bookmark saved!"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2023-03-06 10:55+0000\n"
"Last-Translator: Anders Obro <oebro@duck.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Danish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/da/>\n"
"Language: da\n"
@@ -1806,7 +1806,7 @@ msgid ""
msgstr ""
" \n"
" Heltekstsøgning forsøger at normalisere de givne ord så de "
"matcher stammevarianter. F.eks: 'skeen', 'skeer' og 'sket' vil alt "
"matcher stammevarianter. F.eks: 'skeen', 'skeer' og 'sket' vil alt "
"normaliseres til 'ske'.\n"
" Der er flere metoder tilgængelige, beskrevet herunder, som vil "
"bestemme hvordan søgningen skal opfører sig når flere søgeord er angivet.\n"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2021-04-11 15:23+0000\n"
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
"PO-Revision-Date: 2023-04-17 20:55+0000\n"
"Last-Translator: Espen Sellevåg <buskmenn.drammer03@icloud.com>\n"
"Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/nb_NO/>\n"
"Language: nb_NO\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.3\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91
#: .\cookbook\templates\forms\edit_internal_recipe.html:219
@@ -34,19 +34,23 @@ msgstr ""
#: .\cookbook\forms.py:46
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
msgstr ""
msgstr "Standard enhet når ny ingrediens legges til en oppskrift."
#: .\cookbook\forms.py:47
msgid ""
"Enables support for fractions in ingredient amounts (e.g. convert decimals "
"to fractions automatically)"
msgstr ""
"Aktiverer støtte for deler av ingrediensmengde (konverterer feks. desimaler "
"til deler automatisk)"
#: .\cookbook\forms.py:48
msgid ""
"Users with whom newly created meal plan/shopping list entries should be "
"shared by default."
msgstr ""
"Brukere som oppretter nye måltidsplaner/handlelister, deler disse "
"oppføringene som standard."
#: .\cookbook\forms.py:49
msgid "Show recently viewed recipes on search page."
@@ -58,7 +62,7 @@ msgstr "Antall desimaler ingredienser skal avrundes til."
#: .\cookbook\forms.py:51
msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
msgstr "Hvis du ønsker å opprette og se kommentarer under oppskrifter."
#: .\cookbook\forms.py:53
msgid ""
@@ -67,6 +71,11 @@ msgid ""
"Useful when shopping with multiple people but might use a little bit of "
"mobile data. If lower than instance limit it is reset when saving."
msgstr ""
"0 vil deaktivere automatisk synkronisering. Når en handleliste vises, "
"oppdateres listen med oppgitt antall sekunders mellomrom for å synkronisere "
"endringer fra andre brukere. Nyttig dersom flere brukere handler samtidig. "
"Datatrafikk oppstår når aktiv. Hvis verdien er lavere enn grensen, "
"tilbakestilles den ved lagring."
#: .\cookbook\forms.py:56
msgid "Makes the navbar stick to the top of the page."
@@ -100,11 +109,11 @@ msgstr ""
#: .\cookbook\forms.py:97 .\cookbook\forms.py:317
msgid "Path"
msgstr ""
msgstr "Sti"
#: .\cookbook\forms.py:98
msgid "Storage UID"
msgstr ""
msgstr "Lagring UID"
#: .\cookbook\forms.py:121
msgid "Default"
@@ -129,7 +138,6 @@ msgid "Old Unit"
msgstr "Gammel enhet"
#: .\cookbook\forms.py:156
#, fuzzy
msgid "Unit that should be replaced."
msgstr "Enhet som skal erstattes."
@@ -204,12 +212,11 @@ msgstr ""
#: .\cookbook\views\views.py:112 .\cookbook\views\views.py:116
#: .\cookbook\views\views.py:184
msgid "You do not have the required permissions to view this page!"
msgstr "Du har ikke påkrevd tilgang for å vise denne siden."
msgstr "Du har ikke påkrevd tilgang for å vise denne siden!"
#: .\cookbook\helper\permission_helper.py:141
#, fuzzy
msgid "You are not logged in and therefore cannot view this page!"
msgstr "Du er ikke innlogget og kan derfor ikke vise siden."
msgstr "Du er ikke innlogget og kan derfor ikke vise siden!"
#: .\cookbook\helper\permission_helper.py:145
#: .\cookbook\helper\permission_helper.py:167
@@ -379,7 +386,7 @@ msgstr "Finner ikke siden du leter etter."
#: .\cookbook\templates\404.html:33
msgid "Take me Home"
msgstr ""
msgstr "Tilbake til Startsiden"
#: .\cookbook\templates\404.html:35
msgid "Report a Bug"
@@ -388,12 +395,12 @@ msgstr "Rapporter en feil"
#: .\cookbook\templates\account\login.html:7
#: .\cookbook\templates\base.html:170
msgid "Login"
msgstr ""
msgstr "Logg inn"
#: .\cookbook\templates\account\login.html:13
#: .\cookbook\templates\account\login.html:28
msgid "Sign In"
msgstr ""
msgstr "Opprett bruker"
#: .\cookbook\templates\account\login.html:38
msgid "Social Login"
@@ -401,7 +408,7 @@ msgstr "Sosial innlogging"
#: .\cookbook\templates\account\login.html:39
msgid "You can use any of the following providers to sign in."
msgstr ""
msgstr "Velg en av følgende leverandører for å logge på."
#: .\cookbook\templates\account\logout.html:5
#: .\cookbook\templates\account\logout.html:9
@@ -416,20 +423,20 @@ msgstr "Er du sikker på at du vil logge ut?"
#: .\cookbook\templates\account\password_reset.html:5
#: .\cookbook\templates\account\password_reset_done.html:5
msgid "Password Reset"
msgstr ""
msgstr "Nullstill passord"
#: .\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 "Det er foreløpig ikke implementert funksjon for å nullstille passord!"
#: .\cookbook\templates\account\signup.html:5
msgid "Register"
msgstr ""
msgstr "Registrer"
#: .\cookbook\templates\account\signup.html:9
msgid "Create your Account"
msgstr "Opprett din konto"
msgstr "Opprett konto"
#: .\cookbook\templates\account\signup.html:14
msgid "Create User"
@@ -442,11 +449,11 @@ msgstr "API-dokumentasjon"
#: .\cookbook\templates\base.html:78
msgid "Utensils"
msgstr ""
msgstr "Redskaper"
#: .\cookbook\templates\base.html:88
msgid "Shopping"
msgstr ""
msgstr "Handle"
#: .\cookbook\templates\base.html:102 .\cookbook\views\delete.py:84
#: .\cookbook\views\edit.py:93 .\cookbook\views\lists.py:26
@@ -456,27 +463,27 @@ msgstr "Nøkkelord"
#: .\cookbook\templates\base.html:104
msgid "Batch Edit"
msgstr ""
msgstr "Oppdatere flere"
#: .\cookbook\templates\base.html:109
msgid "Storage Data"
msgstr ""
msgstr "Datalagring"
#: .\cookbook\templates\base.html:113
msgid "Storage Backends"
msgstr ""
msgstr "Lagringsplasser"
#: .\cookbook\templates\base.html:115
msgid "Configure Sync"
msgstr ""
msgstr "Konfigurer synkronisering"
#: .\cookbook\templates\base.html:117
msgid "Discovered Recipes"
msgstr ""
msgstr "Oppdagede oppskrifter"
#: .\cookbook\templates\base.html:119
msgid "Discovery Log"
msgstr ""
msgstr "Logg Oppdagelser"
#: .\cookbook\templates\base.html:121 .\cookbook\templates\stats.html:10
msgid "Statistics"
@@ -484,7 +491,7 @@ msgstr "Statistikk"
#: .\cookbook\templates\base.html:123
msgid "Units & Ingredients"
msgstr ""
msgstr "Enheter & Ingredienser"
#: .\cookbook\templates\base.html:125
msgid "Import Recipe"
@@ -521,58 +528,61 @@ msgid "API Browser"
msgstr "API-utforsker"
#: .\cookbook\templates\base.html:165
#, fuzzy
msgid "Logout"
msgstr "Logg ut"
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
msgstr ""
msgstr "Oppdater flere kategorier"
#: .\cookbook\templates\batch\edit.html:15
msgid "Batch edit Recipes"
msgstr ""
msgstr "Oppdater flere oppskrifter"
#: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
msgstr ""
msgstr "Legg til spesifikt nøkkelord til alle oppskrifter som inneholder et ord"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:76
msgid "Sync"
msgstr ""
msgstr "Synkronisering"
#: .\cookbook\templates\batch\monitor.html:10
msgid "Manage watched Folders"
msgstr ""
msgstr "Behandle overvåkede mapper"
#: .\cookbook\templates\batch\monitor.html:14
msgid ""
"On this Page you can manage all storage folder locations that should be "
"monitored and synced."
msgstr ""
"Her kan du behandle alle lagringsmapper og plasseringer for monitorering og "
"synkronisering."
#: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format"
msgstr ""
msgstr "Stien må være i følgende format"
#: .\cookbook\templates\batch\monitor.html:27
msgid "Sync Now!"
msgstr ""
msgstr "Synkroniser nå!"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
msgid "Importing Recipes"
msgstr ""
msgstr "Importerer oppskrifter"
#: .\cookbook\templates\batch\waiting.html:23
msgid ""
"This can take a few minutes, depending on the number of recipes in sync, "
"please wait."
msgstr ""
"Dette kan ta noen minutter, avhenging av antall oppskrifter som skal "
"synkroniseres. Vennligst vent."
#: .\cookbook\templates\books.html:5 .\cookbook\templates\books.html:11
msgid "Recipe Books"
msgstr ""
msgstr "Oppskriftsbøker"
#: .\cookbook\templates\books.html:15
msgid "New Book"
@@ -584,32 +594,32 @@ msgstr "av"
#: .\cookbook\templates\books.html:34
msgid "Toggle Recipes"
msgstr ""
msgstr "Veksle oppskrifter"
#: .\cookbook\templates\books.html:54
#: .\cookbook\templates\meal_plan_entry.html:48
#: .\cookbook\templates\recipes_table.html:64
msgid "Last cooked"
msgstr ""
msgstr "Forrige tilbereding"
#: .\cookbook\templates\books.html:71
msgid "There are no recipes in this book yet."
msgstr ""
msgstr "Det er foreløpig ingen oppskrifter i denne boken."
#: .\cookbook\templates\export.html:6 .\cookbook\templates\test2.html:6
msgid "Export Recipes"
msgstr ""
msgstr "Eksporter oppskrifter"
#: .\cookbook\templates\export.html:14 .\cookbook\templates\export.html:20
#: .\cookbook\templates\shopping_list.html:347
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
msgid "Export"
msgstr ""
msgstr "Eksporter"
#: .\cookbook\templates\forms\edit_import_recipe.html:5
#: .\cookbook\templates\forms\edit_import_recipe.html:9
msgid "Import new Recipe"
msgstr ""
msgstr "Importer ny oppskrift"
#: .\cookbook\templates\forms\edit_import_recipe.html:14
#: .\cookbook\templates\forms\edit_internal_recipe.html:389
@@ -635,29 +645,29 @@ msgstr "Beskrivelse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:72
msgid "Waiting Time"
msgstr ""
msgstr "Ventetid"
#: .\cookbook\templates\forms\edit_internal_recipe.html:78
msgid "Servings Text"
msgstr ""
msgstr "Porsjon beskrivelse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:89
msgid "Select Keywords"
msgstr ""
msgstr "Velg nøkkelord"
#: .\cookbook\templates\forms\edit_internal_recipe.html:90
#: .\cookbook\templates\url_import.html:212
msgid "Add Keyword"
msgstr ""
msgstr "Legg til nøkkelord"
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
msgid "Nutrition"
msgstr ""
msgstr "Næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:112
#: .\cookbook\templates\forms\edit_internal_recipe.html:162
msgid "Delete Step"
msgstr ""
msgstr "Fjern trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:116
msgid "Calories"
@@ -678,15 +688,15 @@ msgstr "Proteiner"
#: .\cookbook\templates\forms\edit_internal_recipe.html:146
#: .\cookbook\templates\forms\edit_internal_recipe.html:454
msgid "Step"
msgstr ""
msgstr "Trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:167
msgid "Show as header"
msgstr ""
msgstr "Vis som overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:173
msgid "Hide as header"
msgstr ""
msgstr "Skjul overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:178
msgid "Move Up"
@@ -698,15 +708,15 @@ msgstr "Flytt nedover"
#: .\cookbook\templates\forms\edit_internal_recipe.html:192
msgid "Step Name"
msgstr ""
msgstr "Trinn navn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:196
msgid "Step Type"
msgstr ""
msgstr "Trinn type"
#: .\cookbook\templates\forms\edit_internal_recipe.html:207
msgid "Step time in Minutes"
msgstr ""
msgstr "Trinn tid i minutter"
#: .\cookbook\templates\forms\edit_internal_recipe.html:261
#: .\cookbook\templates\shopping_list.html:183
@@ -740,7 +750,7 @@ msgstr "Velg mat"
#: .\cookbook\templates\meal_plan.html:256
#: .\cookbook\templates\url_import.html:171
msgid "Note"
msgstr ""
msgstr "Notis"
#: .\cookbook\templates\forms\edit_internal_recipe.html:319
msgid "Delete Ingredient"
@@ -748,7 +758,7 @@ msgstr "Slett ingrediens"
#: .\cookbook\templates\forms\edit_internal_recipe.html:325
msgid "Make Header"
msgstr ""
msgstr "Bruk som overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:331
msgid "Make Ingredient"
@@ -756,15 +766,15 @@ msgstr "Opprett ingrediens"
#: .\cookbook\templates\forms\edit_internal_recipe.html:337
msgid "Disable Amount"
msgstr ""
msgstr "Deaktiver mengde"
#: .\cookbook\templates\forms\edit_internal_recipe.html:343
msgid "Enable Amount"
msgstr ""
msgstr "Aktiver mengde"
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
msgid "Copy Template Reference"
msgstr ""
msgstr "Kopier mal-referanse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
#: .\cookbook\templates\url_import.html:196
@@ -773,29 +783,28 @@ msgstr "Instruksjoner"
#: .\cookbook\templates\forms\edit_internal_recipe.html:387
#: .\cookbook\templates\forms\edit_internal_recipe.html:418
#, fuzzy
msgid "Save & View"
msgstr "Lagre og vis"
#: .\cookbook\templates\forms\edit_internal_recipe.html:391
#: .\cookbook\templates\forms\edit_internal_recipe.html:424
msgid "Add Step"
msgstr ""
msgstr "Legg til trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:394
#: .\cookbook\templates\forms\edit_internal_recipe.html:428
msgid "Add Nutrition"
msgstr ""
msgstr "Legg til næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:396
#: .\cookbook\templates\forms\edit_internal_recipe.html:430
msgid "Remove Nutrition"
msgstr ""
msgstr "Fjern næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:398
#: .\cookbook\templates\forms\edit_internal_recipe.html:433
msgid "View Recipe"
msgstr ""
msgstr "Vis oppskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:400
#: .\cookbook\templates\forms\edit_internal_recipe.html:435
@@ -804,11 +813,11 @@ msgstr "Slett oppskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:441
msgid "Steps"
msgstr ""
msgstr "Trinn"
#: .\cookbook\templates\forms\ingredients.html:15
msgid "Edit Ingredients"
msgstr ""
msgstr "Rediger ingrediens"
#: .\cookbook\templates\forms\ingredients.html:16
msgid ""
@@ -820,54 +829,61 @@ msgid ""
"them.\n"
" "
msgstr ""
"\n"
" Følgende skjema kan brukes dersom, tilfeldigvis, to eller flere "
"enheter eller ingredienser er opprettet,\n"
" og burde være identiske.\n"
" Det slår sammen to enheter eller ingredienser og oppdaterer alle "
"oppskrifter som inneholder disse.\n"
" "
#: .\cookbook\templates\forms\ingredients.html:24
#: .\cookbook\templates\stats.html:26
msgid "Units"
msgstr ""
msgstr "Enheter"
#: .\cookbook\templates\forms\ingredients.html:26
msgid "Are you sure that you want to merge these two units?"
msgstr ""
msgstr "Er du sikker på at du vil slå sammen disse enhetene?"
#: .\cookbook\templates\forms\ingredients.html:31
#: .\cookbook\templates\forms\ingredients.html:40
msgid "Merge"
msgstr "Flett"
msgstr "Slå sammen"
#: .\cookbook\templates\forms\ingredients.html:36
msgid "Are you sure that you want to merge these two ingredients?"
msgstr ""
msgstr "Er du sikker på at du vil slå sammen disse ingrediensene?"
#: .\cookbook\templates\generic\delete_template.html:18
#, python-format
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
msgstr ""
msgstr "Er du sikker på at du vil slette %(title)s: <b>%(object)s</b> "
#: .\cookbook\templates\generic\delete_template.html:21
msgid "Confirm"
msgstr ""
msgstr "Bekreft"
#: .\cookbook\templates\generic\edit_template.html:30
msgid "View"
msgstr ""
msgstr "Vis"
#: .\cookbook\templates\generic\edit_template.html:34
msgid "Delete original file"
msgstr ""
msgstr "Slett opprinnelig fil"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:12
msgid "List"
msgstr ""
msgstr "Liste"
#: .\cookbook\templates\generic\list_template.html:25
msgid "Filter"
msgstr ""
msgstr "Filtrer"
#: .\cookbook\templates\generic\list_template.html:30
msgid "Import all"
msgstr ""
msgstr "Importer alle"
#: .\cookbook\templates\generic\new_template.html:6
#: .\cookbook\templates\generic\new_template.html:14
@@ -891,19 +907,19 @@ msgstr "Vis logg"
#: .\cookbook\templates\history.html:24
msgid "Cook Log"
msgstr ""
msgstr "Tilberedingslogg"
#: .\cookbook\templates\import.html:6 .\cookbook\templates\test.html:6
msgid "Import Recipes"
msgstr ""
msgstr "Importer oppskrifter"
#: .\cookbook\templates\include\log_cooking.html:7
msgid "Log Recipe Cooking"
msgstr ""
msgstr "Loggfør tilberedt oppskrift"
#: .\cookbook\templates\include\log_cooking.html:13
msgid "All fields are optional and can be left empty."
msgstr ""
msgstr "Alle felt er valgfri og kan stå tomme."
#: .\cookbook\templates\include\log_cooking.html:19
msgid "Rating"
@@ -943,44 +959,53 @@ msgid ""
"can be used.\n"
" "
msgstr ""
"\n"
" <b>Passord og nøkkelfeltene</b> er lagret som <b>ren tekst</b> i "
"databasen.\n"
" Dette er nødvendig for å kunne utføre API-forespørsler, men det øker "
"samtidig risiko for\n"
" uønsket tilgang til dem.<br/>\n"
" For å begrense kosekvensene av uønsket tilgang, kan nøkler eller "
"kontoer med begrenset tilgang benyttes.\n"
" "
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
msgstr ""
msgstr "Søk etter oppskrift..."
#: .\cookbook\templates\index.html:44
msgid "New Recipe"
msgstr ""
msgstr "Ny oppskrift"
#: .\cookbook\templates\index.html:47
msgid "Website Import"
msgstr ""
msgstr "Importer fra nettside"
#: .\cookbook\templates\index.html:53
msgid "Advanced Search"
msgstr ""
msgstr "Avansert søk"
#: .\cookbook\templates\index.html:57
msgid "Reset Search"
msgstr ""
msgstr "Nullstill søk"
#: .\cookbook\templates\index.html:85
msgid "Last viewed"
msgstr ""
msgstr "Sist sett"
#: .\cookbook\templates\index.html:87 .\cookbook\templates\meal_plan.html:178
#: .\cookbook\templates\stats.html:22
msgid "Recipes"
msgstr ""
msgstr "Oppskrifter"
#: .\cookbook\templates\index.html:94
msgid "Log in to view recipes"
msgstr ""
msgstr "Logg inn for å se oppskrifter"
#: .\cookbook\templates\markdown_info.html:5
#: .\cookbook\templates\markdown_info.html:13
msgid "Markdown Info"
msgstr ""
msgstr "Markdown informasjon"
#: .\cookbook\templates\markdown_info.html:14
msgid ""
@@ -997,43 +1022,56 @@ msgid ""
"below.\n"
" "
msgstr ""
"\n"
" Markdown er et lettvekts markup språk som benyttes for å formatere "
"ren tekst.\n"
" Denne siden bruker biblioteket <a href=\"https://python-markdown."
"github.io/\" target=\"_blank\">Python Markdown</a> for\n"
" å konvertere teksten din til velformatert HTML. Fullstendig "
"dokumentasjon for markdown finner du\n"
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
"target=\"_blank\">her</a>.\n"
" En ufullstendig, men sannsynligvis tilstrekkelig dokumentasjon "
"finner du under her.\n"
" "
#: .\cookbook\templates\markdown_info.html:25
msgid "Headers"
msgstr ""
msgstr "Overskrifter"
#: .\cookbook\templates\markdown_info.html:54
msgid "Formatting"
msgstr ""
msgstr "Formatering"
#: .\cookbook\templates\markdown_info.html:56
#: .\cookbook\templates\markdown_info.html:72
msgid "Line breaks are inserted by adding two spaces after the end of a line"
msgstr ""
"Linjeskift er satt inn ved å sette inn to mellomrom på slutten av en linje"
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
msgid "or by leaving a blank line inbetween."
msgstr ""
msgstr "eller ved å sette inn en tom linje mellom."
#: .\cookbook\templates\markdown_info.html:59
#: .\cookbook\templates\markdown_info.html:74
msgid "This text is bold"
msgstr ""
msgstr "Denne teksten er Fet"
#: .\cookbook\templates\markdown_info.html:60
#: .\cookbook\templates\markdown_info.html:75
msgid "This text is italic"
msgstr ""
msgstr "Denne teksten er Kursiv"
#: .\cookbook\templates\markdown_info.html:61
#: .\cookbook\templates\markdown_info.html:77
msgid "Blockquotes are also possible"
msgstr ""
msgstr "Det er også mulig å sitere avsnitt"
#: .\cookbook\templates\markdown_info.html:84
msgid "Lists"
msgstr ""
msgstr "Lister"
#: .\cookbook\templates\markdown_info.html:85
msgid ""
@@ -1264,7 +1302,7 @@ msgstr ""
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12
msgid "No Permissions"
msgstr "Ingen tilganger."
msgstr "Ingen tilgang"
#: .\cookbook\templates\no_groups_info.html:17
msgid "You do not have any groups and therefor cannot use this application."
@@ -1298,12 +1336,11 @@ msgstr ""
#: .\cookbook\templates\offline.html:6
msgid "Offline"
msgstr "Frakoblet."
msgstr "Frakoblet"
#: .\cookbook\templates\offline.html:19
#, fuzzy
msgid "You are currently offline!"
msgstr "Du er ikke tilkoblet Internett."
msgstr "Du er ikke tilkoblet!"
#: .\cookbook\templates\offline.html:20
msgid ""
@@ -1366,7 +1403,7 @@ msgstr "Stil"
#: .\cookbook\templates\settings.html:79
msgid "API Token"
msgstr "API-symbol"
msgstr "API nøkkel"
#: .\cookbook\templates\settings.html:80
msgid ""
@@ -1389,9 +1426,8 @@ msgid "Cookbook Setup"
msgstr "Kokeboksoppsett"
#: .\cookbook\templates\setup.html:14
#, fuzzy
msgid "Setup"
msgstr "Sett opp"
msgstr "Installering"
#: .\cookbook\templates\setup.html:15
msgid ""
@@ -1424,11 +1460,11 @@ msgstr "Mengde"
#: .\cookbook\templates\shopping_list.html:226
msgid "Supermarket"
msgstr "Matbutikk"
msgstr "Butikk"
#: .\cookbook\templates\shopping_list.html:236
msgid "Select Supermarket"
msgstr "Velg matbutikk"
msgstr "Velg butikk"
#: .\cookbook\templates\shopping_list.html:260
msgid "Select User"
@@ -1540,7 +1576,6 @@ msgstr ""
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
#, fuzzy
msgid "Ok"
msgstr "OK"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-11 08:52+0100\n"
"PO-Revision-Date: 2023-02-18 10:55+0000\n"
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/pt_BR/>\n"
"Language: pt_BR\n"
@@ -2208,7 +2208,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:38
msgid "URL"
msgstr ""
msgstr "URL"
#: .\cookbook\templates\url_import.html:40
msgid "App"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,17 +8,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
"PO-Revision-Date: 2022-11-30 19:09+0000\n"
"Last-Translator: Alex <kovsharoff@gmail.com>\n"
"PO-Revision-Date: 2023-05-01 07:55+0000\n"
"Last-Translator: axeron2036 <admin@axeron2036.ru>\n"
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/ru/>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.14.1\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34
@@ -286,7 +286,7 @@ msgstr ""
#: .\cookbook\forms.py:497
msgid "Search Method"
msgstr ""
msgstr "Способ поиска"
#: .\cookbook\forms.py:498
msgid "Fuzzy Lookups"
@@ -861,7 +861,7 @@ msgstr ""
#: .\cookbook\templates\base.html:220
msgid "GitHub"
msgstr ""
msgstr "GitHub"
#: .\cookbook\templates\base.html:224
msgid "API Browser"
@@ -1937,7 +1937,7 @@ msgstr ""
#: .\cookbook\templates\space.html:106
msgid "user"
msgstr ""
msgstr "пользователь"
#: .\cookbook\templates\space.html:107
msgid "guest"

View File

@@ -8,17 +8,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
"PO-Revision-Date: 2022-02-02 15:31+0000\n"
"Last-Translator: Mario Dvorsek <mario.dvorsek@gmail.com>\n"
"PO-Revision-Date: 2023-08-13 08:19+0000\n"
"Last-Translator: Miha Perpar <miha.perpar2@gmail.com>\n"
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sl/>\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
"%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 4.10.1\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || "
"n%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34
@@ -964,7 +964,7 @@ msgstr ""
#: .\cookbook\templates\base.html:275
msgid "GitHub"
msgstr ""
msgstr "GitHub"
#: .\cookbook\templates\base.html:277
msgid "Translate Tandoor"
@@ -1961,7 +1961,7 @@ msgstr ""
#: .\cookbook\templates\space.html:106
msgid "user"
msgstr ""
msgstr "uporabnik"
#: .\cookbook\templates\space.html:107
msgid "guest"
@@ -2107,7 +2107,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:36
msgid "URL"
msgstr ""
msgstr "URL"
#: .\cookbook\templates\url_import.html:38
msgid "App"

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2023-02-09 13:55+0000\n"
"Last-Translator: vertilo <vertilo.dev@gmail.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/uk/>\n"
"Language: uk\n"
@@ -1091,7 +1091,7 @@ msgstr ""
#: .\cookbook\templates\base.html:311
msgid "GitHub"
msgstr ""
msgstr "GitHub"
#: .\cookbook\templates\base.html:313
msgid "Translate Tandoor"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
# Generated by Django 4.1.9 on 2023-05-25 13:05
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_prometheus.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0188_space_no_sharing_limit'),
]
operations = [
migrations.CreateModel(
name='Property',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('property_amount', models.DecimalField(decimal_places=4, default=0, max_digits=32)),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
migrations.CreateModel(
name='PropertyType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('unit', models.CharField(blank=True, max_length=64, null=True)),
('icon', models.CharField(blank=True, max_length=16, null=True)),
('description', models.CharField(blank=True, max_length=512, null=True)),
('category', models.CharField(blank=True, choices=[('NUTRITION', 'Nutrition'), ('ALLERGEN', 'Allergen'), ('PRICE', 'Price'), ('GOAL', 'Goal'), ('OTHER', 'Other')], max_length=64, null=True)),
('open_data_slug', models.CharField(blank=True, default=None, max_length=128, null=True)),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
migrations.CreateModel(
name='UnitConversion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('base_amount', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
('converted_amount', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('open_data_slug', models.CharField(blank=True, default=None, max_length=128, null=True)),
],
bases=(django_prometheus.models.ExportModelOperationsMixin('unit_conversion'), models.Model, cookbook.models.PermissionModelMixin),
),
migrations.AddField(
model_name='food',
name='fdc_id',
field=models.CharField(blank=True, default=None, max_length=128, null=True),
),
migrations.AddField(
model_name='food',
name='open_data_slug',
field=models.CharField(blank=True, default=None, max_length=128, null=True),
),
migrations.AddField(
model_name='food',
name='preferred_shopping_unit',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preferred_shopping_unit', to='cookbook.unit'),
),
migrations.AddField(
model_name='food',
name='preferred_unit',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preferred_unit', to='cookbook.unit'),
),
migrations.AddField(
model_name='food',
name='properties_food_amount',
field=models.IntegerField(blank=True, default=100),
),
migrations.AddField(
model_name='food',
name='properties_food_unit',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.unit'),
),
migrations.AddField(
model_name='supermarket',
name='open_data_slug',
field=models.CharField(blank=True, default=None, max_length=128, null=True),
),
migrations.AddField(
model_name='supermarketcategory',
name='open_data_slug',
field=models.CharField(blank=True, default=None, max_length=128, null=True),
),
migrations.AddField(
model_name='unit',
name='base_unit',
field=models.TextField(blank=True, default=None, max_length=256, null=True),
),
migrations.AddField(
model_name='unit',
name='open_data_slug',
field=models.CharField(blank=True, default=None, max_length=128, null=True),
),
migrations.AddConstraint(
model_name='supermarketcategoryrelation',
constraint=models.UniqueConstraint(fields=('supermarket', 'category'), name='unique_sm_category_relation'),
),
migrations.AddField(
model_name='unitconversion',
name='base_unit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_base_relation', to='cookbook.unit'),
),
migrations.AddField(
model_name='unitconversion',
name='converted_unit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_converted_relation', to='cookbook.unit'),
),
migrations.AddField(
model_name='unitconversion',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='unitconversion',
name='food',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.food'),
),
migrations.AddField(
model_name='unitconversion',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AddField(
model_name='propertytype',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AddField(
model_name='property',
name='property_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.propertytype'),
),
migrations.AddField(
model_name='property',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AddField(
model_name='food',
name='properties',
field=models.ManyToManyField(blank=True, to='cookbook.property'),
),
migrations.AddField(
model_name='recipe',
name='properties',
field=models.ManyToManyField(blank=True, to='cookbook.property'),
),
migrations.AddConstraint(
model_name='unitconversion',
constraint=models.UniqueConstraint(fields=('space', 'base_unit', 'converted_unit', 'food'), name='f_unique_conversion_per_space'),
),
migrations.AddConstraint(
model_name='propertytype',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='property_type_unique_name_per_space'),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 4.1.9 on 2023-05-25 13:06
from django.db import migrations
from django_scopes import scopes_disabled
from gettext import gettext as _
def migrate_old_nutrition_data(apps, schema_editor):
print('Transforming nutrition information, this might take a while on large databases')
with scopes_disabled():
PropertyType = apps.get_model('cookbook', 'PropertyType')
RecipeProperty = apps.get_model('cookbook', 'Property')
Recipe = apps.get_model('cookbook', 'Recipe')
Space = apps.get_model('cookbook', 'Space')
# TODO respect space
for s in Space.objects.all():
property_fat = PropertyType.objects.get_or_create(name=_('Fat'), unit=_('g'), space=s, )[0]
property_carbohydrates = PropertyType.objects.get_or_create(name=_('Carbohydrates'), unit=_('g'), space=s, )[0]
property_proteins = PropertyType.objects.get_or_create(name=_('Proteins'), unit=_('g'), space=s, )[0]
property_calories = PropertyType.objects.get_or_create(name=_('Calories'), unit=_('kcal'), space=s, )[0]
for r in Recipe.objects.filter(nutrition__isnull=False, space=s).all():
rp_fat = RecipeProperty.objects.create(property_type=property_fat, property_amount=r.nutrition.fats, space=s)
rp_carbohydrates = RecipeProperty.objects.create(property_type=property_carbohydrates, property_amount=r.nutrition.carbohydrates, space=s)
rp_proteins = RecipeProperty.objects.create(property_type=property_proteins, property_amount=r.nutrition.proteins, space=s)
rp_calories = RecipeProperty.objects.create(property_type=property_calories, property_amount=r.nutrition.calories, space=s)
r.properties.add(rp_fat, rp_carbohydrates, rp_proteins, rp_calories)
r.nutrition = None
r.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0189_property_propertytype_unitconversion_food_fdc_id_and_more'),
]
operations = [
migrations.RunPython(migrate_old_nutrition_data)
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 4.1.9 on 2023-06-20 13:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0190_auto_20230525_1506'),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.RunSQL(
sql="ALTER TABLE cookbook_food_properties RENAME TO cookbook_foodproperty",
reverse_sql="ALTER TABLE cookbook_foodproperty RENAME TO cookbook_food_properties",
),
],
state_operations=[
migrations.CreateModel(
name='FoodProperty',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('food', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.food')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.property')),
],
),
migrations.AlterField(
model_name='food',
name='properties',
field=models.ManyToManyField(blank=True, through='cookbook.FoodProperty', to='cookbook.property'),
),
]
),
migrations.AddConstraint(
model_name='foodproperty',
constraint=models.UniqueConstraint(fields=('food', 'property'), name='property_unique_food'),
),
migrations.AddField(
model_name='property',
name='import_food_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddConstraint(
model_name='property',
constraint=models.UniqueConstraint(fields=('space', 'property_type', 'import_food_id'), name='property_unique_import_food_per_space'),
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 4.1.9 on 2023-06-20 13:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0191_foodproperty_property_import_food_id_and_more'),
]
operations = [
migrations.AddConstraint(
model_name='food',
constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='food_unique_open_data_slug_per_space'),
),
migrations.AddConstraint(
model_name='propertytype',
constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='property_type_unique_open_data_slug_per_space'),
),
migrations.AddConstraint(
model_name='supermarket',
constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='supermarket_unique_open_data_slug_per_space'),
),
migrations.AddConstraint(
model_name='supermarketcategory',
constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='supermarket_category_unique_open_data_slug_per_space'),
),
migrations.AddConstraint(
model_name='unit',
constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='unit_unique_open_data_slug_per_space'),
),
migrations.AddConstraint(
model_name='unitconversion',
constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='unit_conversion_unique_open_data_slug_per_space'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.9 on 2023-06-21 13:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0192_food_food_unique_open_data_slug_per_space_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='internal_note',
field=models.TextField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.9 on 2023-06-26 13:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0193_space_internal_note'),
]
operations = [
migrations.AlterField(
model_name='food',
name='properties_food_amount',
field=models.DecimalField(blank=True, decimal_places=2, default=100, max_digits=16),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1.9 on 2023-06-30 20:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0194_alter_food_properties_food_amount'),
]
operations = [
migrations.AddField(
model_name='invitelink',
name='internal_note',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='userspace',
name='internal_note',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='userspace',
name='invite_link',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.invitelink'),
),
migrations.AlterField(
model_name='userpreference',
name='theme',
field=models.CharField(choices=[('TANDOOR', 'Tandoor'), ('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero'), ('TANDOOR_DARK', 'Tandoor Dark (INCOMPLETE)')], default='TANDOOR', max_length=128),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2023-07-22 06:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0195_invitelink_internal_note_userspace_internal_note_and_more'),
]
operations = [
migrations.AddField(
model_name='food',
name='url',
field=models.CharField(blank=True, default='', max_length=1024, null=True),
),
]

View File

@@ -82,31 +82,34 @@ class TreeManager(MP_NodeManager):
# model.Manager get_or_create() is not compatible with MP_Tree
def get_or_create(self, *args, **kwargs):
kwargs['name'] = kwargs['name'].strip()
if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
return obj, False
if hasattr(self, 'space'):
if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
return obj, False
else:
with scopes_disabled():
try:
defaults = kwargs.pop('defaults', None)
if defaults:
kwargs = {**kwargs, **defaults}
# ManyToMany fields can't be set this way, so pop them out to save for later
fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
obj = self.model.add_root(**kwargs)
for field in many_to_many:
field_model = getattr(obj, field).model
for related_obj in many_to_many[field]:
if isinstance(related_obj, User):
getattr(obj, field).add(field_model.objects.get(id=related_obj.id))
else:
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
return obj, True
except IntegrityError as e:
if 'Key (path)' in e.args[0]:
self.model.fix_tree(fix_paths=True)
return self.model.add_root(**kwargs), True
if obj := self.filter(name__iexact=kwargs['name']).first():
return obj, False
with scopes_disabled():
try:
defaults = kwargs.pop('defaults', None)
if defaults:
kwargs = {**kwargs, **defaults}
# ManyToMany fields can't be set this way, so pop them out to save for later
fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
obj = self.model.add_root(**kwargs)
for field in many_to_many:
field_model = getattr(obj, field).model
for related_obj in many_to_many[field]:
if isinstance(related_obj, User):
getattr(obj, field).add(field_model.objects.get(id=related_obj.id))
else:
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
return obj, True
except IntegrityError as e:
if 'Key (path)' in e.args[0]:
self.model.fix_tree(fix_paths=True)
return self.model.add_root(**kwargs), True
class TreeModel(MP_Node):
@@ -267,6 +270,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False)
internal_note = models.TextField(blank=True, null=True)
def safe_delete(self):
"""
Safely deletes a space by deleting all objects belonging to the space first and then deleting the space itself
@@ -326,6 +331,7 @@ class UserPreference(models.Model, PermissionModelMixin):
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
TANDOOR = 'TANDOOR'
TANDOOR_DARK = 'TANDOOR_DARK'
THEMES = (
(TANDOOR, 'Tandoor'),
@@ -333,6 +339,7 @@ class UserPreference(models.Model, PermissionModelMixin):
(DARKLY, 'Darkly'),
(FLATLY, 'Flatly'),
(SUPERHERO, 'Superhero'),
(TANDOOR_DARK, 'Tandoor Dark (INCOMPLETE)'),
)
# Nav colors
@@ -408,6 +415,9 @@ class UserSpace(models.Model, PermissionModelMixin):
# that having more than one active space should just break certain parts of the application and not leak any data
active = models.BooleanField(default=False)
invite_link = models.ForeignKey("InviteLink", on_delete=models.PROTECT, null=True, blank=True)
internal_note = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -454,6 +464,7 @@ class Sync(models.Model, PermissionModelMixin):
class SupermarketCategory(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -463,7 +474,8 @@ class SupermarketCategory(models.Model, PermissionModelMixin):
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space')
models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_category_unique_open_data_slug_per_space')
]
@@ -471,6 +483,7 @@ 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')
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -480,7 +493,8 @@ class Supermarket(models.Model, PermissionModelMixin):
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space')
models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_unique_open_data_slug_per_space')
]
@@ -496,6 +510,9 @@ class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
return 'supermarket', 'space'
class Meta:
constraints = [
models.UniqueConstraint(fields=['supermarket', 'category'], name='unique_sm_category_relation')
]
ordering = ('order',)
@@ -534,6 +551,8 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
description = models.TextField(blank=True, null=True)
base_unit = models.TextField(max_length=256, null=True, blank=True, default=None)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -543,7 +562,8 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space')
models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_unique_open_data_slug_per_space')
]
@@ -559,6 +579,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
url = models.CharField(max_length=1024, blank=True, null=True, default='')
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
ignore_shopping = models.BooleanField(default=False) # inherited field
onhand_users = models.ManyToManyField(User, blank=True)
@@ -569,6 +590,15 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
substitute_children = models.BooleanField(default=False)
child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit')
properties = models.ManyToManyField("Property", blank=True, through='FoodProperty')
properties_food_amount = models.DecimalField(default=100, max_digits=16, decimal_places=2, blank=True)
properties_food_unit = models.ForeignKey(Unit, on_delete=models.PROTECT, blank=True, null=True)
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit')
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
@@ -642,7 +672,8 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='food_unique_open_data_slug_per_space')
]
indexes = (
Index(fields=['id']),
@@ -650,6 +681,32 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
)
class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model, PermissionModelMixin):
base_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
base_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_base_relation')
converted_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
converted_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_converted_relation')
food = models.ForeignKey('Food', on_delete=models.CASCADE, null=True, blank=True)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.base_amount} {self.base_unit} -> {self.converted_amount} {self.converted_unit} {self.food}'
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'base_unit', 'converted_unit', 'food'], name='f_unique_conversion_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_conversion_unique_open_data_slug_per_space')
]
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
# delete method on Food and Unit checks if they are part of a Recipe, if it is raises a ProtectedError instead of cascading the delete
food = models.ForeignKey(Food, on_delete=models.CASCADE, null=True, blank=True)
@@ -663,30 +720,9 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
order = models.IntegerField(default=0)
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
food = ""
unit = ""
if self.always_use_plural_food and self.food.plural_name not in (None, "") and not self.no_amount:
food = self.food.plural_name
else:
if self.amount > 1 and self.food.plural_name not in (None, "") and not self.no_amount:
food = self.food.plural_name
else:
food = str(self.food)
if self.always_use_plural_unit and self.unit.plural_name not in (None, "") and not self.no_amount:
unit = self.unit.plural_name
else:
if self.amount > 1 and self.unit is not None and self.unit.plural_name not in (None, "") and not self.no_amount:
unit = self.unit.plural_name
else:
unit = str(self.unit)
return str(self.amount) + ' ' + str(unit) + ' ' + str(food)
class Meta:
ordering = ['order', 'pk']
indexes = (
@@ -720,6 +756,64 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
indexes = (GinIndex(fields=["search_vector"]),)
class PropertyType(models.Model, PermissionModelMixin):
NUTRITION = 'NUTRITION'
ALLERGEN = 'ALLERGEN'
PRICE = 'PRICE'
GOAL = 'GOAL'
OTHER = 'OTHER'
name = models.CharField(max_length=128)
unit = models.CharField(max_length=64, blank=True, null=True)
icon = models.CharField(max_length=16, blank=True, null=True)
description = models.CharField(max_length=512, blank=True, null=True)
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')), (PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
# TODO show if empty property?
# TODO formatting property?
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.name}'
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='property_type_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='property_type_unique_open_data_slug_per_space')
]
class Property(models.Model, PermissionModelMixin):
property_amount = models.DecimalField(default=0, decimal_places=4, max_digits=32)
property_type = models.ForeignKey(PropertyType, on_delete=models.PROTECT)
import_food_id = models.IntegerField(null=True, blank=True) # field to hold food id when importing properties from the open data project
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.property_amount} {self.property_type.unit} {self.property_type.name}'
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'property_type', 'import_food_id'], name='property_unique_import_food_per_space')
]
class FoodProperty(models.Model):
food = models.ForeignKey(Food, on_delete=models.CASCADE)
property = models.ForeignKey(Property, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['food', 'property'], name='property_unique_food')
]
class NutritionInformation(models.Model, PermissionModelMixin):
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
carbohydrates = models.DecimalField(
@@ -736,14 +830,6 @@ class NutritionInformation(models.Model, PermissionModelMixin):
return f'Nutrition {self.pk}'
# class NutritionType(models.Model, PermissionModelMixin):
# name = models.CharField(max_length=128)
# icon = models.CharField(max_length=16, blank=True, null=True)
# description = models.CharField(max_length=512, blank=True, null=True)
#
# space = models.ForeignKey(Space, on_delete=models.CASCADE)
# objects = ScopedManager(space='space')
class RecipeManager(models.Manager.from_queryset(models.QuerySet)):
def get_queryset(self):
return super(RecipeManager, self).get_queryset().annotate(rating=Avg('cooklog__rating')).annotate(last_cooked=Max('cooklog__created_at'))
@@ -766,6 +852,7 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
waiting_time = models.IntegerField(default=0)
internal = models.BooleanField(default=False)
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
properties = models.ManyToManyField(Property, blank=True)
show_ingredient_overview = models.BooleanField(default=True)
private = models.BooleanField(default=False)
shared = models.ManyToManyField(User, blank=True, related_name='recipe_shared_with')
@@ -1042,6 +1129,8 @@ class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, Permis
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
internal_note = models.TextField(blank=True, null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')

View File

@@ -7,6 +7,7 @@ from html import escape
from smtplib import SMTPException
from django.contrib.auth.models import Group, User, AnonymousUser
from django.core.cache import caches
from django.core.mail import send_mail
from django.db.models import Avg, Q, QuerySet, Sum
from django.http import BadHeaderError
@@ -21,15 +22,18 @@ from rest_framework.exceptions import NotFound, ValidationError
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.permission_helper import above_space_limit
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog)
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
PropertyType, Property)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL
@@ -74,6 +78,19 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
return path
class OpenDataModelMixin(serializers.ModelSerializer):
def create(self, validated_data):
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
validated_data['open_data_slug'] = None
return super().create(validated_data)
def update(self, instance, validated_data):
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
validated_data['open_data_slug'] = None
return super().update(instance, validated_data)
class CustomDecimalField(serializers.Field):
"""
Custom decimal field to normalize useless decimal places
@@ -92,7 +109,7 @@ class CustomDecimalField(serializers.Field):
if data == '':
return 0
try:
return float(data.replace(',', ''))
return float(data.replace(',', '.'))
except ValueError:
raise ValidationError('A valid number is required')
@@ -102,15 +119,21 @@ class CustomOnHandField(serializers.Field):
return instance
def to_representation(self, obj):
shared_users = None
if request := self.context.get('request', None):
shared_users = getattr(request, '_shared_users', None)
if shared_users is None:
if not self.context["request"].user.is_authenticated:
return []
shared_users = []
if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
shared_users = c
else:
try:
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
self.context['request'].user.id]
caches['default'].set(
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
shared_users, timeout=5 * 60)
# TODO ugly hack that improves API performance significantly, should be done properly
except AttributeError: # Anonymous users (using share links) don't have shared users
shared_users = []
pass
return obj.onhand_users.filter(id__in=shared_users).exists()
def to_internal_value(self, data):
@@ -276,10 +299,13 @@ class SpaceSerializer(WritableNestedModelSerializer):
class Meta:
model = Space
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'use_plural',)
read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'use_plural',)
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo',)
class UserSpaceSerializer(WritableNestedModelSerializer):
@@ -296,8 +322,8 @@ class UserSpaceSerializer(WritableNestedModelSerializer):
class Meta:
model = UserSpace
fields = ('id', 'user', 'space', 'groups', 'active', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
class SpacedModelSerializer(serializers.ModelSerializer):
@@ -427,7 +453,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin):
recipe_filter = 'steps__ingredients__unit'
def create(self, validated_data):
@@ -440,7 +466,8 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
return unit
space = validated_data.pop('space', self.context['request'].space)
obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data)
obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space,
defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -451,11 +478,11 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
class Meta:
model = Unit
fields = ('id', 'name', 'plural_name', 'description', 'numrecipe', 'image')
fields = ('id', 'name', 'plural_name', 'description', 'base_unit', 'numrecipe', 'image', 'open_data_slug')
read_only_fields = ('id', 'numrecipe', 'image')
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin):
def create(self, validated_data):
name = validated_data.pop('name').strip()
@@ -479,12 +506,41 @@ class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
fields = ('id', 'category', 'supermarket', 'order')
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin):
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
class Meta:
model = Supermarket
fields = ('id', 'name', 'description', 'category_to_supermarket')
fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
id = serializers.IntegerField(required=False)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).filter(space=self.context['request'].space).first():
return property_type
return super().create(validated_data)
class Meta:
model = PropertyType
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
property_type = PropertyTypeSerializer()
property_amount = CustomDecimalField()
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = Property
fields = ('id', 'property_amount', 'property_type')
class RecipeSimpleSerializer(WritableNestedModelSerializer):
@@ -512,7 +568,7 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'plural_name')
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
# shopping = serializers.SerializerMethodField('get_shopping_status')
@@ -523,19 +579,30 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
properties = PropertySerializer(many=True, allow_null=True, required=False)
properties_food_unit = UnitSerializer(allow_null=True, required=False)
properties_food_amount = CustomDecimalField(required=False)
recipe_filter = 'steps__ingredients__food'
images = ['recipe__image']
def get_substitute_onhand(self, obj):
shared_users = None
if request := self.context.get('request', None):
shared_users = getattr(request, '_shared_users', None)
if shared_users is None:
if not self.context["request"].user.is_authenticated:
return []
shared_users = []
if c := caches['default'].get(
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
shared_users = c
else:
try:
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
self.context['request'].user.id]
except AttributeError:
shared_users = []
caches['default'].set(
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
shared_users, timeout=5 * 60)
# TODO ugly hack that improves API performance significantly, should be done properly
except AttributeError: # Anonymous users (using share links) don't have shared users
pass
filter = Q(id__in=obj.substitute.all())
if obj.substitute_siblings:
filter |= Q(path__startswith=obj.path[:Food.steplen * (obj.depth - 1)], depth=obj.depth)
@@ -547,7 +614,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
def create(self, validated_data):
name = validated_data.pop('name').strip()
name = validated_data['name'].strip()
if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip()
@@ -579,7 +646,18 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
else:
validated_data['onhand_users'] = list(set(onhand_users) - set(shared_users))
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data)
if properties_food_unit := validated_data.pop('properties_food_unit', None):
properties_food_unit = Unit.objects.filter(name=properties_food_unit['name']).first()
properties = validated_data.pop('properties', None)
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, properties_food_unit=properties_food_unit,
defaults=validated_data)
if properties and len(properties) > 0:
for p in properties:
obj.properties.add(Property.objects.create(property_type_id=p['property_type']['id'], property_amount=p['property_amount'], space=space))
return obj
def update(self, instance, validated_data):
@@ -606,9 +684,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
'properties', 'properties_food_amount', 'properties_food_unit',
'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields'
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@@ -618,9 +698,24 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
unit = UnitSerializer(allow_null=True)
used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
amount = CustomDecimalField()
conversions = serializers.SerializerMethodField('get_conversions')
def get_used_in_recipes(self, obj):
return list(Recipe.objects.filter(steps__ingredients=obj.id).values('id', 'name'))
used_in = []
for s in obj.step_set.all():
for r in s.recipe_set.all():
used_in.append({'id': r.id, 'name': r.name})
return used_in
def get_conversions(self, obj):
if obj.unit and obj.food:
uch = UnitConversionHelper(self.context['request'].space)
conversions = []
for c in uch.get_conversions(obj):
conversions.append({'food': c.food.name, 'unit': c.unit.name, 'amount': c.amount}) # TODO do formatting in helper
return conversions
else:
return []
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@@ -633,10 +728,11 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
class Meta:
model = Ingredient
fields = (
'id', 'food', 'unit', 'amount', 'note', 'order',
'id', 'food', 'unit', 'amount', 'conversions', 'note', 'order',
'is_header', 'no_amount', 'original_text', 'used_in_recipes',
'always_use_plural_unit', 'always_use_plural_food',
)
read_only_fields = ['conversions', ]
class IngredientSerializer(IngredientSimpleSerializer):
@@ -688,6 +784,30 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
)
class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin):
name = serializers.SerializerMethodField('get_conversion_name')
base_unit = UnitSerializer()
converted_unit = UnitSerializer()
food = FoodSerializer(allow_null=True, required=False)
base_amount = CustomDecimalField()
converted_amount = CustomDecimalField()
def get_conversion_name(self, obj):
text = f'{round(obj.base_amount)} {obj.base_unit} '
if obj.food:
text += f' {obj.food}'
return text + f' = {round(obj.converted_amount)} {obj.converted_unit}'
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 = UnitConversion
fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
class NutritionInformationSerializer(serializers.ModelSerializer):
carbohydrates = CustomDecimalField()
fats = CustomDecimalField()
@@ -738,21 +858,28 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
class RecipeSerializer(RecipeBaseSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
properties = PropertySerializer(many=True, required=False)
steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True)
shared = UserSerializer(many=True, required=False)
rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
food_properties = serializers.SerializerMethodField('get_food_properties')
def get_food_properties(self, obj):
fph = FoodPropertyHelper(obj.space) # initialize with object space since recipes might be viewed anonymously
return fph.calculate_recipe_properties(obj)
class Meta:
model = Recipe
fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
'internal', 'show_ingredient_overview', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
'last_cooked',
'private', 'shared',
)
read_only_fields = ['image', 'created_by', 'created_at']
read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
def validate(self, data):
above_limit, msg = above_space_limit(self.context['request'].space)
@@ -1089,13 +1216,19 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
if obj.email:
try:
if InviteLink.objects.filter(space=self.context['request'].space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.get_user_display_name())
message += _(' to join their Tandoor Recipes space ') + escape(self.context['request'].space.name) + '.\n\n'
message += _('Click the following link to activate your account: ') + self.context['request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n'
if InviteLink.objects.filter(space=self.context['request'].space,
created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(
self.context['request'].user.get_user_display_name())
message += _(' to join their Tandoor Recipes space ') + escape(
self.context['request'].space.name) + '.\n\n'
message += _('Click the following link to activate your account: ') + self.context[
'request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
message += _('If the link does not work use the following code to manually join the space: ') + str(
obj.uuid) + '\n\n'
message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n'
message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
message += _(
'Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
send_mail(
_('Tandoor Recipes Invite'),
@@ -1112,7 +1245,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
class Meta:
model = InviteLink
fields = (
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'created_by', 'created_at',)
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by', 'created_at',)
read_only_fields = ('id', 'uuid', 'created_by', 'created_at',)
@@ -1204,7 +1337,8 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
class Meta:
model = Ingredient
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit', 'always_use_plural_food')
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit',
'always_use_plural_food')
class StepExportSerializer(WritableNestedModelSerializer):
@@ -1228,7 +1362,7 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
model = Recipe
fields = (
'name', 'description', 'keywords', 'steps', 'working_time',
'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text',
'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text', 'source_url',
)
def create(self, validated_data):

View File

@@ -4,15 +4,17 @@ from functools import wraps
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.postgres.search import SearchVector
from django.core.cache import caches
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import translation
from django_scopes import scope, scopes_disabled
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.managers import DICTIONARY
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields)
ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit, PropertyType)
SQLITE = True
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
@@ -149,3 +151,15 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
print("MEAL_AUTO_ADD Created SLR")
except AttributeError:
pass
@receiver(post_save, sender=Unit)
def clear_unit_cache(sender, instance=None, created=False, **kwargs):
if instance:
caches['default'].delete(CacheHelper(instance.space).BASE_UNITS_CACHE_KEY)
@receiver(post_save, sender=PropertyType)
def clear_property_type_cache(sender, instance=None, created=False, **kwargs):
if instance:
caches['default'].delete(CacheHelper(instance.space).PROPERTY_TYPE_CACHE_KEY)

View File

@@ -7,8 +7,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="2000px"
height="2000px"
viewBox="0 0 2000 2000"
version="1.1"
id="SVGRoot"

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

10486
cookbook/static/themes/tandoor_dark.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex,nofollow"/>
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
@@ -49,7 +50,7 @@
<script type="text/javascript">
$.fn.select2.defaults.set("theme", "bootstrap");
{% if request.user.is_authenticated %}
window.ACTIVE_SPACE_ID = '{{request.space.id}}';
window.ACTIVE_SPACE_ID = '{{request.space.id}}';
{% endif %}
</script>
@@ -72,7 +73,7 @@
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}"
<nav class="navbar navbar-expand-lg {% nav_color request %}"
id="id_main_nav"
style="{% sticky_nav request %}">
@@ -117,6 +118,10 @@
<a class="nav-link" href="{% url 'view_books' %}"><i
class="fas fa-fw fa-book-open"></i> {% trans 'Books' %}</a>
</li>
{% plugin_main_nav_templates as plugin_main_nav_templates %}
{% for pn in plugin_main_nav_templates %}
{% include pn %}
{% endfor %}
</ul>
<ul class="navbar-nav ml-auto">
@@ -269,6 +274,33 @@
</div>
</a>
</div>
<div class="col-4">
<a href="{% url 'list_property_type' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-database fa-2x"></i>
</div>
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
{% trans 'Properties' %}
</div>
</div>
</a>
</div>
</div>
<div class="row m-0 mt-2 mt-md-0">
<div class="col-4">
<a href="{% url 'list_unit_conversion' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-exchange-alt fa-2x"></i>
</div>
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
{% trans 'Unit Conversions' %}
</div>
</div>
</a>
</div>
</div>
</div>
</li>
@@ -322,6 +354,12 @@
<a class="dropdown-item" href="{% url 'view_space_overview' %}"><i
class="fas fa-list"></i> {% trans 'Overview' %}</a>
{% endif %}
{% plugin_dropdown_nav_templates as plugin_dropdown_nav_templates %}
{% for pn in plugin_dropdown_nav_templates %}
<div class="dropdown-divider"></div>
{% include pn %}
{% endfor %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i
class="fab fa-markdown fa-fw"></i> {% trans 'Markdown Guide' %}</a>
@@ -348,6 +386,7 @@
</div>
</nav>
{% message_of_the_day request as message_of_the_day %}
{% if message_of_the_day %}
<div class="bg-info" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
@@ -412,7 +451,7 @@
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
localStorage.setItem('DEBUG', "{% is_debug %}")
localStorage.setItem('USER_ID', "{{request.user.pk}}")
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {

View File

@@ -11,29 +11,39 @@
{% block content %}
<h1>{% trans 'System' %}</h1>
<br/>
<br/>
<br/>
<br/>
<h3>{% trans 'System Information' %}</h3>
{% blocktrans %}
Django Recipes is an open source free software application. It can be found on
<a href="https://github.com/vabene1111/recipes">GitHub</a>.
Changelogs can be found <a href="https://github.com/vabene1111/recipes/releases">here</a>.
{% endblocktrans %}
<br/>
<br/>
Current Version: {% if version and version != '' %}
<a href="https://github.com/vabene1111/recipes/releases/tag/{{ version }}">{{ version }}</a>{% else %}
{{ version }}{% endif %}<br/>
Ref: <a href="https://github.com/vabene1111/recipes/commit/{{ ref }}">{{ ref }}</a>
<br/>
<br/>
<br/>
<h4>{% trans 'Media Serving' %} <span class="badge badge-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
<h3 class="mt-5">{% trans 'System Information' %}</h3>
{% if version_info %}
<div class="row">
<div class="col">
<div class="list-group">
{% for v in version_info %}
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ v.name }} ({{ v.branch }}) {% if v.tag %}- {{ v.tag }}{% endif %}</h5>
</div>
<pre class="card-text p-2" style="border: 1px solid lightgrey; border-radius: 5px" target="_blank">{{ v.version }}</pre>
<a href="{{ v.website }}">Website</a>
{% if v.commit_link %}
- <a href="{{ v.commit_link }}" target="_blank">Commit</a>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% else %}
{% blocktrans %}
You need to execute <code>version.py</code> in your update script to generate version information (done automatically in docker).
{% endblocktrans %}
{% endif %}
<h4 class="mt-3">{% trans 'Media Serving' %} <span class="badge badge-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if gunicorn_media %}
{% blocktrans %}Serving media files directly using gunicorn/python is <b>not recommend</b>!
@@ -44,10 +54,9 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>{% trans 'Secret Key' %} <span
<h4 class="mt-3">{% trans 'Secret Key' %} <span
class="badge badge-{% if secret_key %}danger{% else %}success{% endif %}">{% if secret_key %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if secret_key %}
@@ -60,10 +69,8 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>{% trans 'Debug Mode' %} <span
<h4 class="mt-3">{% trans 'Debug Mode' %} <span
class="badge badge-{% if debug %}danger{% else %}success{% endif %}">{% if debug %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if debug %}
@@ -75,10 +82,8 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>{% trans 'Database' %} <span
<h4 class="mt-3">{% trans 'Database' %} <span
class="badge badge-{% if postgres %}warning{% else %}success{% endif %}">{% if postgres %}
{% trans 'Info' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if postgres %}
@@ -89,9 +94,8 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>Debug</h4>
<h4 class="mt-3">Debug</h4>
<textarea class="form-control" rows="20">
Gunicorn Media: {{ gunicorn_media }}
Sqlite: {{ postgres }}
@@ -99,9 +103,9 @@ Debug: {{ debug }}
{% for key,value in request.META.items %}{% if key in 'SERVER_PORT,REMOTE_HOST,REMOTE_ADDR,SERVER_PROTOCOL' %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
{% for key,value in request.META.items %}{% if 'HTTP_' in key %}{{ key }}:{{ value }}
{% for key,value in request.META.items %}{% if 'HTTP_' in key %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
{% for key,value in request.META.items %}{% if 'wsgi.' in key %}{{ key }}:{{ value }}
{% for key,value in request.META.items %}{% if 'wsgi.' in key %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
</textarea>
<br/>

View File

@@ -16,7 +16,7 @@ from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name
from recipes import settings
from recipes.settings import STATIC_URL
from recipes.settings import STATIC_URL, PLUGINS
register = template.Library()
@@ -132,6 +132,22 @@ def is_debug():
def markdown_link():
return f"{_('You can use markdown to format this field. See the ')}<a target='_blank' href='{reverse('docs_markdown')}'>{_('docs here')}</a>"
@register.simple_tag
def plugin_dropdown_nav_templates():
templates = []
for p in PLUGINS:
if p['nav_dropdown']:
templates.append(p['nav_dropdown'])
return templates
@register.simple_tag
def plugin_main_nav_templates():
templates = []
for p in PLUGINS:
if p['nav_main']:
templates.append(p['nav_main'])
return templates
@register.simple_tag
def bookmarklet(request):

View File

@@ -16,6 +16,7 @@ def theme_url(request):
UserPreference.DARKLY: 'themes/darkly.min.css',
UserPreference.SUPERHERO: 'themes/superhero.min.css',
UserPreference.TANDOOR: 'themes/tandoor.min.css',
UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
@@ -26,8 +27,12 @@ def theme_url(request):
@register.simple_tag
def nav_color(request):
if not request.user.is_authenticated:
return 'primary'
return request.user.userpreference.nav_color.lower()
return 'navbar-light bg-primary'
if request.user.userpreference.nav_color.lower() in ['light', 'warning', 'info', 'success']:
return f'navbar-light bg-{request.user.userpreference.nav_color.lower()}'
else:
return f'navbar-dark bg-{request.user.userpreference.nav_color.lower()}'
@register.simple_tag

View File

@@ -2,11 +2,12 @@ import json
import pytest
from django.contrib import auth
from django.core.cache import caches
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Food, FoodInheritField, Ingredient, ShoppingList, ShoppingListEntry
from cookbook.models import Food, Ingredient, ShoppingListEntry
from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
SupermarketCategoryFactory)
@@ -28,7 +29,6 @@ if (Food.node_order_by):
else:
node_location = 'last-child'
register(FoodFactory, 'obj_1', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_2', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_3', space=LazyFixture('space_2'))
@@ -56,23 +56,32 @@ def obj_tree_1(request, space_1):
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
objs = []
inherit = params.pop('inherit', False)
objs.extend(FoodFactory.create_batch(3, space=space_1, **params))
FoodFactory.create_batch(3, space=space_1, **params)
objs = Food.objects.values_list('id', flat=True)
obj_id = objs[1]
child_id = objs[0]
parent_id = objs[2]
# set all foods to inherit everything
if inherit:
inherit = Food.inheritable_fields
Through = Food.objects.filter(space=space_1).first().inherit_fields.through
Through = Food.objects.filter(
space=space_1).first().inherit_fields.through
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
])
objs[0].move(objs[1], node_location)
objs[1].move(objs[2], node_location)
return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object
Food.objects.get(id=child_id).move(
Food.objects.get(id=obj_id), node_location)
Food.objects.get(id=obj_id).move(
Food.objects.get(id=parent_id), node_location)
# whenever you move/merge a tree it's safest to re-get the object
return Food.objects.get(id=obj_id)
@pytest.mark.parametrize("arg", [
@@ -90,8 +99,12 @@ def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0
obj_1.space = space_2
obj_1.save()
with scopes_disabled():
# for some reason the 'path' attribute changes between the factory and the test
obj_1 = Food.objects.get(id=obj_1.id)
obj_2 = Food.objects.get(id=obj_2.id)
obj_1.space = space_2
obj_1.save()
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
@@ -107,19 +120,23 @@ def test_list_filter(obj_1, obj_2, u1_s1):
assert obj_2.name in [x['name'] for x in response['results']]
assert response['results'][0]['name'] < response['results'][1]['name']
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert response['count'] == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
assert response['count'] == 1
@@ -262,8 +279,9 @@ def test_integrity(u1_s1, recipe_1_s1):
def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
@@ -295,8 +313,9 @@ def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
def test_move_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# move child to root
r = u1_s1.put(reverse(MOVE_URL, args=[obj_tree_1.id, 0]))
assert r.status_code == 200
@@ -351,7 +370,7 @@ def test_merge_shopping_entries(obj_tree_1, u1_s1, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
ShoppingListEntryFactory.create(food=parent, space=space_1)
ShoppingListEntryFactory.create(food=parent, space=space_1)
ShoppingListEntryFactory.create(food=child, space=space_1)
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
@@ -371,8 +390,10 @@ def test_merge_shopping_entries(obj_tree_1, u1_s1, space_1):
assert obj_tree_1.shopping_entries.count() == 1 # now has child's ingredient
def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1
@@ -416,8 +437,9 @@ def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# attempt to merge with non-existent parent
r = u1_s1.put(
@@ -451,44 +473,63 @@ def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
obj_2 = Food.objects.get(id=obj_2.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# should return root objects in the space (obj_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
assert len(response['results']) == 2
# django_tree bypasses ORM - best to retrieve all changed objects
with scopes_disabled():
obj_2.move(parent, node_location)
obj_2 = Food.objects.get(id=obj_2.id)
parent = Food.objects.get(id=parent.id)
# should return direct children of parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
assert response['count'] == 2
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
response = json.loads(u1_s1.get(
f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 2
def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
obj_2 = Food.objects.get(id=obj_2.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_2.move(parent, node_location)
# should return full tree starting at parent (obj_tree_1, obj_2), ignoring query filters
obj_2 = Food.objects.get(id=obj_2.id)
# should return full tree starting at, but excluding parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
assert response['count'] == 4
# filtering is ignored - should return identical results as ?tree=x
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 4
# This is more about the model than the API - should this be moved to a different test?
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'),
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'),
({'substitute_children': True, 'inherit': True}, 'substitute_children', True, 'false'),
({'substitute_children': True, 'inherit': False}, 'substitute_children', False, 'false'),
({'substitute_siblings': True, 'inherit': True}, 'substitute_siblings', True, 'false'),
({'substitute_siblings': True, 'inherit': False}, 'substitute_siblings', False, 'false'),
({'has_category': True, 'inherit': True},
'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False},
'supermarket_category', False, 'cat_1'),
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
({'ignore_shopping': True, 'inherit': False},
'ignore_shopping', False, 'false'),
({'substitute_children': True, 'inherit': True},
'substitute_children', True, 'false'),
({'substitute_children': True, 'inherit': False},
'substitute_children', False, 'false'),
({'substitute_siblings': True, 'inherit': True},
'substitute_siblings', True, 'false'),
({'substitute_siblings': True, 'inherit': False},
'substitute_siblings', False, 'false'),
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space):
@@ -498,8 +539,10 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
new_val = request.getfixturevalue(new_val)
# if this test passes it demonstrates that inheritance works
# when moving to a parent as each food is created with a different category
assert (getattr(parent, field) == getattr(obj_tree_1, field)) in [inherit, True]
assert (getattr(obj_tree_1, field) == getattr(child, field)) in [inherit, True]
assert (getattr(parent, field) == getattr(
obj_tree_1, field)) in [inherit, True]
assert (getattr(obj_tree_1, field) == getattr(
child, field)) in [inherit, True]
# change parent to a new value
setattr(parent, field, new_val)
with scope(space=parent.space):
@@ -511,11 +554,13 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
assert (getattr(obj_tree_1, field) == new_val) == inherit
assert (getattr(child, field) == new_val) == inherit
# TODO add test_inherit with child_inherit
@pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}),
({'has_category': True, 'inherit': False, 'ignore_shopping': True,
'substitute_children': True, 'substitute_siblings': True}),
], indirect=['obj_tree_1'])
@pytest.mark.parametrize("global_reset", [True, False])
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category'])
@@ -534,10 +579,13 @@ def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field):
assert getattr(parent, field) != getattr(obj_tree_1, field)
if global_reset:
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
# set default inherit fields
space_1.food_inherit.add(
*Food.inheritable_fields.values_list('id', flat=True))
parent.reset_inheritance(space=space_1)
else:
obj_tree_1.child_inherit_fields.set(Food.inheritable_fields.values_list('id', flat=True))
obj_tree_1.child_inherit_fields.set(
Food.inheritable_fields.values_list('id', flat=True))
obj_tree_1.save()
parent.reset_inheritance(space=space_1, food=obj_tree_1)
# djangotree bypasses ORM and need to be retrieved again
@@ -545,12 +593,14 @@ def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field):
parent = Food.objects.get(id=parent.id)
child = Food.objects.get(id=child.id)
assert (getattr(parent, field) == getattr(obj_tree_1, field)) == global_reset
assert (getattr(parent, field) == getattr(
obj_tree_1, field)) == global_reset
assert getattr(obj_tree_1, field) == getattr(child, field)
@pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}),
({'has_category': True, 'inherit': False, 'ignore_shopping': True,
'substitute_children': True, 'substitute_siblings': True}),
], indirect=['obj_tree_1'])
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category'])
def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
@@ -558,13 +608,15 @@ def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
parent = obj_tree_1.get_parent()
Food.objects.all().delete()
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
# set default inherit fields
space_1.food_inherit.add(
*Food.inheritable_fields.values_list('id', flat=True))
parent.reset_inheritance(space=space_1)
def test_onhand(obj_1, u1_s1, u2_s1):
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
def test_onhand(obj_1, u1_s1, u2_s1, space_1):
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is False
u1_s1.patch(
reverse(
@@ -574,10 +626,12 @@ def test_onhand(obj_1, u1_s1, u2_s1):
{'food_onhand': True},
content_type='application/json'
)
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is False
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
user1.userpreference.shopping_share.add(user2)
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
caches['default'].set(f'shopping_shared_users_{space_1.id}_{user2.id}', None)
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is True

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 Food, MealType, PropertyType, Property
LIST_URL = 'api:property-list'
DETAIL_URL = 'api:property-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
pt = PropertyType.objects.get_or_create(name='test_1', space=space_1)[0]
return Property.objects.get_or_create(property_amount=100, property_type=pt, space=space_1)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
pt = PropertyType.objects.get_or_create(name='test_2', space=space_1)[0]
return Property.objects.get_or_create(property_amount=100, property_type=pt, 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
@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}
),
{'property_amount': 200},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['property_amount'] == 200
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2, space_1):
with scopes_disabled():
pt = PropertyType.objects.get_or_create(name='test_1', space=space_1)[0]
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'property_amount': 100, 'property_type': {'id': pt.id, 'name': pt.name}},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['property_amount'] == 100
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 MealType.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, PropertyType
LIST_URL = 'api:propertytype-list'
DETAIL_URL = 'api:propertytype-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
return PropertyType.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
return PropertyType.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
@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}
)
)
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

@@ -81,10 +81,10 @@ def test_share_permission(recipe_1_s1, u1_s1, u1_s2, u2_s1, a_u):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 404],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
@@ -140,7 +140,7 @@ def test_update_private_recipe(u1_s1, u2_s1, recipe_1_s1):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])

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