Compare commits

..

261 Commits

Author SHA1 Message Date
vabene1111
b90c70b2a3 huge documentation and setup restructure 2021-01-05 10:38:18 +01:00
vabene1111
bcf50f30bc Merge pull request #311 from vabene1111/dependabot/pip/django-3.1.5
Bump django from 3.1.4 to 3.1.5
2021-01-05 08:13:00 +01:00
dependabot[bot]
065ed6c437 Bump django from 3.1.4 to 3.1.5
Bumps [django](https://github.com/django/django) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.4...3.1.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-05 06:24:30 +00:00
vabene1111
285e09f40a moved config again 2021-01-05 00:31:48 +01:00
vabene1111
0398f36949 removed ngxinx from ignored directories 2021-01-05 00:19:14 +01:00
vabene1111
ea30eb96cd changed up structure again 2021-01-04 23:40:58 +01:00
vabene1111
b787ae49bb another way of doing it 2021-01-04 23:12:01 +01:00
vabene1111
f8e2283a69 testing automatic creation of nginx config dir 2021-01-04 22:50:07 +01:00
vabene1111
13d51a7b46 further testing of nginx configurations 2021-01-04 22:31:48 +01:00
vabene1111
e74ae06b64 testing nginx configuration 2021-01-04 22:30:03 +01:00
vabene1111
aa495250c9 further work on documentation 2021-01-04 09:41:26 +01:00
vabene1111
f8ee48c23b mkdocs settings 2021-01-04 08:38:57 +01:00
vabene1111
320246b18b Merge pull request #309 from vabene1111/dependabot/pip/pillow-8.1.0
Bump pillow from 8.0.1 to 8.1.0
2021-01-04 08:32:26 +01:00
dependabot[bot]
00992da998 Bump pillow from 8.0.1 to 8.1.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.0.1 to 8.1.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.0.1...8.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-04 07:22:34 +00:00
vabene1111
2b9ad2feed removed fixture based backup
many different things can lead to this method of backing up failing both on backup and on restore.
to not give the user a false sense of security this feature was removed for the time being.
once i have time i will add a proper backup system.
2021-01-04 00:30:39 +01:00
vabene1111
257127bd4e ... 2021-01-03 10:34:14 +01:00
vabene1111
b1df118140 testing another action 2021-01-03 10:26:29 +01:00
vabene1111
da6b437b20 further testing 2021-01-03 10:19:32 +01:00
vabene1111
6fe4c79b0d testing with mkdocs 2021-01-03 10:08:54 +01:00
vabene1111
1793753cb4 different action to include theme 2021-01-03 10:05:30 +01:00
vabene1111
9ed1aff0d2 theme and name for mkdocs 2021-01-03 10:01:51 +01:00
vabene1111
51c3ec5762 added doc name 2021-01-03 09:58:12 +01:00
vabene1111
5feeabb498 mk docs 2021-01-03 09:57:15 +01:00
vabene1111
c4aa3eb019 test navigation 2021-01-03 09:49:17 +01:00
vabene1111
e8b9f473a6 testing around with github pages 2021-01-03 09:47:57 +01:00
vabene1111
279b4dc025 testing GH pages 2021-01-03 09:35:49 +01:00
vabene1111
6a1226ca26 updated contributers 2021-01-01 21:18:50 +01:00
vabene1111
b9ee7d53fa Merge pull request #299 from melkypie/locale-lv
Add latvian language
2021-01-01 20:59:11 +01:00
melkypie
ace7ee4274 Add latvian language 2020-12-31 18:47:07 +02:00
vabene1111
16968db1cf Update README.md 2020-12-31 13:41:47 +01:00
vabene1111
2b24155dd2 emoji updated 2020-12-31 13:39:10 +01:00
vabene1111
5a7c914fe7 added lots of user information 2020-12-31 13:38:16 +01:00
vabene1111
f822e03be0 Create SECURITY.md 2020-12-31 13:11:00 +01:00
vabene1111
1bdf14dbf9 made and compiled messages + fixed lots of typos 2020-12-31 12:56:18 +01:00
vabene1111
6ef173d82d fixed and cleand up a lot of servings related stuff 2020-12-31 12:44:19 +01:00
vabene1111
1e471ad40d removed all trans tags from javascript code
replaced by django recommended javascript catalog
still need to sort out the different translation domains
2020-12-31 12:30:31 +01:00
vabene1111
4ff1a6bc93 Merge pull request #297 from lipschultz/created-by
Assign authenticated user to created_by
2020-12-31 12:16:16 +01:00
vabene1111
7d1f47edc5 temporary fix for javascript translation 2020-12-30 17:00:38 +01:00
vabene1111
f69d7898d5 Merge pull request #290 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_es
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'es'
2020-12-30 11:20:32 +01:00
transifex-integration[bot]
9692e2386b Apply translations in es
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'es' language.
2020-12-28 13:34:59 +00:00
vabene1111
93b2e2d7e4 fixed small issues 2020-12-28 14:18:51 +01:00
vabene1111
8b2833f353 re added stuff 2020-12-28 14:14:34 +01:00
vabene1111
643dbbc294 Merge pull request #58 from tourn/recipe-serving-count
Add serving count to recipe
2020-12-28 10:20:00 +01:00
vabene1111
c4273a4c3f Merge branch 'develop' into recipe-serving-count 2020-12-28 10:19:52 +01:00
vabene1111
95461316a5 prevent creation of empty string units 2020-12-27 23:12:24 +01:00
vabene1111
1775b64ba4 better migration for unit fix 2020-12-27 23:10:22 +01:00
vabene1111
5a9270373f Merge branch 'translations_cookbook-locale-en-lc-messages-django-po--develop_it' into develop
# Conflicts:
#	cookbook/locale/it/LC_MESSAGES/django.po
2020-12-27 16:57:35 +01:00
transifex-integration[bot]
37f98ce9fe Apply translations in it
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'it' language.
2020-12-27 12:46:39 +00:00
vabene1111
fa556c9a7f added migration to fix emtpy units
set all units to none for all recipes containing empty named units and delete them
2020-12-26 17:20:17 +01:00
vabene1111
29e1d1286c fixed importer empty units 2020-12-26 15:17:37 +01:00
Michael Lipschultz
f489043077 Assign authenticated user to created_by 2020-12-26 09:09:47 -05:00
vabene1111
bdd004518c verified and updated importer tests 2020-12-26 14:50:54 +01:00
vabene1111
840f5ec60d added ingredient parser test 2020-12-26 13:57:51 +01:00
vabene1111
566eea1d75 Merge pull request #277 from l0c4lh057/master
Improve text to ingredient parsing
2020-12-26 13:52:24 +01:00
vabene1111
bb48655acb Merge pull request #279 from l0c4lh057/improve
Minor improvements
2020-12-26 13:48:54 +01:00
vabene1111
d723165b1c updated base translations 2020-12-26 13:48:33 +01:00
vabene1111
592bd4f11e fixed some english translations 2020-12-26 13:46:55 +01:00
vabene1111
0aec23fcdd added tests for recipe view permissions 2020-12-26 13:28:49 +01:00
vabene1111
a23dc717aa added new languages 2020-12-26 13:16:01 +01:00
vabene1111
d364994ed7 compiled messages 2020-12-26 13:14:36 +01:00
vabene1111
a38ed28512 Merge pull request #280 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_nl
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'nl'
2020-12-26 10:52:56 +01:00
vabene1111
1f5c02bcc3 Merge branch 'develop' into translations_cookbook-locale-en-lc-messages-django-po--develop_nl 2020-12-26 10:52:27 +01:00
vabene1111
f4afdfbc07 Merge pull request #288 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_es
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'es'
2020-12-26 10:47:51 +01:00
transifex-integration[bot]
f753b63b13 Apply translations in es
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'es' language.
2020-12-25 22:47:41 +00:00
tourn
6f3068a28c Show default serving count in meal planner 2020-12-24 11:51:51 +01:00
tourn
aa57b47d18 Use correct serving count from recipe view -> add to shopping 2020-12-24 11:28:59 +01:00
tourn
113e9ef1e3 Merge remote-tracking branch 'upstream/develop' into recipe-serving-count 2020-12-24 11:17:56 +01:00
transifex-integration[bot]
5899527621 Apply translations in nl
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'nl' language.
2020-12-23 15:13:42 +00:00
Aaron
0a40de0f14 Add min/max to servings spinner 2020-12-22 23:52:40 +01:00
Aaron
bc31f013c0 Fix 'All Keywords' label in website import 2020-12-22 23:51:55 +01:00
Aaron
e7fc15dc72 Show advanced search again if it was used 2020-12-22 23:50:59 +01:00
Aaron
79396cec9e switch from double to single quotes 2020-12-21 22:42:27 +01:00
Aaron
5e07c6130f Switch to 4-space indentation 2020-12-21 20:14:32 +01:00
Aaron
94e1fdfbff Improve text to ingredient parsing
The previous implementation of parsing ingredients was very simple. I now wrote a parser
that I would consider good. It takes care of several edge cases and notations.

- Supports fraction unicode (½, ¼, ⅜, ...)
- Supports notations like `1½` and `1 1/2`
- Supports unit directly after the amount without space inbetween (`2g`, `2½g`)
- Supports notes (`5g onion (cubed)` -> amount: 5, unit: g, ingredient: onion, note: cubed)
- Supports notes (`5g onion, cubed` -> amount: 5, unit: g, ingredient: onion, note: cubed)
- Does not break when both commas and brackets exist
2020-12-21 20:00:46 +01:00
vabene1111
a0d414c83f Merge pull request #267 from Nailik/patch-1
Update Synology Setup Readme
2020-12-18 20:16:41 +01:00
Nailik
1441368465 Update Synology Setup Readme
Fixed env folder to .env folder and added instruction to set postgres password

Added information how to setup ssl and a task to update ssl certificate when needed
2020-12-18 16:32:34 +01:00
vabene1111
6f301c4771 Merge pull request #264 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_it
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'it'
2020-12-18 14:01:08 +01:00
transifex-integration[bot]
ec31d251ea Apply translations in it
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'it' language.
2020-12-18 12:09:20 +00:00
vabene1111
289625923f Merge pull request #261 from vabene1111/dependabot/pip/requests-2.25.1
Bump requests from 2.25.0 to 2.25.1
2020-12-17 09:36:47 +01:00
dependabot[bot]
a42a76a2cf Bump requests from 2.25.0 to 2.25.1
Bumps [requests](https://github.com/psf/requests) from 2.25.0 to 2.25.1.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.25.0...v2.25.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-17 06:23:09 +00:00
vabene1111
fd1216cd22 Merge pull request #259 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_fr
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'fr'
2020-12-16 15:49:05 +01:00
transifex-integration[bot]
3f6a342026 Apply translations in fr
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'fr' language.
2020-12-16 14:47:47 +00:00
vabene1111
f72fc699f8 fixed import image error 2020-12-15 22:54:10 +01:00
vabene1111
cdcca80196 Merge pull request #252 from vabene1111/dependabot/pip/django-cleanup-5.1.0
Bump django-cleanup from 4.0.0 to 5.1.0
2020-12-15 22:44:08 +01:00
vabene1111
400cd2f6a0 Merge pull request #251 from vabene1111/dependabot/pip/djangorestframework-3.12.2
Bump djangorestframework from 3.11.0 to 3.12.2
2020-12-15 22:44:02 +01:00
dependabot[bot]
37a4821d01 Bump django-cleanup from 4.0.0 to 5.1.0
Bumps [django-cleanup](https://github.com/un1t/django-cleanup) from 4.0.0 to 5.1.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/4.0.0...5.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-15 21:44:00 +00:00
vabene1111
d165075a96 Merge pull request #250 from vabene1111/dependabot/pip/django-crispy-forms-1.10.0
Bump django-crispy-forms from 1.9.1 to 1.10.0
2020-12-15 22:43:38 +01:00
vabene1111
a062173ebd Merge pull request #249 from vabene1111/dependabot/pip/django-3.1.4
Bump django from 3.1.3 to 3.1.4
2020-12-15 22:43:31 +01:00
vabene1111
806963c396 Merge pull request #248 from vabene1111/dependabot/pip/icalendar-4.0.7
Bump icalendar from 4.0.6 to 4.0.7
2020-12-15 22:42:44 +01:00
vabene1111
851853740d added catalan language chooser 2020-12-15 22:19:36 +01:00
vabene1111
7ca88f3c0a fixed scaling for fractions 2020-12-15 22:16:15 +01:00
vabene1111
ac2e9dd6cb updated german translation 2020-12-15 22:04:40 +01:00
vabene1111
b2a34ce59a Merge pull request #255 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_de
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'de'
2020-12-15 22:04:12 +01:00
transifex-integration[bot]
6124501f5a Apply translations in de
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'de' language.
2020-12-15 21:03:40 +00:00
vabene1111
4ec313f752 fixed another importer issue 2020-12-15 21:59:34 +01:00
vabene1111
dd07c56ede fixed issue with new fraction importer 2020-12-15 21:52:56 +01:00
vabene1111
77fae46aee translation updates 2020-12-15 21:39:42 +01:00
vabene1111
ff573b0358 Merge pull request #254 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_ca
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'ca'
2020-12-15 13:02:52 +01:00
transifex-integration[bot]
247eab2a4f Apply translations in ca
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'ca' language.
2020-12-15 12:00:28 +00:00
dependabot[bot]
dc46502667 Bump djangorestframework from 3.11.0 to 3.12.2
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.11.0 to 3.12.2.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.11.0...3.12.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-15 06:28:44 +00:00
dependabot[bot]
ac58f1959d Bump django-crispy-forms from 1.9.1 to 1.10.0
Bumps [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) from 1.9.1 to 1.10.0.
- [Release notes](https://github.com/django-crispy-forms/django-crispy-forms/releases)
- [Changelog](https://github.com/django-crispy-forms/django-crispy-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/django-crispy-forms/django-crispy-forms/compare/1.9.1...1.10.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-15 06:28:43 +00:00
dependabot[bot]
f4543f8d65 Bump django from 3.1.3 to 3.1.4
Bumps [django](https://github.com/django/django) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.3...3.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-15 06:28:42 +00:00
dependabot[bot]
d0ef5e27df Bump icalendar from 4.0.6 to 4.0.7
Bumps [icalendar](https://github.com/collective/icalendar) from 4.0.6 to 4.0.7.
- [Release notes](https://github.com/collective/icalendar/releases)
- [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst)
- [Commits](https://github.com/collective/icalendar/compare/4.0.6...4.0.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-15 06:28:41 +00:00
vabene1111
9ea90f1c87 fraction import 2020-12-14 19:56:21 +01:00
vabene1111
e7922a7e47 nutirtion fix 2020-12-14 19:12:01 +01:00
vabene1111
d3bc440c83 meal plan quality of life stuff 2020-12-14 17:37:51 +01:00
vabene1111
910b28fe2d meal plan date handling rewrite 2020-12-14 17:11:40 +01:00
vabene1111
89cd8bc2d2 fixed wrong translation 2020-12-14 15:49:32 +01:00
vabene1111
d2e9ad2ae6 requirement still needed 2020-12-14 15:14:55 +01:00
vabene1111
56c9edd328 removed deprecated psycopg2 2020-12-14 15:11:41 +01:00
vabene1111
7732aa7646 added support for fractions 2020-12-14 14:08:11 +01:00
vabene1111
9863447bac small nutrition fixes 2020-12-14 12:56:18 +01:00
vabene1111
53b00cc4c8 Merge pull request #199 from sebimarkgraf/feature/nutrition
Add nutritional values
2020-12-14 12:29:56 +01:00
vabene1111
4f34ec1be8 Merge pull request #235 from vabene1111/dependabot/pip/pillow-8.0.1
Bump pillow from 7.1.2 to 8.0.1
2020-12-14 12:08:48 +01:00
dependabot[bot]
e444ba91f0 Bump pillow from 7.1.2 to 8.0.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.1.2 to 8.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.1.2...8.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-14 11:08:35 +00:00
vabene1111
fa3513eb65 Merge pull request #234 from vabene1111/dependabot/pip/webdavclient3-3.14.5
Bump webdavclient3 from 3.14.4 to 3.14.5
2020-12-14 12:08:29 +01:00
vabene1111
d323778f1d Merge pull request #232 from vabene1111/dependabot/pip/django-autocomplete-light-3.8.1
Bump django-autocomplete-light from 3.5.1 to 3.8.1
2020-12-14 12:08:14 +01:00
vabene1111
53cb5afef6 Merge pull request #236 from vabene1111/dependabot/pip/beautifulsoup4-4.9.3
Bump beautifulsoup4 from 4.9.2 to 4.9.3
2020-12-14 12:07:37 +01:00
vabene1111
0349301919 Merge pull request #242 from vabene1111/dependabot/pip/lxml-4.6.2
Bump lxml from 4.5.1 to 4.6.2
2020-12-14 12:07:24 +01:00
vabene1111
a5d2bd75d6 recompiled messages + added timezone setting 2020-12-14 12:03:43 +01:00
vabene1111
26499ad431 Merge pull request #229 from sebimarkgraf/fix/225-random-button
Include random in filtermixin
2020-12-14 11:51:59 +01:00
dependabot[bot]
2eb72953f0 Bump lxml from 4.5.1 to 4.6.2
Bumps [lxml](https://github.com/lxml/lxml) from 4.5.1 to 4.6.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.5.1...lxml-4.6.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-30 07:22:55 +00:00
tourn
6fd9cf0d8c Use serving count in shopping list 2020-11-20 15:22:58 +01:00
tourn
1e800889e4 Merge remote-tracking branch 'upstream/master' into recipe-serving-count 2020-11-20 14:44:09 +01:00
Sebastian Markgraf
422113a745 Finish editing of nutritional information 2020-11-17 22:27:01 +01:00
dependabot[bot]
e687d0e569 Bump beautifulsoup4 from 4.9.2 to 4.9.3
Bumps [beautifulsoup4](http://www.crummy.com/software/BeautifulSoup/bs4/) from 4.9.2 to 4.9.3.

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-16 07:19:43 +00:00
dependabot[bot]
76c1529ec1 Bump webdavclient3 from 3.14.4 to 3.14.5
Bumps [webdavclient3](https://github.com/ezhov-evgeny/webdav-client-python-3) from 3.14.4 to 3.14.5.
- [Release notes](https://github.com/ezhov-evgeny/webdav-client-python-3/releases)
- [Changelog](https://github.com/ezhov-evgeny/webdav-client-python-3/blob/develop/RELEASE_NOTES.md)
- [Commits](https://github.com/ezhov-evgeny/webdav-client-python-3/compare/v3.14.4...v3.14.5)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-16 07:19:41 +00:00
dependabot[bot]
d30b2b7ec8 Bump django-autocomplete-light from 3.5.1 to 3.8.1
Bumps [django-autocomplete-light](https://github.com/yourlabs/django-autocomplete-light) from 3.5.1 to 3.8.1.
- [Release notes](https://github.com/yourlabs/django-autocomplete-light/releases)
- [Changelog](https://github.com/yourlabs/django-autocomplete-light/blob/master/CHANGELOG)
- [Commits](https://github.com/yourlabs/django-autocomplete-light/compare/3.5.1...3.8.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-16 07:19:35 +00:00
Sebastian Markgraf
c4fbad614e Include random in filtermixin 2020-11-14 20:20:30 +01:00
vabene1111
4c92a4b39c fixed shopping list recipe search 2020-11-14 11:20:43 +01:00
vabene1111
3dad5132bb Merge pull request #219 from vabene1111/dependabot/pip/markdown-3.3.3
Bump markdown from 3.2.2 to 3.3.3
2020-11-14 11:16:55 +01:00
vabene1111
7d7890445e Merge pull request #220 from vabene1111/dependabot/pip/django-3.1.3
Bump django from 3.1.1 to 3.1.3
2020-11-14 11:16:49 +01:00
vabene1111
cea015f23d Merge pull request #221 from vabene1111/dependabot/pip/drf-writable-nested-0.6.2
Bump drf-writable-nested from 0.6.1 to 0.6.2
2020-11-14 11:16:43 +01:00
vabene1111
3e8610912e Merge pull request #223 from vabene1111/dependabot/pip/django-tables2-2.3.3
Bump django-tables2 from 2.3.1 to 2.3.3
2020-11-14 11:16:38 +01:00
vabene1111
ba80ca42e6 Merge pull request #226 from vabene1111/dependabot/pip/requests-2.25.0
Bump requests from 2.23.0 to 2.25.0
2020-11-14 11:16:32 +01:00
vabene1111
c413db5460 fixed english typo 2020-11-14 11:16:01 +01:00
vabene1111
0e319ff293 fixed typo in german transaltion 2020-11-14 11:15:21 +01:00
vabene1111
88e3b22dcd Merge pull request #228 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_nl
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'nl'
2020-11-14 11:02:53 +01:00
transifex-integration[bot]
19e2094ecd Apply translations in nl
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'nl' language.
2020-11-13 22:21:02 +00:00
dependabot[bot]
215989682b Bump requests from 2.23.0 to 2.25.0
Bumps [requests](https://github.com/psf/requests) from 2.23.0 to 2.25.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.23.0...v2.25.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-12 06:37:40 +00:00
Sebastian Markgraf
2f038edf8c Fix missing string literals for translations. 2020-11-06 17:24:20 +01:00
Sebastian Markgraf
a754002f4e Revert approach to fixed nutritional values. 2020-11-06 17:22:21 +01:00
dependabot[bot]
1af2211010 Bump django-tables2 from 2.3.1 to 2.3.3
Bumps [django-tables2](https://github.com/jieter/django-tables2) from 2.3.1 to 2.3.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.3.1...v2.3.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-05 06:32:16 +00:00
dependabot[bot]
724d57ecd7 Bump django from 3.1.1 to 3.1.3
Bumps [django](https://github.com/django/django) from 3.1.1 to 3.1.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.1...3.1.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-05 06:32:15 +00:00
dependabot[bot]
4b04fada51 Bump drf-writable-nested from 0.6.1 to 0.6.2
Bumps [drf-writable-nested](https://github.com/beda-software/drf-writable-nested) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/beda-software/drf-writable-nested/releases)
- [Changelog](https://github.com/beda-software/drf-writable-nested/blob/master/CHANGELOG.md)
- [Commits](https://github.com/beda-software/drf-writable-nested/compare/v0.6.1...v0.6.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-05 06:32:15 +00:00
dependabot[bot]
4ad7043f91 Bump markdown from 3.2.2 to 3.3.3
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.2.2 to 3.3.3.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.2.2...3.3.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-05 06:32:13 +00:00
vabene1111
4dfda4439c re added standard filter to recipe api 2020-11-04 16:53:49 +01:00
vabene1111
591d185b9d improved random recipe queryset function 2020-11-04 15:09:35 +01:00
vabene1111
8d582548bd Merge pull request #213 from tourn/random-recipes2
Show random recipes in meal planner
2020-11-04 15:06:50 +01:00
vabene1111
209924e5b3 Merge pull request #191 from vabene1111/dependabot/pip/psycopg2-binary-2.8.6
Bump psycopg2-binary from 2.8.5 to 2.8.6
2020-11-04 15:01:52 +01:00
dependabot[bot]
7e3e2aadaf Bump psycopg2-binary from 2.8.5 to 2.8.6
Bumps [psycopg2-binary](https://github.com/psycopg/psycopg2) from 2.8.5 to 2.8.6.
- [Release notes](https://github.com/psycopg/psycopg2/releases)
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 14:01:23 +00:00
vabene1111
0930e615f0 Merge pull request #192 from vabene1111/dependabot/pip/simplejson-3.17.2
Bump simplejson from 3.17.0 to 3.17.2
2020-11-04 15:00:59 +01:00
vabene1111
21c759b127 Merge pull request #193 from vabene1111/dependabot/pip/whitenoise-5.2.0
Bump whitenoise from 5.1.0 to 5.2.0
2020-11-04 15:00:46 +01:00
vabene1111
7d1a83440d Merge pull request #195 from vabene1111/dependabot/pip/bleach-3.2.1
Bump bleach from 3.1.5 to 3.2.1
2020-11-04 15:00:37 +01:00
vabene1111
2d75b303fd Merge pull request #210 from vabene1111/dependabot/pip/python-dotenv-0.15.0
Bump python-dotenv from 0.13.0 to 0.15.0
2020-11-04 15:00:26 +01:00
vabene1111
a1b15d46b8 fixed note only meal plan entries not working 2020-11-04 14:58:31 +01:00
tourn
69a6edee99 Show random recipes in meal planner 2020-11-03 16:46:31 +01:00
vabene1111
0ac23b4e3a Merge pull request #211 from Tmaxxrox97/develop
Username field consistency
2020-10-29 17:00:27 +01:00
Tmaxxrox97
085e777ee0 Changed Username field label from "Name" to "Username" 2020-10-29 09:52:05 -05:00
Tmaxxrox97
c31df3f7a6 Merge pull request #1 from vabene1111/develop
Update repo
2020-10-29 07:58:32 -05:00
dependabot[bot]
98e2c0acaf Bump python-dotenv from 0.13.0 to 0.15.0
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.13.0 to 0.15.0.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v0.13.0...v0.15.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-29 06:36:05 +00:00
vabene1111
1509b8243b Update docker-publish-release.yml 2020-10-24 17:19:23 +02:00
vabene1111
e427d8b714 donst export checked items 2020-10-21 20:35:26 +02:00
vabene1111
89b8dbe57f added online check to prevent message spam 2020-10-20 23:08:30 +02:00
vabene1111
f2a17fe3bb fixed shopping list GET param regex 2020-10-20 22:51:19 +02:00
vabene1111
14e0dae6e3 compiled translations 2020-10-20 21:23:37 +02:00
vabene1111
733c281dc8 Merge pull request #200 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_de
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'de'
2020-10-20 21:22:36 +02:00
transifex-integration[bot]
c542f3154e Apply translations in de
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'de' language.
2020-10-20 19:22:06 +00:00
vabene1111
c6f40db7e3 shopping list tweaks 2020-10-20 20:54:15 +02:00
Sebastian Markgraf
31dabd4757 Add nutritions output 2020-10-18 17:31:56 +02:00
Sebastian Markgraf
7a89015ac5 Add migrations. 2020-10-16 23:59:19 +02:00
Sebastian Markgraf
2b1cde2efc Add basic output for nutritions. 2020-10-16 23:44:18 +02:00
vabene1111
cb3b8c931e fixed broken defautl share 2020-10-16 00:11:41 +02:00
vabene1111
72bea14c3a added sharing 2020-10-16 00:01:14 +02:00
vabene1111
cd46203d55 added permission classes for sharing + tests 2020-10-15 23:41:38 +02:00
Sebastian Markgraf
368d631602 Add nutrition to model. 2020-10-15 22:03:25 +02:00
vabene1111
5c1cecb7e7 fixed saving null units 2020-10-15 21:37:15 +02:00
vabene1111
526cf13b8d increased max text area size of instructions 2020-10-15 21:27:41 +02:00
vabene1111
3c21baf876 fixed create shopping list from recipe 2020-10-15 21:26:24 +02:00
vabene1111
2d2c38517c fixed null units breaking shopping lists 2020-10-15 21:19:49 +02:00
vabene1111
163b259bd1 fixed migrations for postgres 2020-10-14 22:05:49 +02:00
dependabot[bot]
24ced66c69 Bump bleach from 3.1.5 to 3.2.1
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.5 to 3.2.1.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.5...v3.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-30 06:25:32 +00:00
dependabot[bot]
6c1982cccb Bump simplejson from 3.17.0 to 3.17.2
Bumps [simplejson](https://github.com/simplejson/simplejson) from 3.17.0 to 3.17.2.
- [Release notes](https://github.com/simplejson/simplejson/releases)
- [Changelog](https://github.com/simplejson/simplejson/blob/master/CHANGES.txt)
- [Commits](https://github.com/simplejson/simplejson/compare/v3.17.0...v3.17.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-30 06:25:31 +00:00
dependabot[bot]
3bae7283d1 Bump whitenoise from 5.1.0 to 5.2.0
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/evansd/whitenoise/releases)
- [Changelog](https://github.com/evansd/whitenoise/blob/master/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/v5.1.0...v5.2.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-30 06:25:31 +00:00
vabene1111
0b458f7565 update source file as well 2020-09-29 19:44:46 +02:00
vabene1111
675f30126c updated base translation files 2020-09-29 19:38:58 +02:00
vabene1111
25b051323c shopping list basic sorting 2020-09-29 18:18:43 +02:00
vabene1111
697de3d9fc small mobile layout improvements 2020-09-29 14:19:17 +02:00
vabene1111
7bc09dfe89 finishes shopping lists 2020-09-29 14:15:18 +02:00
vabene1111
711dfbe55f cleanup import 2020-09-29 13:19:06 +02:00
vabene1111
76108c66c6 Merge pull request #189 from vabene1111/dependabot/pip/drf-writable-nested-0.6.1
Bump drf-writable-nested from 0.6.0 to 0.6.1
2020-09-29 12:56:31 +02:00
dependabot[bot]
db3c390d03 Bump drf-writable-nested from 0.6.0 to 0.6.1
Bumps [drf-writable-nested](https://github.com/beda-software/drf-writable-nested) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/beda-software/drf-writable-nested/releases)
- [Changelog](https://github.com/beda-software/drf-writable-nested/blob/master/CHANGELOG.md)
- [Commits](https://github.com/beda-software/drf-writable-nested/compare/v0.6.0...v0.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-29 10:56:26 +00:00
vabene1111
138fb14107 Merge pull request #188 from vabene1111/dependabot/pip/django-3.1.1
Bump django from 3.0.7 to 3.1.1
2020-09-29 12:56:19 +02:00
dependabot[bot]
17ebdd7711 Bump django from 3.0.7 to 3.1.1
Bumps [django](https://github.com/django/django) from 3.0.7 to 3.1.1.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.7...3.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-29 10:56:07 +00:00
vabene1111
fc9a42029a Merge pull request #187 from vabene1111/dependabot/pip/beautifulsoup4-4.9.2
Bump beautifulsoup4 from 4.9.1 to 4.9.2
2020-09-29 12:55:35 +02:00
vabene1111
7d942d551a Merge pull request #186 from vabene1111/dependabot/pip/django-filter-2.4.0
Bump django-filter from 2.2.0 to 2.4.0
2020-09-29 12:55:25 +02:00
vabene1111
78c94f2b64 Merge pull request #185 from vabene1111/dependabot/pip/bleach-whitelist-0.0.11
Bump bleach-whitelist from 0.0.10 to 0.0.11
2020-09-29 12:55:15 +02:00
dependabot[bot]
b317d7ba29 Bump beautifulsoup4 from 4.9.1 to 4.9.2
Bumps [beautifulsoup4](http://www.crummy.com/software/BeautifulSoup/bs4/) from 4.9.1 to 4.9.2.

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-29 10:48:54 +00:00
dependabot[bot]
71b8ddd1bf Bump django-filter from 2.2.0 to 2.4.0
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 2.2.0 to 2.4.0.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/master/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/2.2.0...2.4.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-29 10:48:52 +00:00
dependabot[bot]
23de4d4239 Bump bleach-whitelist from 0.0.10 to 0.0.11
Bumps [bleach-whitelist](https://github.com/yourcelf/bleach-whitelist) from 0.0.10 to 0.0.11.
- [Release notes](https://github.com/yourcelf/bleach-whitelist/releases)
- [Commits](https://github.com/yourcelf/bleach-whitelist/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-29 10:48:50 +00:00
vabene1111
4641b81f70 Create dependabot.yml 2020-09-29 12:48:28 +02:00
vabene1111
a9bad5e5f9 create shopping from mealplan 2020-09-29 12:41:59 +02:00
vabene1111
9f7106a325 shopping list fixes 2020-09-29 11:41:10 +02:00
vabene1111
73f13f56e1 added ability to display message to users (via admin) 2020-09-22 12:36:27 +02:00
vabene1111
312c364797 fixed wrongly changed permission check order 2020-09-22 12:19:30 +02:00
vabene1111
ad9b10c9c1 allow entry deletion 2020-09-22 12:17:22 +02:00
vabene1111
678cfaca12 fixed several shopping list issues 2020-09-22 12:01:11 +02:00
vabene1111
9b36f51d16 added .env examples 2020-09-22 09:42:58 +02:00
vabene1111
fa8389d783 Merge pull request #172 from stewartadam/bugfix/configurable-urls
Permit MEDIA_URL and STATIC_URL to be set from environment variables
2020-09-22 09:29:48 +02:00
vabene1111
30d766be77 fixed clearing amount in recipe edit would result in error 2020-09-22 09:09:52 +02:00
vabene1111
5e2dba7b04 added basic exporting 2020-09-22 00:32:42 +02:00
vabene1111
70df7c5307 improved autosync data efficency 2020-09-22 00:20:44 +02:00
vabene1111
f91d9fcfe2 autosync shopping list and settings 2020-09-21 23:54:46 +02:00
vabene1111
086a4aea47 basics of shopping list working 2020-09-21 23:05:58 +02:00
vabene1111
148ce2faef shopping list item checking 2020-09-21 22:55:52 +02:00
vabene1111
4827364e37 basic acceptable design 2020-09-21 22:16:19 +02:00
vabene1111
da958faf33 basic shopping list ui cleanup 2020-09-21 22:05:53 +02:00
vabene1111
f5117abcfb fixed shopping list multipliers and recipe names 2020-09-21 11:56:39 +02:00
vabene1111
df79c8f889 basic shopping list load and save 2020-09-15 16:51:20 +02:00
vabene1111
0ff65d35dc partial shopping list saving 2020-09-07 13:09:03 +02:00
vabene1111
8239dc3604 fixed unit creation typo 2020-09-07 12:27:29 +02:00
vabene1111
4a4d4b4486 shopping display seperation 2020-09-03 11:38:22 +02:00
vabene1111
34733a427f model __str__ methods 2020-09-01 21:37:33 +02:00
vabene1111
7f68bbd25d added link based signup 2020-09-01 21:35:37 +02:00
vabene1111
392ee73719 basics of invite link creation 2020-09-01 14:57:20 +02:00
vabene1111
2a0a85018a fixed import and validation errors 2020-09-01 13:26:58 +02:00
vabene1111
62868cd2b2 added note support for recipe import 2020-09-01 11:49:19 +02:00
vabene1111
4e92be3bbc fixed scrolling issue in internal recipe edit
related to bootstrap vue issue, waiting on proper fix
2020-09-01 11:39:46 +02:00
vabene1111
14c94bf7ab WIP shopping list 2020-09-01 11:31:29 +02:00
Stewart Adam
ce3148ac89 Permit MEDIA_URL and STATIC_URL to be set from environment variables (#143) 2020-08-30 16:39:43 -07:00
tourn
652b4bf2af Add serving count to recipe 2020-08-30 16:00:01 +02:00
vabene1111
bc39b53aad fixed typo 2020-08-27 10:06:53 +02:00
vabene1111
984192e479 basics of scaling 2020-08-26 21:41:04 +02:00
vabene1111
3c73b084cf super basic shopping list working 2020-08-26 21:11:20 +02:00
vabene1111
fc073124d4 Merge pull request #162 from LBBO/amount-without-unit
Show amounts even when unit is empty
2020-08-26 20:33:18 +02:00
Michael Kuckuk
f6fb07926e Show amounts even when unit is empty 2020-08-26 12:39:21 +02:00
vabene1111
90dddd34f3 removed test for invalid recipe as its no longer invalid due to parser improvements 2020-08-26 11:46:56 +02:00
vabene1111
0b948618f3 improved website parser 2020-08-26 11:37:59 +02:00
vabene1111
78be002134 Merge pull request #154 from Mwoua/develop
Instructions for manual installation
2020-08-21 18:09:20 +02:00
Mwoua
7acd72ff3a Clone master instead of getting release 2020-08-21 09:12:57 -04:00
Mwoua
c5edeb7e8f Missing alias for media files 2020-08-19 18:09:23 -04:00
David Lévy
5d5c5a8597 Instructions for manual installation 2020-08-19 17:32:28 -04:00
David Lévy
03bdcdf9b4 Fix markdown rules 2020-08-18 16:32:00 -04:00
vabene1111
16d755fd76 Merge pull request #150 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_fr
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'fr'
2020-08-12 11:24:17 +02:00
transifex-integration[bot]
587426e3d3 Apply translations in fr
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'fr' language.
2020-08-11 15:26:15 +00:00
vabene1111
be55e034bf first parts of shopping rework 2020-08-11 15:24:12 +02:00
vabene1111
8055754455 shopping list basics 2020-08-11 12:17:12 +02:00
vabene1111
82497c734a uniform button style in recipe view 2020-08-09 20:55:02 +02:00
vabene1111
b39a55ee94 removed mistakingly added language 2020-08-09 20:49:16 +02:00
vabene1111
9d837cd633 added contributers 2020-08-09 20:44:21 +02:00
vabene1111
968206a7ab compiled translations 2020-08-09 20:40:55 +02:00
vabene1111
a769fe6906 Merge pull request #148 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_fr_FR
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'fr_FR' [manual sync]
2020-08-09 20:35:05 +02:00
vabene1111
21cf4c5d70 Merge pull request #149 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_de
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'de' [manual sync]
2020-08-09 20:34:53 +02:00
vabene1111
6c02912dad Merge pull request #147 from vabene1111/translations_cookbook-locale-en-lc-messages-django-po--develop_nl
Translate '/cookbook/locale/en/LC_MESSAGES/django.po' in 'nl' [manual sync]
2020-08-09 20:34:26 +02:00
transifex-integration[bot]
c0756d87a6 Apply translations in de
at least 95% translated for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'de' language.

 Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format
2020-08-09 18:33:53 +00:00
transifex-integration[bot]
2471c7982d Apply translations in fr_FR
at least 95% translated for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'fr_FR' language.

 Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format
2020-08-09 18:33:48 +00:00
transifex-integration[bot]
b36e440920 Apply translations in nl
at least 95% translated for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'nl' language.

 Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format
2020-08-09 18:33:43 +00:00
vabene1111
a8b1ee9765 changed url import api call to post url 2020-08-09 20:13:05 +02:00
vabene1111
782d276724 cleanup ids in import as well 2020-07-20 21:12:18 +02:00
vabene1111
9510562576 backup basic fixture 2020-07-15 22:05:48 +02:00
vabene1111
363a4b6ff7 fixed sys page broken html tags in german locale 2020-07-15 21:52:31 +02:00
155 changed files with 24190 additions and 4130 deletions

View File

@@ -14,5 +14,4 @@ LICENSE
.idea
LICENSE.md
docs
nginx
update.sh

View File

@@ -8,28 +8,45 @@ ALLOWED_HOSTS=*
# random secret key, use for example base64 /dev/urandom | head -c50 to generate one
SECRET_KEY=
# your default timezone
TIMEZONE=Europe/Berlin
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
DB_ENGINE=django.db.backends.postgresql_psycopg2
DB_ENGINE=django.db.backends.postgresql
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangodb
POSTGRES_PASSWORD=
POSTGRES_DB=djangodb
# the default value for the user preference 'fractions' (enable/disable fraction support)
# when unset: 0 (disabled)
FRACTION_PREF_DEFAULT=0
# the default value for the user preference 'comments' (enable/disable commenting system)
# when unset: 1 (true)
COMMENT_PREF_DEFAULT=1
# Users can set a amount of time after which the shopping list is refreshed when they are in viewing mode
# This is the minimum interval users can set. Setting this to low will allow users to refresh very frequently which
# might cause high load on the server. (Technically they can obviously refresh as often as they want with their own scripts)
SHOPPING_MIN_AUTOSYNC_INTERVAL=5
# If staticfiles are stored at a different location uncomment and change accordingly
# STATIC_URL=/static/
# If mediafiles are stored at a different location uncomment and change accordingly
# MEDIA_URL=/media/
# Serve mediafiles directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
# provided that include an additional nxginx container to handle media file serving.
# If you know what you are doing turn this back on (1) to serve media files using djangos serve() method.
# when unset: 1 (true) - this is temporary until an appropriate amount of time has passed for everyone to migrate
GUNICORN_MEDIA=0
# allow authentication via reverse proxy (e.g. authelia), leave of if you dont know what you are doing
# docs: https://github.com/vabene1111/recipes/tree/develop/docs/docker/nginx-proxy%20with%20proxy%20authentication
# when unset: 0 (false)
REVERSE_PROXY_AUTH=0
# the default value for the user preference 'comments' (enable/disable commenting system)
# when unset: 1 (true)
COMMENT_PREF_DEFAULT=1

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View File

@@ -11,7 +11,7 @@ jobs:
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master#
uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}

17
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Make Docs
on:
push:
branches:
- master
- develop
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -1,6 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="vabene1111-PC">
<words>
<w>autosync</w>
<w>csrftoken</w>
<w>gunicorn</w>
<w>ical</w>

63
CONTRIBUTERS.md Normal file
View File

@@ -0,0 +1,63 @@
Many thanks to everyone who contributed to this project! If you add something or help out feel free to add yourself
to this list.
## Code/Features
Please have a look at the [list of pull requests](https://github.com/vabene1111/recipes/pulls) for
a complete list of contributions.
Below are some of the larger contributions made yet.
- @tourn provided the serving feature and **several** other improvements!
- @l0c4lh057 provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277)
- @sebimarkgraf added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199)
- @cazier added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88)
## Translations
### Catalan
[Rubenix](https://www.transifex.com/user/profile/rubenix/)
### Dutch
[D0T1X](https://www.transifex.com/user/profile/D0T1X/)
[ikbenfrank](https://www.transifex.com/user/profile/ikbenfrank/)
[kampsj](https://www.transifex.com/user/profile/kampsj/)
### French
[jt117](https://www.transifex.com/user/profile/jt117/)
[nerdinator](https://www.transifex.com/user/profile/nerdinator/)
[agaume](https://www.transifex.com/user/profile/agaume/)
### German
[eTaurus](https://www.transifex.com/user/profile/eTaurus/)
[l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/)
### Hungarian
[igazka](https://www.transifex.com/user/profile/igazka/)
### Italian
[SK3LA](https://www.transifex.com/user/profile/SK3LA/)
[auanasgheps](https://www.transifex.com/user/profile/auanasgheps/)
### Latvian
[melkypie](https://github.com/melkypie)
### Portuguese
[hds](https://www.transifex.com/user/profile/hds/)
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
[stormsz](https://www.transifex.com/user/profile/stormsz/)
### Spanish
[albertocp](https://www.transifex.com/user/profile/albertocp/)
[alfa5](https://www.transifex.com/user/profile/alfa5/)
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
[sergio.laya](https://www.transifex.com/user/profile/sergio.laya/)
### Turkish
[batmanisnaked](https://www.transifex.com/user/profile/batmanisnaked/)
### Vietnamese
[vuongtrunghieu](https://www.transifex.com/user/profile/vuongtrunghieu/)

View File

@@ -1,81 +1,82 @@
# Recipes ![CI](https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop)
Recipes is a Django application to manage, tag and search recipes using either built in models or external storage providers hosting PDF's, Images or other files.
# Recipes
![CI](https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop)
![Stars](https://img.shields.io/github/stars/vabene1111/recipes)
![Forks](https://img.shields.io/github/forks/vabene1111/recipes)
![Docker Pulls](https://img.shields.io/docker/pulls/vabene1111/recipes)
Recipes is a Django application to manage, tag and search recipes using either built in models or
external storage providers hosting PDF's, Images or other files.
![Preview](docs/preview.png)
[More Screenshots](https://imgur.com/a/V01151p)
### Features
## Features
- :package: **Sync** files with Dropbox and Nextcloud (more can easily be added)
- :mag: Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- :label: Create and search for **tags**, assign them in batch to all files matching certain filters
- :page_facing_up: **Create recipes** locally within a nice, standardized web interface
- :page_facing_up: **Create recipes** locally within a nice, standardized web interface
- :arrow_down: **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
- :iphone: Optimized for use on **mobile** devices like phones and tablets
- :shopping_cart: Generate **shopping** lists from recipes
- :calendar: Create a **Plan** on what to eat when
- :family: **Share** recipes with friends and comment on them to suggest or remember changes you made
- :heavy_division_sign: automatically convert decimal units to **fractions** for those who like this
- :whale: Easy setup with **Docker**
- :art: Customize your interface with **themes**
- :envelope: Export and import recipes from other users
- :earth_africa: localized in many languages thanks to the awesome community
- :heavy_plus_sign: Many more like recipe scaling, image compression, cookbooks, printing views, ...
This application is meant for people with a collection of recipes they want to share with family and friends or simply
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as a public page.
This application is meant for people with a collection of recipes they want to share with family and friends or simply
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as
a public page.
Some Documentation can be found [here](https://github.com/vabene1111/recipes/wiki)
# Installation
The docker image (`vabene1111/recipes`) simply exposes the application on port `8080`. You may choose any preferred installation method, the following are just examples to make it easier.
While this application has been around for a while and is actively used by many (including myself) it is still considered
**beta** software that has a lot of rough edges and unpolished parts.
### Docker-Compose
2. Choose one of the included configurations [here](docs/docker).
2. Download the environment (config) file template and fill it out `wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env `
3. Start the container `docker-compose up -d`
4. Open the page to create the first user. Alternatively use `docker-compose exec web_recipes createsuperuser`
### Manual
**Python >= 3.8** is required to run this!
Copy `.env.template` to `.env` and fill in the missing values accordingly.
Make sure all variables are available to whatever serves your application.
Otherwise simply follow the instructions for any django based deployment
(for example [this one](http://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html)).
## Updating
While intermediate updates can be skipped when updating please make sure to **read the release notes** in case some special action is required to update.
0. Before updating it is recommended to **create a backup!**
1. Stop the container using `docker-compose down`
2. Pull the latest image using `docker-compose pull`
3. Start the container again using `docker-compose up -d`
## Kubernetes
You can find a basic kubernetes setup [here](docs/k8s/). Please see the README in the folder for more detail.
## Installation
Please refer to the Installation section of the [Documentation](https://vabene1111.github.io/recipes/).
## Contributing
Pull Requests and ideas are welcome, feel free to contribute in any way.
For any questions on how to work with django please refer to their excellent [documentation](https://www.djangoproject.com/start/).
**If you want feel free to open an issue or pull request to add yourself to the list of awesome contributors.**
### Getting Started
This application is developed using the django framework for Python. They have excellent
[documentation](https://www.djangoproject.com/start/) on how to get started, so I will only give you the basics here
1. Clone this repository wherever you like and install the Python language for your OS (at least version 3.8)
2. Open it in your favorite editor/IDE (e.g. PyCharm)
1. if you want, create a virutal environment for all your packages.
3. Install all required packages by running `pip install -r requirements.txt`
4. Run the migrations `python manage.py migrate`
5. Start the development server `python manage.py runserver`
There is **no** need to set any environment variables. By default, a simple sqlite database is used and all settings are
populated from default values.
### Translating
There is a [transifex project](https://www.transifex.com/django-recipes/django-cookbook/) project to enable community driven translations. If you want to contribute a new language or help maintain an already existing one feel free to create a transifex account (using the link above) and request to join the project.
It is also possible to provide the translations directly by creating a new language using `manage.py makemessages -l <language_code> -i venv`. Once finished simply open a PR with the changed files.
## License
Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with an
[common clause](https://commonsclause.com/) selling exception. See [LICENSE.md](https://github.com/vabene1111/recipes/blob/develop/LICENSE.md) for details.
**Reasoning**
**This software and *all* its features are and will always be free for everyone to use and enjoy.**
#### This software and **all** its features are and will always be free for everyone to use and enjoy.
The reason for the selling exception is that a significant amount of time was spend over multiple years to develop this software.
The reason for the selling exception is that a significant amount of time was spend over multiple years to develop this software.
A payed hosted version which will be identical in features and code base to the software offered in this repository will
likely be released in the future (including all features needed to sell a hosted version as they might also be useful for personal use).
This will not only benefit me personally but also everyone who self-hosts this software as any profits made trough selling the hosted option
This will not only benefit me personally but also everyone who self-hosts this software as any profits made trough selling the hosted option
allow me to spend more time developing and improving the software for everyone. Selling exceptions are [approved by Richard Stallman](http://www.gnu.org/philosophy/selling-exceptions.en.html) and the
common clause license is very permissive (see the [FAQ](https://commonsclause.com/)).
common clause license is very permissive (see the [FAQ](https://commonsclause.com/)).

10
SECURITY.md Normal file
View File

@@ -0,0 +1,10 @@
# Security Policy
## Supported Versions
Since this software is still considered beta/WIP support is always only given for the latest version. There are no backports of security or any other fixes.
## Reporting a Vulnerability
Please open a normal public issue if you have any security related concerns. If you feel like the issue should not be discussed in
public just open a generic issue and we will discuss further communitcation there (since GitHub does not allow everyone to create a security advisory :/).

View File

@@ -2,6 +2,13 @@ from django.contrib import admin
from .models import *
class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'message')
admin.site.register(Space, SpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style', 'comments')
@@ -125,6 +132,13 @@ class ViewLogAdmin(admin.ModelAdmin):
admin.site.register(ViewLog, ViewLogAdmin)
class InviteLinkAdmin(admin.ModelAdmin):
list_display = ('username', 'group', 'valid_until', 'created_by', 'created_at', 'used_by')
admin.site.register(InviteLink, InviteLinkAdmin)
class CookLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
@@ -132,8 +146,36 @@ class CookLogAdmin(admin.ModelAdmin):
admin.site.register(CookLog, CookLogAdmin)
class ShoppingListRecipeAdmin(admin.ModelAdmin):
list_display = ('id', 'recipe', 'servings')
admin.site.register(ShoppingListRecipe, ShoppingListRecipeAdmin)
class ShoppingListEntryAdmin(admin.ModelAdmin):
list_display = ('id', 'food', 'unit', 'list_recipe', 'checked')
admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin)
class ShoppingListAdmin(admin.ModelAdmin):
list_display = ('id', 'created_by', 'created_at')
admin.site.register(ShoppingList, ShoppingListAdmin)
class ShareLinkAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'uuid', 'created_at',)
admin.site.register(ShareLink, ShareLinkAdmin)
class NutritionInformationAdmin(admin.ModelAdmin):
list_display = ('id',)
admin.site.register(NutritionInformation, NutritionInformationAdmin)

View File

@@ -2,7 +2,7 @@ import django_filters
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from cookbook.forms import MultiSelectWidget
from cookbook.models import Recipe, Keyword, Food
from cookbook.models import Recipe, Keyword, Food, ShoppingList
from django.conf import settings
from django.utils.translation import gettext as _
@@ -52,3 +52,16 @@ class IngredientFilter(django_filters.FilterSet):
class Meta:
model = Food
fields = ['name']
class ShoppingListFilter(django_filters.FilterSet):
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
data.setdefault("finished", False)
super(ShoppingListFilter, self).__init__(data, *args, **kwargs)
class Meta:
model = ShoppingList
fields = ['finished']

View File

@@ -31,15 +31,19 @@ class UserPreferenceForm(forms.ModelForm):
class Meta:
model = UserPreference
fields = ('default_unit', 'theme', 'nav_color', 'default_page', 'show_recent', 'search_style', 'plan_share', 'ingredient_decimals', 'comments')
fields = ('default_unit', 'use_fractions', 'theme', 'nav_color', 'default_page', 'show_recent', 'search_style', 'plan_share', 'ingredient_decimals', 'shopping_auto_sync', 'comments')
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'plan_share': _('Default user to share newly created meal plan entries with.'),
'use_fractions': _('Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
'plan_share': _('Users with whom newly created meal plan/shopping list entries should be shared by default.'),
'show_recent': _('Show recently viewed recipes on search page.'),
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.')
'comments': _('If you want to be able to create and see comments underneath recipes.'),
'shopping_auto_sync': _(
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. 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.')
}
widgets = {
@@ -84,13 +88,14 @@ class InternalRecipeForm(forms.ModelForm):
class Meta:
model = Recipe
fields = ('name', 'image', 'working_time', 'waiting_time', 'keywords')
fields = ('name', 'image', 'working_time', 'waiting_time', 'servings', 'keywords')
labels = {
'name': _('Name'),
'keywords': _('Keywords'),
'working_time': _('Preparation time in minutes'),
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
'servings': _('Number of servings'),
}
widgets = {'keywords': MultiSelectWidget}
@@ -261,7 +266,7 @@ class MealPlanForm(forms.ModelForm):
class Meta:
model = MealPlan
fields = ('recipe', 'title', 'meal_type', 'note', 'date', 'shared')
fields = ('recipe', 'title', 'meal_type', 'note', 'servings', 'date', 'shared')
help_texts = {
'shared': _('You can list default users to share recipes with in the settings.'),
@@ -271,7 +276,16 @@ class MealPlanForm(forms.ModelForm):
widgets = {'recipe': SelectWidget, 'date': DateWidget, 'shared': MultiSelectWidget}
class SuperUserForm(forms.Form):
name = forms.CharField()
class InviteLinkForm(forms.ModelForm):
class Meta:
model = InviteLink
fields = ('username', 'group', 'valid_until')
help_texts = {
'username': _('A username is not required, if left blank the new user can choose one.')
}
class UserCreateForm(forms.Form):
name = forms.CharField(label='Username')
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))
password_confirm = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))

View File

@@ -0,0 +1,131 @@
import unicodedata
import string
def parse_fraction(x):
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
frac_split = unicodedata.decomposition(x[-1:]).split()
return float((frac_split[1]).replace('003', '')) / float((frac_split[3]).replace('003', ''))
else:
frac_split = x.split('/')
if not len(frac_split) == 2:
raise ValueError
try:
return int(frac_split[0]) / int(frac_split[1])
except ZeroDivisionError:
raise ValueError
def parse_amount(x):
amount = 0
unit = ''
did_check_frac = False
end = 0
while end < len(x) and (x[end] in string.digits or ((x[end] == '.' or x[end] == ',') and end + 1 < len(x) and x[end+1] in string.digits)):
end += 1
if end > 0:
amount = float(x[:end].replace(',', '.'))
else:
amount = parse_fraction(x[0])
end += 1
did_check_frac = True
if end < len(x):
if did_check_frac:
unit = x[end:]
else:
try:
amount += parse_fraction(x[end])
unit = x[end+1:]
except ValueError:
unit = x[end:]
return amount, unit
def parse_ingredient_with_comma(tokens):
ingredient = ''
note = ''
start = 0
# search for first occurence of an argument ending in a comma
while start < len(tokens) and not tokens[start].endswith(','):
start += 1
if start == len(tokens):
# no token ending in a comma found -> use everything as ingredient
ingredient = ' '.join(tokens)
else:
ingredient = ' '.join(tokens[:start+1])[:-1]
note = ' '.join(tokens[start+1:])
return ingredient, note
def parse_ingredient(tokens):
ingredient = ''
note = ''
if tokens[-1].endswith(')'):
# last argument ends with closing bracket -> look for opening bracket
start = len(tokens) - 1
while not tokens[start].startswith('(') and not start == 0:
start -= 1
if start == 0:
# the whole list is wrapped in brackets -> assume it is an error (e.g. assumed first argument was the unit)
raise ValueError
elif start < 0:
# no opening bracket anywhere -> just ignore the last bracket
ingredient, note = parse_ingredient_with_comma(tokens)
else:
# opening bracket found -> split in ingredient and note, remove brackets from note
note = ' '.join(tokens[start:])[1:-1]
ingredient = ' '.join(tokens[:start])
else:
ingredient, note = parse_ingredient_with_comma(tokens)
return ingredient, note
def parse(x):
# initialize default values
amount = 0
unit = ''
ingredient = ''
note = ''
tokens = x.split()
if len(tokens) == 1:
# there only is one argument, that must be the ingredient
ingredient = tokens[0]
else:
try:
# try to parse first argument as amount
amount, unit = parse_amount(tokens[0])
# only try to parse second argument as amount if there are at least three arguments
# if it already has a unit there can't be a fraction for the amount
if len(tokens) > 2:
try:
if not unit == '':
# a unit is already found, no need to try the second argument for a fraction
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
raise ValueError
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
amount += parse_fraction(tokens[1])
# assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','):
# try to use third argument as unit and everything else as ingredient, use everything as ingredient if it fails
try:
ingredient, note = parse_ingredient(tokens[3:])
unit = tokens[2]
except ValueError:
ingredient, note = parse_ingredient(tokens[2:])
else:
ingredient, note = parse_ingredient(tokens[2:])
except ValueError:
# assume that units can't end with a comma
if not tokens[1].endswith(','):
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails
try:
ingredient, note = parse_ingredient(tokens[2:])
unit = tokens[1]
except ValueError:
ingredient, note = parse_ingredient(tokens[1:])
else:
ingredient, note = parse_ingredient(tokens[1:])
else:
# only two arguments, first one is the amount which means this is the ingredient
ingredient = tokens[1]
except ValueError:
# can't parse first argument as amount -> no unit -> parse everything as ingredient
ingredient, note = parse_ingredient(tokens)
return amount, unit.strip(), ingredient.strip(), note.strip()

View File

@@ -67,9 +67,28 @@ def is_object_owner(user, obj):
return owner == user
if owner := getattr(obj, 'user', None):
return owner == user
if getattr(obj, 'get_owner', None):
return obj.get_owner() == user
return False
def is_object_shared(user, obj):
"""
Tests if a given user is shared for a given object
test performed by checking user against the objects shared table
superusers bypass all checks, unauthenticated users cannot own anything
:param user django auth user object
:param obj any object that should be tested
:return: true if user is shared for object, false otherwise
"""
# TODO this could be improved/cleaned up by adding share checks for relevant objects
if not user.is_authenticated:
return False
if user.is_superuser:
return True
return user in obj.shared.all()
def share_link_valid(recipe, share):
"""
Verifies the validity of a share uuid
@@ -122,7 +141,7 @@ class OwnerRequiredMixin(object):
return HttpResponseRedirect(reverse_lazy('login'))
else:
if not is_object_owner(request.user, self.get_object()):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
@@ -136,7 +155,7 @@ class CustomIsOwner(permissions.BasePermission):
verifies user has ownership over object
(either user or created_by or user is request user)
"""
message = _('You cannot interact with this object as its not owned by you!')
message = _('You cannot interact with this object as it is not owned by you!')
def has_permission(self, request, view):
return request.user.is_authenticated
@@ -145,6 +164,20 @@ class CustomIsOwner(permissions.BasePermission):
return is_object_owner(request.user, obj)
class CustomIsShared(permissions.BasePermission): # TODO function duplicate/too similar name
"""
Custom permission class for django rest framework views
verifies user is shared for the object he is trying to access
"""
message = _('You cannot interact with this object as it is not owned by you!')
def has_permission(self, request, view):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return is_object_shared(request.user, obj)
class CustomIsGuest(permissions.BasePermission):
"""
Custom permission class for django rest framework views

View File

@@ -1,6 +1,7 @@
import json
import random
import re
import unicodedata
from json import JSONDecodeError
import microdata
@@ -10,6 +11,7 @@ from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from cookbook.models import Keyword
from cookbook.helper.ingredient_parser import parse as parse_ingredient
def get_from_html(html_text, url):
@@ -18,7 +20,7 @@ def get_from_html(html_text, url):
# first try finding ld+json as its most common
for ld in soup.find_all('script', type='application/ld+json'):
try:
ld_json = json.loads(ld.string)
ld_json = json.loads(ld.string.replace('\n', ''))
if type(ld_json) != list:
ld_json = [ld_json]
@@ -31,8 +33,8 @@ def get_from_html(html_text, url):
if '@type' in ld_json_item and ld_json_item['@type'] == 'Recipe':
return find_recipe_json(ld_json_item, url)
except JSONDecodeError:
JsonResponse({'error': True, 'msg': _('The requested site provided malformed data and cannot be read.')}, status=400)
except JSONDecodeError as e:
return JsonResponse({'error': True, 'msg': _('The requested site provided malformed data and cannot be read.')}, status=400)
# now try to find microdata
items = microdata.get_items(html_text)
@@ -69,31 +71,12 @@ def find_recipe_json(ld_json, url):
ingredients = []
for x in ld_json['recipeIngredient']:
ingredient_split = x.split()
ingredient = None
amount = 0
unit = ''
if len(ingredient_split) > 2:
ingredient = " ".join(ingredient_split[2:])
unit = ingredient_split[1]
try:
amount = float(ingredient_split[0].replace(',', '.'))
except ValueError:
amount = 0
ingredient = " ".join(ingredient_split)
if len(ingredient_split) == 2:
ingredient = " ".join(ingredient_split[1:])
unit = ''
try:
amount = float(ingredient_split[0].replace(',', '.'))
except ValueError:
amount = 0
ingredient = " ".join(ingredient_split)
if len(ingredient_split) == 1:
ingredient = " ".join(ingredient_split)
if ingredient:
ingredients.append({'amount': amount, 'unit': {'text': unit, 'id': random.randrange(10000, 99999)}, 'ingredient': {'text': ingredient, 'id': random.randrange(10000, 99999)}, 'original': x})
try:
amount, unit, ingredient, note = parse_ingredient(x)
if ingredient:
ingredients.append({'amount': amount, 'unit': {'text': unit, 'id': random.randrange(10000, 99999)}, 'ingredient': {'text': ingredient, 'id': random.randrange(10000, 99999)}, "note": note, 'original': x})
except:
pass
ld_json['recipeIngredient'] = ingredients
else:
@@ -149,7 +132,7 @@ def find_recipe_json(ld_json, url):
else:
ld_json['recipeInstructions'] = ''
ld_json['recipeInstructions'] += '\n\n' + _('Imported from ') + url
ld_json['recipeInstructions'] += '\n\n' + _('Imported from') + ' ' + url
if 'image' in ld_json:
# check if list of images is returned, take first if so

Binary file not shown.

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
# Generated by Django 3.0.7 on 2020-08-11 10:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0074_remove_keyword_created_by'),
]
operations = [
migrations.CreateModel(
name='ShoppingListRecipe',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('multiplier', models.IntegerField(default=1)),
('recipe', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
],
),
migrations.CreateModel(
name='ShoppingListEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.IntegerField(default=1)),
('order', models.IntegerField(default=0)),
('checked', models.BooleanField(default=False)),
('food', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Food')),
('list_recipe', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.ShoppingListRecipe')),
('unit', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.Unit')),
],
),
migrations.CreateModel(
name='ShoppingList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4)),
('note', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('recipes', models.ManyToManyField(blank=True, to='cookbook.ShoppingListRecipe')),
('shared', models.ManyToManyField(blank=True, related_name='list_share', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-08-26 18:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0075_shoppinglist_shoppinglistentry_shoppinglistrecipe'),
]
operations = [
migrations.AddField(
model_name='shoppinglist',
name='entries',
field=models.ManyToManyField(blank=True, to='cookbook.ShoppingListEntry'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.0.7 on 2020-09-01 11:31
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0076_shoppinglist_entries'),
]
operations = [
migrations.CreateModel(
name='InviteLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4)),
('username', models.CharField(blank=True, max_length=64)),
('valid_until', models.DateField(default=datetime.date(2020, 9, 15))),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-09-01 11:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0077_invitelink'),
]
operations = [
migrations.AddField(
model_name='invitelink',
name='used_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-09-01 12:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('auth', '0011_update_proxy_permissions'),
('cookbook', '0078_invitelink_used_by'),
]
operations = [
migrations.AddField(
model_name='invitelink',
name='group',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-09-21 21:31
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0079_invitelink_group'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='shopping_auto_sync',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='invitelink',
name='valid_until',
field=models.DateField(default=datetime.date(2020, 10, 5)),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-09-21 21:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0080_auto_20200921_2331'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='shopping_auto_sync',
field=models.IntegerField(default=5),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-09-22 09:43
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0081_auto_20200921_2349'),
]
operations = [
migrations.AlterField(
model_name='invitelink',
name='valid_until',
field=models.DateField(default=datetime.date(2020, 10, 6)),
),
migrations.AlterField(
model_name='shoppinglistentry',
name='amount',
field=models.DecimalField(decimal_places=16, default=0, max_digits=32),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-09-22 10:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0082_auto_20200922_1143'),
]
operations = [
migrations.CreateModel(
name='Space',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='Default', max_length=128)),
('message', models.CharField(default='', max_length=512)),
],
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-09-22 10:33
from django.db import migrations
def create_default_space(apps, schema_editor):
Space = apps.get_model('cookbook', 'Space')
Space.objects.create(
name='Default',
message=''
)
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0083_space'),
]
operations = [
migrations.RunPython(create_default_space),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-09-22 10:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0084_auto_20200922_1233'),
]
operations = [
migrations.AlterField(
model_name='space',
name='message',
field=models.CharField(blank=True, default='', max_length=512),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-09-29 09:43
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0085_auto_20200922_1235'),
]
operations = [
migrations.AddField(
model_name='mealplan',
name='recipe_multiplier',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='invitelink',
name='valid_until',
field=models.DateField(default=datetime.date(2020, 10, 13)),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2020-09-29 09:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0086_auto_20200929_1143'),
]
operations = [
migrations.AlterField(
model_name='mealplan',
name='recipe_multiplier',
field=models.DecimalField(decimal_places=4, default=1, max_digits=8),
),
migrations.AlterField(
model_name='shoppinglistrecipe',
name='multiplier',
field=models.DecimalField(decimal_places=4, default=1, max_digits=8),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.1 on 2020-09-29 11:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0087_auto_20200929_1152'),
]
operations = [
migrations.AddField(
model_name='shoppinglist',
name='finished',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 3.1.1 on 2020-11-17 21:22
import datetime
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0088_shoppinglist_finished'),
]
operations = [
migrations.CreateModel(
name='NutritionInformation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fats', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
('carbohydrates', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
('proteins', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
('calories', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
('source', models.CharField(blank=True, default='', max_length=512, null=True)),
],
),
migrations.AlterField(
model_name='invitelink',
name='valid_until',
field=models.DateField(default=datetime.date(2020, 12, 1)),
),
migrations.AddField(
model_name='recipe',
name='nutrition',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.nutritioninformation'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.3 on 2020-12-14 12:59
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0089_auto_20201117_2222'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='use_fractions',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='invitelink',
name='valid_until',
field=models.DateField(default=datetime.date(2020, 12, 28)),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.4 on 2020-12-26 14:51
from django.db import migrations
def migrate_empty_units(apps, schema_editor):
Unit = apps.get_model('cookbook', 'Unit')
Ingredient = apps.get_model('cookbook', 'Ingredient')
empty_units = Unit.objects.filter(name='').all()
for x in empty_units:
for i in Ingredient.objects.all():
if i.unit == x:
i.unit = None
i.save()
x.delete()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0090_auto_20201214_1359'),
]
operations = [
migrations.RunPython(migrate_empty_units),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-08-30 13:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0091_auto_20201226_1551'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='servings',
field=models.IntegerField(default=1),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.4 on 2020-12-31 11:36
import datetime
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0092_recipe_servings'),
]
operations = [
migrations.RenameField(
model_name='mealplan',
old_name='recipe_multiplier',
new_name='servings',
),
migrations.AlterField(
model_name='invitelink',
name='valid_until',
field=models.DateField(default=datetime.date(2021, 1, 14)),
),
migrations.AlterField(
model_name='unit',
name='name',
field=models.CharField(max_length=128, unique=True, validators=[django.core.validators.MinLengthValidator(1)]),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-31 11:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0093_auto_20201231_1236'),
]
operations = [
migrations.RenameField(
model_name='shoppinglistrecipe',
old_name='multiplier',
new_name='servings',
),
]

View File

@@ -1,12 +1,16 @@
import re
import uuid
from datetime import date, timedelta
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from django.core.validators import MinLengthValidator
from django.utils.translation import gettext as _
from django.db import models
from django_random_queryset import RandomManager
from recipes.settings import COMMENT_PREF_DEFAULT
from recipes.settings import COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT
def get_user_name(self):
@@ -23,6 +27,11 @@ def get_model_name(model):
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
class Space(models.Model):
name = models.CharField(max_length=128, default='Default')
message = models.CharField(max_length=512, default='', blank=True)
class UserPreference(models.Model):
# Themes
BOOTSTRAP = 'BOOTSTRAP'
@@ -61,12 +70,14 @@ class UserPreference(models.Model):
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
default_unit = models.CharField(max_length=32, default='g')
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
search_style = models.CharField(choices=SEARCH_STYLE, max_length=64, default=LARGE)
show_recent = models.BooleanField(default=True)
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
ingredient_decimals = models.IntegerField(default=2)
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
shopping_auto_sync = models.IntegerField(default=5)
def __str__(self):
return str(self.user)
@@ -126,7 +137,7 @@ class Keyword(models.Model):
class Unit(models.Model):
name = models.CharField(unique=True, max_length=128)
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
def __str__(self):
@@ -173,8 +184,20 @@ class Step(models.Model):
ordering = ['order', 'pk']
class NutritionInformation(models.Model):
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
carbohydrates = models.DecimalField(default=0, decimal_places=16, max_digits=32)
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
source = models.CharField(max_length=512, default="", null=True, blank=True)
def __str__(self):
return f'Nutrition'
class Recipe(models.Model):
name = models.CharField(max_length=128)
servings = models.IntegerField(default=1)
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
file_uid = models.CharField(max_length=256, default="", blank=True)
@@ -186,10 +209,13 @@ class Recipe(models.Model):
working_time = models.IntegerField(default=0)
waiting_time = models.IntegerField(default=0)
internal = models.BooleanField(default=False)
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = RandomManager()
def __str__(self):
return self.name
@@ -246,6 +272,7 @@ class MealType(models.Model):
class MealPlan(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
title = models.CharField(max_length=64, blank=True, default='')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
@@ -265,12 +292,74 @@ class MealPlan(models.Model):
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
class ShoppingListRecipe(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}'
def get_owner(self):
try:
return self.shoppinglist_set.first().created_by
except AttributeError:
return None
class ShoppingListEntry(models.Model):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
food = models.ForeignKey(Food, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
order = models.IntegerField(default=0)
checked = models.BooleanField(default=False)
def __str__(self):
return f'Shopping list entry {self.id}'
def get_owner(self):
try:
return self.shoppinglist_set.first().created_by
except AttributeError:
return None
class ShoppingList(models.Model):
uuid = models.UUIDField(default=uuid.uuid4)
note = models.TextField(blank=True, null=True)
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
entries = models.ManyToManyField(ShoppingListEntry, blank=True)
shared = models.ManyToManyField(User, blank=True, related_name='list_share')
finished = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'Shopping list {self.id}'
class ShareLink(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.recipe} - {self.uuid}'
class InviteLink(models.Model):
uuid = models.UUIDField(default=uuid.uuid4)
username = models.CharField(blank=True, max_length=64)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
valid_until = models.DateField(default=date.today() + timedelta(days=14))
used_by = models.ForeignKey(User, null=True, on_delete=models.CASCADE, related_name='used_by')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.uuid}'
class CookLog(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)

View File

@@ -1,10 +1,12 @@
from decimal import Decimal
from django.contrib.auth.models import User
from drf_writable_nested import WritableNestedModelSerializer, UniqueFieldsMixin
from rest_framework import serializers
from rest_framework.exceptions import APIException, ValidationError
from rest_framework.fields import CurrentUserDefault
from rest_framework.exceptions import ValidationError
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Unit, Ingredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog, Food, Step
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Unit, Ingredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog, Food, Step, ShoppingList, \
ShoppingListEntry, ShoppingListRecipe, NutritionInformation
from cookbook.templatetags.custom_tags import markdown
@@ -14,19 +16,24 @@ class CustomDecimalField(serializers.Field):
"""
def to_representation(self, value):
return value.normalize()
if isinstance(value, Decimal):
return value.normalize()
else:
return Decimal(value).normalize()
def to_internal_value(self, data):
if type(data) == int or type(data) == float:
return data
elif type(data) == str:
if data == '':
return 0
try:
return float(data.replace(',', ''))
except ValueError:
raise ValidationError('A valid number is required')
class UserNameSerializer(serializers.ModelSerializer):
class UserNameSerializer(WritableNestedModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
def get_user_label(self, obj):
@@ -106,6 +113,9 @@ class FoodSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
obj, created = Food.objects.get_or_create(**validated_data)
return obj
def update(self, instance, validated_data):
return super(FoodSerializer, self).update(instance, validated_data)
class Meta:
model = Food
fields = ('id', 'name', 'recipe')
@@ -130,15 +140,26 @@ class StepSerializer(WritableNestedModelSerializer):
fields = ('id', 'name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
class NutritionInformationSerializer(serializers.ModelSerializer):
class Meta:
model = NutritionInformation
fields = ('carbohydrates', 'fats', 'proteins', 'calories', 'source')
class RecipeSerializer(WritableNestedModelSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True)
class Meta:
model = Recipe
fields = ['id', 'name', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'internal']
fields = ['id', 'name', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'internal', 'nutrition', 'servings']
read_only_fields = ['image', 'created_by', 'created_at']
def create(self, validated_data):
validated_data['created_by'] = self.context['request']._user
return super().create(validated_data)
class RecipeImageSerializer(WritableNestedModelSerializer):
class Meta:
@@ -181,13 +202,61 @@ class MealPlanSerializer(serializers.ModelSerializer):
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
note_markdown = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()
def get_note_markdown(self, obj):
return markdown(obj.note)
class Meta:
model = MealPlan
fields = ('id', 'title', 'recipe', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name')
fields = ('id', 'title', 'recipe', 'servings', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name')
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
recipe_name = serializers.ReadOnlyField(source='recipe.name')
servings = CustomDecimalField()
class Meta:
model = ShoppingListRecipe
fields = ('id', 'recipe', 'recipe_name', 'servings')
read_only_fields = ('id',)
class ShoppingListEntrySerializer(WritableNestedModelSerializer):
food = FoodSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True)
amount = CustomDecimalField()
class Meta:
model = ShoppingListEntry
fields = ('id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked')
read_only_fields = ('id',)
class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingListEntry
fields = ('id', 'checked')
class ShoppingListSerializer(WritableNestedModelSerializer):
recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
entries = ShoppingListEntrySerializer(many=True, allow_null=True)
shared = UserNameSerializer(many=True)
class Meta:
model = ShoppingList
fields = ('id', 'uuid', 'note', 'recipes', 'entries', 'shared', 'finished', 'created_by', 'created_at',)
read_only_fields = ('id',)
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
entries = ShoppingListEntryCheckedSerializer(many=True, allow_null=True)
class Meta:
model = ShoppingList
fields = ('id', 'entries',)
read_only_fields = ('id',)
class ShareLinkSerializer(serializers.ModelSerializer):

View File

@@ -0,0 +1,43 @@
/* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */
/*https://developer.aliyun.com/mirror/npm/package/frac/v/0.3.0 Apache license*/
var frac = function frac(x, D, mixed) {
var n1 = Math.floor(x), d1 = 1;
var n2 = n1+1, d2 = 1;
if(x !== n1) while(d1 <= D && d2 <= D) {
var m = (n1 + n2) / (d1 + d2);
if(x === m) {
if(d1 + d2 <= D) { d1+=d2; n1+=n2; d2=D+1; }
else if(d1 > d2) d2=D+1;
else d1=D+1;
break;
}
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
else { n1 = n1+n2; d1 = d1+d2; }
}
if(d1 > D) { d1 = d2; n1 = n2; }
if(!mixed) return [0, n1, d1];
var q = Math.floor(n1/d1);
return [q, n1 - q*d1, d1];
};
frac.cont = function cont(x, D, mixed) {
var sgn = x < 0 ? -1 : 1;
var B = x * sgn;
var P_2 = 0, P_1 = 1, P = 0;
var Q_2 = 1, Q_1 = 0, Q = 0;
var A = Math.floor(B);
while(Q_1 < D) {
A = Math.floor(B);
P = A * P_1 + P_2;
Q = A * Q_1 + Q_2;
if((B - A) < 0.00000005) break;
B = 1 / (B - A);
P_2 = P_1; P_1 = P;
Q_2 = Q_1; Q_1 = Q;
}
if(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } }
if(!mixed) return [0, sgn * P, Q];
var q = Math.floor(sgn * P/Q);
return [q, sgn*P - q*Q, Q];
};
// eslint-disable-next-line no-undef
if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_FRAC === 'undefined') module.exports = frac;

View File

@@ -0,0 +1,146 @@
/**
* Vue Cookies v1.7.4
* https://github.com/cmp-cc/vue-cookies
*
* Copyright 2016, cmp-cc
* Released under the MIT license
*/
(function () {
var defaultConfig = {
expires: '1d',
path: '; path=/',
domain: '',
secure: '',
sameSite: '; SameSite=Lax'
};
var VueCookies = {
// install of Vue
install: function (Vue) {
Vue.prototype.$cookies = this;
Vue.$cookies = this;
},
config: function (expireTimes, path, domain, secure, sameSite) {
defaultConfig.expires = expireTimes ? expireTimes : '1d';
defaultConfig.path = path ? '; path=' + path : '; path=/';
defaultConfig.domain = domain ? '; domain=' + domain : '';
defaultConfig.secure = secure ? '; Secure' : '';
defaultConfig.sameSite = sameSite ? '; SameSite=' + sameSite : '; SameSite=Lax';
},
get: function (key) {
var value = decodeURIComponent(document.cookie.replace(new RegExp('(?:(?:^|.*;)\\s*' + encodeURIComponent(key).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1')) || null;
if (value && value.substring(0, 1) === '{' && value.substring(value.length - 1, value.length) === '}') {
try {
value = JSON.parse(value);
} catch (e) {
return value;
}
}
return value;
},
set: function (key, value, expireTimes, path, domain, secure, sameSite) {
if (!key) {
throw new Error('Cookie name is not find in first argument.');
} else if (/^(?:expires|max\-age|path|domain|secure|SameSite)$/i.test(key)) {
throw new Error('Cookie key name illegality, Cannot be set to ["expires","max-age","path","domain","secure","SameSite"]\t current key name: ' + key);
}
// support json object
if (value && value.constructor === Object) {
value = JSON.stringify(value);
}
var _expires = '';
expireTimes = expireTimes == undefined ? defaultConfig.expires : expireTimes;
if (expireTimes && expireTimes != 0) {
switch (expireTimes.constructor) {
case Number:
if (expireTimes === Infinity || expireTimes === -1) _expires = '; expires=Fri, 31 Dec 9999 23:59:59 GMT';
else _expires = '; max-age=' + expireTimes;
break;
case String:
if (/^(?:\d+(y|m|d|h|min|s))$/i.test(expireTimes)) {
// get capture number group
var _expireTime = expireTimes.replace(/^(\d+)(?:y|m|d|h|min|s)$/i, '$1');
// get capture type group , to lower case
switch (expireTimes.replace(/^(?:\d+)(y|m|d|h|min|s)$/i, '$1').toLowerCase()) {
// Frequency sorting
case 'm':
_expires = '; max-age=' + +_expireTime * 2592000;
break; // 60 * 60 * 24 * 30
case 'd':
_expires = '; max-age=' + +_expireTime * 86400;
break; // 60 * 60 * 24
case 'h':
_expires = '; max-age=' + +_expireTime * 3600;
break; // 60 * 60
case 'min':
_expires = '; max-age=' + +_expireTime * 60;
break; // 60
case 's':
_expires = '; max-age=' + _expireTime;
break;
case 'y':
_expires = '; max-age=' + +_expireTime * 31104000;
break; // 60 * 60 * 24 * 30 * 12
default:
new Error('unknown exception of "set operation"');
}
} else {
_expires = '; expires=' + expireTimes;
}
break;
case Date:
_expires = '; expires=' + expireTimes.toUTCString();
break;
}
}
document.cookie =
encodeURIComponent(key) + '=' + encodeURIComponent(value) +
_expires +
(domain ? '; domain=' + domain : defaultConfig.domain) +
(path ? '; path=' + path : defaultConfig.path) +
(secure == undefined ? defaultConfig.secure : secure ? '; Secure' : '') +
(sameSite == undefined ? defaultConfig.sameSite : (sameSite ? '; SameSite=' + sameSite : ''));
return this;
},
remove: function (key, path, domain) {
if (!key || !this.isKey(key)) {
return false;
}
document.cookie = encodeURIComponent(key) +
'=; expires=Thu, 01 Jan 1970 00:00:00 GMT' +
(domain ? '; domain=' + domain : defaultConfig.domain) +
(path ? '; path=' + path : defaultConfig.path) +
'; SameSite=Lax';
return this;
},
isKey: function (key) {
return (new RegExp('(?:^|;\\s*)' + encodeURIComponent(key).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=')).test(document.cookie);
},
keys: function () {
if (!document.cookie) return [];
var _keys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, '').split(/\s*(?:\=[^;]*)?;\s*/);
for (var _index = 0; _index < _keys.length; _index++) {
_keys[_index] = decodeURIComponent(_keys[_index]);
}
return _keys;
}
};
if (typeof exports == 'object') {
module.exports = VueCookies;
} else if (typeof define == 'function' && define.amd) {
define([], function () {
return VueCookies;
});
} else if (window.Vue) {
Vue.use(VueCookies);
}
// vue-cookies can exist independently,no dependencies library
if (typeof window !== 'undefined') {
window.$cookies = VueCookies;
}
})();

View File

@@ -108,6 +108,25 @@ class RecipeImportTable(tables.Table):
fields = ('id', 'name', 'file_path')
class ShoppingListTable(tables.Table):
id = tables.LinkColumn('view_shopping', args=[A('id')])
class Meta:
model = ShoppingList
template_name = 'generic/table_template.html'
fields = ('id', 'finished', 'created_by', 'created_at')
class InviteLinkTable(tables.Table):
link = tables.TemplateColumn("<a href='{% url 'view_signup' record.uuid %}' >" + _('Link') + "</a>")
delete = tables.TemplateColumn("<a href='{% url 'delete_invite_link' record.id %}' >" + _('Delete') + "</a>")
class Meta:
model = InviteLink
template_name = 'generic/table_template.html'
fields = ('username', 'group', 'valid_until', 'created_by', 'created_at')
class ViewLogTable(tables.Table):
recipe = tables.LinkColumn('view_recipe', args=[A('recipe_id')])

View File

@@ -1,6 +1,7 @@
{% load static %}
{% load i18n %}
{% load theming_tags %}
{% load custom_tags %}
<html>
<head>
@@ -72,7 +73,7 @@
<a class="dropdown-item" href="{% url 'view_plan' %}"><i
class="fas fa-calendar fa-fw"></i> {% trans 'Meal-Plan' %}
</a>
<a class="dropdown-item" href="{% url 'view_shopping' %}"><i
<a class="dropdown-item" href="{% url 'list_shopping_list' %}"><i
class="fas fa-shopping-cart fa-fw"></i> {% trans 'Shopping' %}
</a>
<a class="dropdown-item" href="{% url 'list_food' %}"><i
@@ -83,7 +84,7 @@
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_keyword,data_batch_edit' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-tags"></i> {% trans 'Tags' %}
<i class="fas fa-tags"></i> {% trans 'Keywords' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'list_keyword' %}"><i
@@ -138,7 +139,7 @@
{% endif %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i
class="fab fa-markdown fa-fw"></i> {% trans 'Markdown Help' %}</a>
class="fab fa-markdown fa-fw"></i> {% trans 'Markdown Guide' %}</a>
<a class="dropdown-item" href="https://github.com/vabene1111/recipes"><i
class="fab fa-github fa-fw"></i> {% trans 'GitHub' %}</a>
<a class="dropdown-item" href="{% url 'docs_api' %}"><i
@@ -158,6 +159,14 @@
</ul>
</div>
</nav>
{% message_of_the_day as message_of_the_day %}
{% if message_of_the_day %}
<div class="bg-warning" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
{{ message_of_the_day }}
</div>
{% endif %}
<br/>
<br/>

View File

@@ -11,9 +11,9 @@
<div class="card border-info">
<div class="card-body text-info">
<p class="card-text">
{% trans 'On this Page you can manage all storage folder locations that should be monitored and synced' %}
{% trans 'On this Page you can manage all storage folder locations that should be monitored and synced.' %}
<br/>
{% trans 'The path must be in the following format' %} <code>/Folder/RecipesFolder</code>
{% trans 'The path must be in the following format' %}: <code>/Folder/RecipesFolder</code>
</p>
<form method="POST" class="post-form">{% csrf_token %}
{{ form|crispy }}

View File

@@ -56,12 +56,15 @@
<input type="file" @change="imageChanged">
</div>
<div class="col-md-6">
<label for="id_name"> {% trans 'Preperation Time' %}</label>
<label for="id_name"> {% trans 'Preparation Time' %}</label>
<input class="form-control" id="id_prep_time" v-model="recipe.working_time">
<br/>
<label for="id_name"> {% trans 'Waiting Time' %}</label>
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time">
<br/>
<label for="id_name"> {% trans 'Servings' %}</label>
<input class="form-control" id="id_servings" v-model="recipe.servings">
<br/>
<label for="id_name"> {% trans 'Keywords' %}</label>
<multiselect
v-model="recipe.keywords"
@@ -80,6 +83,35 @@
</multiselect>
</div>
</div>
<template v-if="recipe !== undefined">
<div class="row" v-if="recipe.nutrition" style="margin-top: 1vh">
<div class="col-md-12">
<div class="card border-grey">
<div class="card-body">
<h4 class="card-title">{% trans 'Nutrition' %}</h4>
<div class="dropdown-menu dropdown-menu-right"
aria-labelledby="dropdownMenuLink">
<button class="dropdown-item" @click="removeStep(step)"><i
class="fa fa-trash fa-fw"></i> {% trans 'Delete Step' %}</button>
</div>
<label for="id_name"> {% trans 'Calories' %}</label>
<input class="form-control" id="id_calories" v-model="recipe.nutrition.calories">
<label for="id_name"> {% trans 'Carbohydrates' %}</label>
<input class="form-control" id="id_carbohydrates" v-model="recipe.nutrition.carbohydrates">
<label for="id_name"> {% trans 'Fats' %}</label>
<input class="form-control" id="id_fats" v-model="recipe.nutrition.fats">
<label for="id_name"> {% trans 'Proteins' %}</label>
<input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins">
<br/>
</div>
</div>
</div>
</div>
</template>
<draggable :list="recipe.steps" group="steps"
@@ -318,8 +350,8 @@
<div class="row">
<div class="col-md-12">
<label :for="'id_instruction_' + step.id">{% trans 'Instructions' %}</label>
<b-form-textarea class="form-control" rows="2" max-rows="8" v-model="step.instruction"
:id="'id_instruction_' + step.id"></b-form-textarea>
<b-form-textarea class="form-control" rows="2" max-rows="20" v-model="step.instruction"
:id="'id_instruction_' + step.id"></b-form-textarea>
<small class="text-muted">{% trans 'You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>' %}</small>
</div>
</div>
@@ -327,7 +359,7 @@
</div>
</draggable>
<div class="row" style="margin-top: 1vh; margin-bottom: 2vh">
<div class="row" style="margin-top: 1vh; margin-bottom: 8vh" v-if="recipe !== undefined">
<div class="col-12">
<button type="button" @click="updateRecipe(true)"
class="btn btn-success shadow-none">{% trans 'Save & View' %}</button>
@@ -335,6 +367,11 @@
class="btn btn-info shadow-none">{% trans 'Save' %}</button>
<button type="button" @click="addStep()"
class="btn btn-primary shadow-none">{% trans 'Add Step' %}</button>
<button type="button" @click="addNutrition()"
class="btn btn-primary shadow-none"
v-if="recipe.nutrition === null">{% trans 'Add Nutrition' %}</button>
<button type="button" @click="removeNutrition()" v-if="recipe.nutrition !== null"
class="btn btn-warning shadow-none">{% trans 'Remove Nutrition' %}</button>
<a href="{% url 'view_recipe' recipe.pk %}" @click="addStep()"
class="btn btn-secondary shadow-none">{% trans 'View Recipe' %}</a>
<a href="{% url 'delete_recipe' recipe.pk %}"
@@ -349,7 +386,7 @@
{% endblock %}
{% block content_xl_right %}
<div class="sticky-top" style="top: 2vh; z-index: 100;">
<div class="sticky-top" style="top: 2vh; z-index: 100;" v-if="recipe !== undefined">
<div class="row">
<div class="col-md-11">
<button type="button" @click="updateRecipe(true)"
@@ -361,6 +398,12 @@
<button type="button" @click="addStep()"
class="btn btn-primary btn-block shadow-none">{% trans 'Add Step' %}</button>
<button type="button" @click="addNutrition()"
class="btn btn-primary btn-block shadow-none"
v-if="recipe.nutrition === null">{% trans 'Add Nutrition' %}</button>
<button type="button" @click="removeNutrition()" v-if="recipe.nutrition !== null"
class="btn btn-warning btn-block shadow-none">{% trans 'Remove Nutrition' %}</button>
<a href="{% url 'view_recipe' recipe.pk %}"
class="btn btn-secondary btn-block shadow-none">{% trans 'View Recipe' %}</a>
<a href="{% url 'delete_recipe' recipe.pk %}"
@@ -399,6 +442,8 @@
{% endblock %}
{% block script %}
<script src="{% url 'javascript-catalog' %}"></script>
<script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
@@ -455,8 +500,8 @@
e.preventDefault(); // present "Save Page" from getting triggered.
for (el of e.path) {
if(el.id !== undefined && el.id.includes('id_card_step_')) {
let step = this.recipe.steps[el.id.replace('id_card_step_','')]
if (el.id !== undefined && el.id.includes('id_card_step_')) {
let step = this.recipe.steps[el.id.replace('id_card_step_', '')]
this.addIngredient(step)
}
}
@@ -491,7 +536,7 @@
}).catch((err) => {
this.loading = false
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading the recipe!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading the recipe!') + err.bodyText, 'danger')
})
},
updateRecipe: function (view_after) {
@@ -502,14 +547,14 @@
this.$http.put("{% url 'api:recipe-detail' recipe.pk %}", this.recipe,
{}).then((response) => {
console.log(response)
this.makeToast('{% trans 'Updated' %}', '{% trans 'Changes saved successfully!' %}', 'success')
this.makeToast(gettext('Updated'), gettext('Changes saved successfully!'), 'success')
this.recipe_changed = false
if (view_after) {
location.href = "{% url 'view_recipe' 12345 %}".replace(/12345/, this.recipe.id);
}
}).catch((err) => {
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error updating the recipe!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error updating the recipe!') + err.bodyText, 'danger')
})
},
imageChanged: function (event) {
@@ -519,11 +564,11 @@
this.$http.put("{% url 'api:recipe-detail' recipe.pk %}" + 'image/', fd,
{headers: {'Content-Type': 'multipart/form-data'}}).then((response) => {
console.log(response)
this.makeToast('{% trans 'Updated' %}', '{% trans 'Changes saved successfully!' %}', 'success')
this.makeToast(gettext('Updated'), gettext('Changes saved successfully!'), 'success')
}).catch((err) => {
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error updating the recipe!' %}' + err.body.image, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error updating the recipe!') + err.body.image, 'danger')
})
let reader = new FileReader();
@@ -567,12 +612,12 @@
},
removeIngredient: function (step, ingredient) {
if (confirm('{% trans 'Are you sure that you want to delete this ingredient?' %}')) {
if (confirm(gettext('Are you sure that you want to delete this ingredient?'))) {
step.ingredients = step.ingredients.filter(item => item !== ingredient)
}
},
removeStep: function (step) {
if (confirm('{% trans 'Are you sure that you want to delete this step?' %}')) {
if (confirm(gettext('Are you sure that you want to delete this step?'))) {
this.recipe.steps = this.recipe.steps.filter(item => item !== step)
}
},
@@ -594,7 +639,7 @@
let new_unit = this.recipe.steps[step].ingredients[id]
new_unit.unit = {'name': tag}
this.foods.push(new_unit.unit)
this.units.push(new_unit.unit)
this.recipe.steps[step].ingredients[id] = new_unit
},
searchKeywords: function (query) {
@@ -604,7 +649,7 @@
this.keywords_loading = false
}).catch((err) => {
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchUnits: function (query) {
@@ -623,7 +668,7 @@
}
this.units_loading = false
}).catch((err) => {
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchFoods: function (query) {
@@ -643,12 +688,18 @@
this.foods_loading = false
}).catch((err) => {
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
scrollToStep: function (step_index) {
document.getElementById('id_step_' + step_index).scrollIntoView({behavior: 'smooth'});
},
addNutrition: function () {
this.recipe.nutrition = {}
},
removeNutrition: function () {
this.recipe.nutrition = null
}
}
});
</script>

View File

@@ -23,7 +23,7 @@
<h4>{% trans 'Units' %}</h4>
<form action="{% url 'edit_food' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two units ?' %}')">
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two units?' %}')">
{% csrf_token %}
{{ units_form|crispy }}
<button class="btn btn-danger" type="submit"
@@ -33,7 +33,7 @@
<h4>{% trans 'Ingredients' %}</h4>
<form action="{% url 'edit_food' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two ingredients ?' %}')">
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two ingredients?' %}')">
{% csrf_token %}
{{ food_form|crispy }}
<button class="btn btn-danger" type="submit">

View File

@@ -9,7 +9,7 @@
{% block content %}
<div class="table-container">
<h3>{{ title }} {% trans 'List' %}
<h3 style="margin-bottom: 2vh">{{ title }} {% trans 'List' %}
{% if create_url %}
<a href="{% url create_url %}"> <i class="fas fa-plus-circle"></i>
</a>

View File

@@ -62,7 +62,7 @@
</div>
<div class="row">
<div class="collapse col-md-12" id="collapse_adv_search">
<div class="collapse col-md-12{% if filter.data.keywords or filter.data.foods or filter.data.internal and not filter.data.internal == "unknown" %} show{% endif %}" id="collapse_adv_search">
<div style="margin-top: 1vh">
{{ filter.form.keywords | as_crispy_field }}
</div>
@@ -91,7 +91,7 @@
{% render_table recipes %}
{% else %}
<div class="alert alert-danger" role="alert">
{% trans "Log in to view Recipies" %}
{% trans "Log in to view recipes" %}
</div>
{% endif %}

View File

@@ -14,7 +14,7 @@
{% blocktrans %}
Markdown is lightweight markup language that can be used to format plain text easily.
This site uses the <a href="https://python-markdown.github.io/" target="_blank">Python Markdown</a> library to
convert your text into nice looking html. Its full markdown documentation can be found
convert your text into nice looking HTML. Its full markdown documentation can be found
<a href="https://daringfireball.net/projects/markdown/syntax" target="_blank">here</a>.
An incomplete but most likely sufficient documentation can be found below.
{% endblocktrans %}
@@ -57,7 +57,7 @@
{% trans 'or by leaving a blank line inbetween.' %}
**{% trans 'This text is bold' %}**
*{% trans 'This text is in italics' %}*
*{% trans 'This text is italic' %}*
> {% trans 'Blockquotes are also possible' %}
</code></pre>
@@ -72,7 +72,7 @@
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}<br/>
{% trans 'or by leaving a blank line inbetween.' %}<br/><br/>
<b>{% trans 'This text is bold' %}</b><br/>
<i>{% trans 'This text is in italics' %}</i>
<i>{% trans 'This text is italic' %}</i>
<blockquote>
<p>{% trans 'Blockquotes are also possible' %}</p>
</blockquote>
@@ -123,13 +123,13 @@
<br/>
<h2>{% trans 'Images & Links' %}</h2>
{% trans 'Links can be formatted with Markdown. This applicaiton also allows to paste links directly into markdown fields without any formatting.' %}
{% trans 'Links can be formatted with Markdown. This application also allows to paste links directly into markdown fields without any formatting.' %}
<pre class="intro-code code-block"><code>
https://github.com/vabene1111/recipes
[](https://github.com/vabene1111/recipes)
[GitHub](https://github.com/vabene1111/recipes)
![{% trans 'This will become and Image' %}]({% static 'favicon.png' %})
![{% trans 'This will become and image' %}]({% static 'favicon.png' %})
</code></pre>
<div style="text-align: center">
@@ -142,7 +142,7 @@
<div class="card-body">
<a href="https://github.com/vabene1111/recipes">https://github.com/vabene1111/recipes</a> <br/>
<a href="https://github.com/vabene1111/recipes">GitHub</a> <br/>
<img src="{% static 'favicon.png' %}" class="img-fluid" alt="{% trans 'This will become and Image' %}"
<img src="{% static 'favicon.png' %}" class="img-fluid" alt="{% trans 'This will become and image' %}"
style="height: 3vw">
</div>
@@ -150,7 +150,7 @@
<br/>
<h2>{% trans 'Tables' %}</h2>
{% trans 'Markdown tables are hard to create by hand. It is recommended to use a table editor like <a href="https://www.tablesgenerator.com/markdown_tables" target="_blank">this</a> one.' %}
{% trans 'Markdown tables are hard to create by hand. It is recommended to use a table editor like <a href="https://www.tablesgenerator.com/markdown_tables" rel="noreferrer noopener" target="_blank">this one.</a>' %}
<pre class="intro-code code-block"><code>
| {% trans 'Table' %} | {% trans 'Header' %} |
|--------|---------|

View File

@@ -12,6 +12,7 @@
<script src="{% static 'js/Sortable.min.js' %}"></script>
<script src="{% static 'js/vuedraggable.umd.min.js' %}"></script>
<script src="{% static 'js/vue-cookies.js' %}"></script>
<script src="{% static 'js/js.cookie.min.js' %}"></script>
@@ -24,14 +25,15 @@
<div class="col-md-4 offset-md-4">
<div class="input-group" style="margin-top: 8px; margin-bottom: 8px">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary shadow-none" @click="changeWeek(-1)">
<button class="btn btn-outline-secondary shadow-none"
@click="changeStartDate(number_of_days * -1)">
<i class="fas fa-arrow-left"></i>
</button>
</div>
<input name="week" id="id_week" class="form-control" type="week" v-model="week"
<input name="date" id="id_date" class="form-control" type="date" v-model="start_date"
@change="updatePlan()">
<div class="input-group-append">
<button class="btn btn-outline-secondary shadow-none" @click="changeWeek(1)">
<button class="btn btn-outline-secondary shadow-none" @click="changeStartDate(number_of_days)">
<i class="fas fa-arrow-right"></i>
</button>
</div>
@@ -41,10 +43,10 @@
<div class="row">
<div class="col-md-12">
<table class="table table-sm table-striped table-responsive-sm">
<table class="table table-sm table-striped table-responsive-sm" style=" table-layout:fixed;">
<thead class="thead-dark">
<tr>
<th v-for="d in days" style="width: 14.2%; text-align: center">[[d]]<br/>[[formatDateDay(d)]].
<th v-for="d in dates" style="width: 14.2%; text-align: center">[[formatDateDayname(d)]]<br/>[[formatDateDay(d)]].
<button class="btn btn-sm btn-outline-secondary shadow-none" @click="addDayToShopping(d)"><i
class="fas fa-cart-plus fa-sm"></i></button>
</th>
@@ -52,7 +54,7 @@
</thead>
<tbody v-for="t in meal_types">
<tr v-if="meal_plan[t.name] !== undefined">
<td colspan="7" style="text-align: center">
<td :colspan="number_of_days" style="text-align: center">
[[ meal_plan[t.name].name]]
<template
v-if="t.created_by !== {{ request.user.pk }} && user_names[t.created_by] !== undefined">
@@ -66,18 +68,21 @@
@change="dragChanged(d.date, t, $event)"
:empty-insert-threshold="10" handle=".handle">
<div class="" v-for="(element, index) in d.items" :key="element.id">
<!-- small layout with handle -->
<div class="d-block d-md-none">
<div class="col-">
<i class="fas fa-arrows-alt handle input-group-text"
style="width: 100%"></i>
</div>
<div class="list-group-item">
<div class="list-group-item" style="word-wrap: break-word;">
<a href="#" @click="plan_detail = element" data-toggle="modal"
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
</div>
</div>
<div class="list-group-item handle d-md-block d-none">
<div class="col-md-12">
<!-- big layout -->
<div class="list-group-item handle d-md-block d-none"
style="word-wrap: break-word; padding: 2;margin-bottom: 4">
<div class="col-md-12" style="padding: 0">
<a href="#" @click="plan_detail = element" data-toggle="modal"
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
</div>
@@ -103,14 +108,25 @@
<div class="card-body">
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
placeholder="{% trans 'Search Recipe' %}" style="margin-bottom: 8px">
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
placeholder="{% trans 'Search Recipe' %}">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"
@click="getRandomRecipes">
<i class="fas fa-dice"></i>
</button>
</div>
</div>
</div>
</div>
<draggable class="list-group" :list="recipes"
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneRecipe">
<div class="list-group-item" v-for="(element, index) in recipes" :key="element.id">
<i class="fas fa-arrows-alt"></i> [[element.name]]
<div class="list-group-item d-flex align-items-center justify-content-between" v-for="(element, index) in recipes" :key="element.id">
<span>
<i class="fas fa-arrows-alt"></i> [[element.name]]
</span>
<span class="badge badge-light badge-pill">[[element.servings]]</span>
</div>
</draggable>
</div>
@@ -126,6 +142,9 @@
class="text-muted">{% trans 'You can use markdown to format this field. See the <a href="/docs/markdown/" target="_blank" rel="noopener noreferrer">docs here</a>' %}</span></small>
<br/>
<br/>
<input type="number" class="form-control" v-model="new_note_servings"
placeholder="{% trans 'Serving Count' %}" style="margin-bottom: 8px">
<br/>
<draggable :list="pseudo_note_list"
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneNote">
<div class="list-group-item" v-for="(element, index) in pseudo_note_list"
@@ -149,7 +168,7 @@
</div>
<div class="card-body">
<template v-if="shopping_list.length < 1">{% trans 'Shopping List currently empty' %}</template>
<template v-if="shopping_list.length < 1">{% trans 'Shopping list currently empty' %}</template>
<template v-else>
<a v-bind:href="getShoppingUrl()" class="btn btn-success"
target="_blank">{% trans 'Open Shopping List' %}</a>
@@ -170,6 +189,29 @@
</div>
<div class="card-body">
<div class="row">
<div class="col">
<label>
{% trans 'Number of Days' %}
<input class="form-control" type="number" v-model="number_of_days"
@change="updatePlan(); $cookies.set('number_of_days',number_of_days)">
</label>
</div>
</div>
<div class="row">
<div class="col">
<label>
{% trans 'Weekday offset' %}
<input class="form-control" type="number" v-model="start_offset"
@change="updatePlan(); $cookies.set('start_offset',start_offset)">
<small class="text-muted">{% trans 'Number of days starting from the first day of the week to offset the default view.' %}</small>
</label>
</div>
</div>
<a href="#" data-toggle="modal"
data-target="#id_plan_types_modal">{% trans 'Edit plan types' %}</a> <br/>
<a href="#" data-toggle="modal"
@@ -204,6 +246,9 @@
<small class="text-muted">{% trans 'Recipe' %}</small><br/>
<a v-bind:href="planDetailRecipeUrl()" target="_blank">[[ plan_detail.recipe_name ]]</a>
<br/>
<br/>
<small class="text-muted">{% trans 'Serving Count' %}</small><br/>
<span>[[ plan_detail.servings ]]</span>
</template>
<template v-if="plan_detail.note !== ''">
@@ -296,12 +341,12 @@
</div>
<div class="modal-body">
{% blocktrans %}
<p>The meal plan module allows planning of meals both with recipes or just notes.</p>
<p>The meal plan module allows planning of meals both with recipes and notes.</p>
<p>Simply select a recipe from the list of recently viewed recipes or search the one you
want and drag it to the desired plan position. You can also add a note and a title and
then drag the recipe to create a plan entry with a custom title and note. Creating only
Notes is possible by dragging the create note box into the plan.</p>
<p>Click on a recipe in order to open the detail view. Here you can also add it to the
<p>Click on a recipe in order to open the detailed view. There you can also add it to the
shopping list. You can also add all recipes of a day to the shopping list by
clicking the shopping cart at the top of the table.</p>
<p>Since a common use case is to plan meals together you can define
@@ -324,7 +369,8 @@
</div>
</div>
<script src="{% url 'javascript-catalog' %}"></script>
<script type="application/javascript">
moment.locale('{{request.LANGUAGE_CODE}}');
@@ -335,8 +381,10 @@
delimiters: ['[[', ']]'],
el: '#app',
data: {
week: moment().format('YYYY-[W]WW'),
days: moment.weekdays(true),
start_date: undefined,
start_offset: 0,
dates: [],
number_of_days: $cookies.isKey('number_of_days') ? $cookies.get('number_of_days') : 7,
plan_entries: [],
meal_types: [],
meal_types_edit: [],
@@ -349,6 +397,7 @@
],
new_note_title: '',
new_note_text: '',
new_note_servings: '',
default_shared_users: [],
user_id_update: [],
user_names: {},
@@ -363,11 +412,14 @@
this.$set(this.user_names, {{ request.user.pk }}, '{{ request.user.get_user_name }}')
this.user_id_update = Array.from(this.default_shared_users)
this.start_offset = $cookies.isKey('start_offset') ? $cookies.get('start_offset') : 0;
this.start_date = moment().weekday(0).add(this.start_offset, 'days').format('YYYY-MM-DD')
this.updatePlan();
this.getRecipes();
},
methods: {
makeToast: function(title, message, variant=null) {
makeToast: function (title, message, variant = null) {
//TODO remove duplicate function in favor of central one
this.$bvToast.toast(message, {
title: title,
@@ -377,6 +429,11 @@
})
},
updatePlan: function () {
this.dates = [];
for (var i = 0; i <= (this.number_of_days - 1); i++) {
this.dates.push(moment(this.start_date).add(i, 'days'));
}
let planEntryPromise = this.getPlanEntries();
let planTypePromise = this.getPlanTypes();
@@ -385,11 +442,11 @@
})
},
getPlanEntries: function () {
return this.$http.get("{% url 'api:mealplan-list' %}?html_week=" + this.week).then((response) => {
return this.$http.get("{% url 'api:mealplan-list' %}?from_date=" + this.dates[0].format('YYYY-MM-DD') + "&to_date=" + this.dates[this.dates.length - 1].format('YYYY-MM-DD')).then((response) => {
this.plan_entries = response.data;
}).catch((err) => {
console.log("getPlanEntries error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getPlanTypes: function () {
@@ -401,7 +458,7 @@
}
}).catch((err) => {
console.log("getPlanTypes error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
buildGrid: function () {
@@ -420,11 +477,10 @@
meal_type: t.id,
days: {}
})
for (let d of this.days) {
let date = moment(this.week).weekday(this.days.indexOf(d)).format('YYYY-MM-DD')
this.$set(this.meal_plan[t.name].days, date, {
name: d,
date: date,
for (let d of this.dates) {
this.$set(this.meal_plan[t.name].days, d.format('YYYY-MM-DD'), {
name: this.formatDateDayname(d),
date: d.format('YYYY-MM-DD'),
items: []
})
}
@@ -441,17 +497,23 @@
this.updateUserNames()
},
getRandomRecipes: function () {
this.$set(this, 'recipe_query', '');
this.getRecipes();
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?limit=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
url += '&random=True'
}
this.$http.get(url).then((response) => {
this.recipes = response.data;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getMdNote: function () {
@@ -464,18 +526,18 @@
this.recipes = response.data;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
updateUserNames: function () {
return this.$http.get("{% url 'api:username-list' %}?filter_list=[" + this.user_id_update + ']').then((response) => {
for (let u of response.data) {
this.$set(this.user_names, u.id, u.username);
this.$set(this.user_names, u.id, u.username);
}
}).catch((err) => {
console.log("updateUserNames error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
dragChanged: function (date, meal_type, evt) {
@@ -501,7 +563,7 @@
this.$http.put(`{% url 'api:mealplan-list' %}${plan_entry.id}/`, plan_entry).then((response) => {
}).catch((err) => {
console.log("dragChanged update error", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
}
}
@@ -512,7 +574,7 @@
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => item !== entry)
}).catch((err) => {
console.log("deleteEntry error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
updatePlanTypes: function () {
@@ -526,14 +588,14 @@
promise_list.push(this.$http.post("{% url 'api:mealtype-list' %}", x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes create error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
} else if (x.delete) {
if (x.id !== undefined) {
promise_list.push(this.$http.delete(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes delete error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
}
} else {
@@ -541,7 +603,7 @@
}).catch((err) => {
console.log("updatePlanTypes update error: ", err);
this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
}
}
@@ -551,34 +613,43 @@
})
},
markTypeDelete: function (element) {
if (confirm('{% trans 'When deleting a meal type all entries using that type will be deleted as well. Deletion will apply when configuration is saved. Do you want to proceed?' %}')) {
if (confirm(gettext('When deleting a meal type all entries using that type will be deleted as well. Deletion will apply when configuration is saved. Do you want to proceed?'))) {
element.delete = true
}
},
cloneRecipe: function (recipe) {
return {
let r = {
id: Math.round(Math.random() * 1000) + 10000,
recipe: recipe.id,
recipe_name: recipe.name,
servings: (this.new_note_servings > 1) ? this.new_note_servings : recipe.servings,
title: this.new_note_title,
note: this.new_note_text,
is_new: true
}
this.new_note_title = ''
this.new_note_text = ''
this.new_note_servings = ''
return r
},
cloneNote: function () {
let new_entry = {
id: Math.round(Math.random() * 1000) + 10000,
title: this.new_note_title,
note: this.new_note_text,
servings: 1,
is_new: true,
}
if (new_entry.title === '') {
new_entry.title = '{% trans 'Title' %}'
new_entry.title = gettext('Title')
}
this.new_note_title = ''
this.new_note_text = ''
this.new_note_servings = ''
return new_entry
},
planElementName: function (element) {
@@ -606,11 +677,14 @@
formatLocalDate: function (date) {
return moment(date).format('LL')
},
formatDateDay: function (day) {
return moment(this.week).weekday(this.days.indexOf(day)).format('D')
formatDateDay: function (date) {
return moment(date).format('D')
},
changeWeek: function (change) {
this.week = moment(this.week).add(change, 'w').format('YYYY-[W]WW')
formatDateDayname: function (date) {
return moment(date).format('dddd')
},
changeStartDate: function (change) {
this.start_date = moment(this.start_date).add(change, 'days').format('YYYY-MM-DD')
this.updatePlan();
},
getShoppingUrl: function () {
@@ -618,22 +692,23 @@
let first = true
for (let se of this.shopping_list) {
if (first) {
url += `?r=${se.recipe}`
url += `?r=[${se.recipe},${se.servings}]`
first = false
} else {
url += `&r=${se.recipe}`
url += `&r=[${se.recipe},${se.servings}]`
}
}
return url
},
getIcalUrl: function () {
return "{% url 'api_get_plan_ical' 12345 %}".replace(/12345/, this.week);
if (this.dates.length === 0) {
return ""
}
return "{% url 'api_get_plan_ical' 12345 6789 %}".replace(/12345/, this.dates[0].format('YYYY-MM-DD')).replace(/6789/, this.dates[this.dates.length - 1].format('YYYY-MM-DD'));
},
addDayToShopping: function (day) {
let date = moment(this.week).weekday(this.days.indexOf(day)).format('YYYY-MM-DD')
addDayToShopping: function (date) {
for (let t of this.meal_types) {
for (let i of this.meal_plan[t.name].days[date].items) {
for (let i of this.meal_plan[t.name].days[date.format('YYYY-MM-DD')].items) {
if (!this.shopping_list.includes(i)) {
this.shopping_list.push(i)
}

View File

@@ -12,19 +12,11 @@
{% include 'include/vue_base.html' %}
<script src="{% static 'js/moment-with-locales.min.js' %}"></script>
<script src="{% static 'js/frac.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
<style>
/* fixes print layout being disturbed by print button tooltip */
@media print {
.tooltip {
display: none;
}
}
</style>
{% endblock %}
{% block content %}
@@ -46,10 +38,10 @@
class="fas fa-pencil-alt fa-fw"></i> {% trans 'Edit' %}</a>
<button class="dropdown-item" onclick="$('#bookmarkModal').modal({'show':true})">
<i class="fas fa-bookmark fa-fw"></i> {% trans 'Add to Book' %}</button>
{% if ingredients %}
<a class="dropdown-item" href="{% url 'view_shopping' %}?r={{ recipe.pk }}">
<i class="fas fa-shopping-cart fa-fw"></i> {% trans 'Add to Shopping' %}</a>
{% endif %}
<a class="dropdown-item" v-bind:href="shopping_url" v-if="has_ingredients">
<i class="fas fa-shopping-cart fa-fw"></i> {% trans 'Add to Shopping' %}</a>
<a class="dropdown-item" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}"><i
class="fas fa-calendar fa-fw"></i> {% trans 'Add to Plan' %}</a>
<button class="dropdown-item" onclick="openCookLogModal({{ recipe.pk }})"><i
@@ -87,13 +79,13 @@
{% if recipe.working_time and recipe.working_time != 0 %}
<span class="badge badge-secondary"><i
class="fas fa-user-clock"></i> {% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
class="fas fa-user-clock"></i> {% trans 'Preparation time ~' %} {{ recipe.working_time }} min </span>
{% endif %}
{% if recipe.waiting_time and recipe.waiting_time != 0 %}
<span
class="badge badge-secondary"><i
class="far fa-clock"></i> {% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
class="far fa-clock"></i> {% trans 'Waiting time ~' %} {{ recipe.waiting_time }} min </span>
{% endif %}
{% recipe_last recipe request.user as last_cooked %}
{% if last_cooked %}
@@ -105,10 +97,9 @@
<br/>
{% endif %}
<div class="row" v-if="recipe && has_ingredients">
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2">
<div class="row">
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2" v-if="recipe && has_ingredients">
<!-- TODO duplicate code remove -->
<div class="card border-primary">
<div class="card-body">
<div class="row">
@@ -118,8 +109,8 @@
<div class="col col-md-3">
<div class="input-group d-print-none">
<input type="number" value="1" maxlength="3" class="form-control"
v-model="ingredient_factor"/>
<input type="number" value="1" maxlength="3" class="form-control" style="min-width: 2vw"
v-model="servings"/>
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-calculator"></i></span>
</div>
@@ -160,9 +151,12 @@
<template v-if="i.no_amount">
<span>&#x2063;</span>
</template>
<template v-if="!i.no_amount && i.unit">
<span>[[roundDecimals(i.amount * ingredient_factor)]]</span>
[[i.unit.name]]
<template v-if="!i.no_amount">
<span v-html="calculateAmount(i.amount)"></span>
{# Allow for amounts without units, such as "2 eggs" #}
<template v-if="i.unit">
[[i.unit.name]]
</template>
</template>
</label>
</div>
@@ -180,12 +174,10 @@
</td>
<td style="vertical-align: middle!important;">
<template v-if="i.note">
<a class="btn btn-light btn-sm d-print-none" tabindex="-1"
data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
v-bind:data-content="i.note">
<i class="fas fa-info"></i>
</a>
<b-button v-b-popover.hover="i.note"
class="btn btn-sm d-print-none"><i
class="fas fa-info"></i></b-button>
<div class="d-none d-print-block">
<i class="far fa-comment-alt"></i> [[i.note]]
</div>
@@ -218,6 +210,60 @@
{% endif %}
</div>
{% if recipe.nutrition %}
<div class="row mt-5">
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2">
<div class="card border-primary">
<div class="card-body">
<h4 class="card-title">{% trans 'Nutrition' %}</h4>
<table class="table table-sm">
<tr>
<td style="padding-top: 8px!important; ">
<b>{% trans 'Calories' %}</b>
</td>
<td style="text-align: right">{{ recipe.nutrition.calories|floatformat:2 }}</td>
<td>kcal</td>
</tr>
<tr>
<tr>
<td style="padding-top: 8px!important; ">
<b>{% trans 'Carbohydrates' %}</b>
</td>
<td style="text-align: right">{{ recipe.nutrition.carbohydrates|floatformat:2 }}</td>
<td>g</td>
</tr>
<tr>
<tr>
<td style="padding-top: 8px!important; ">
<b>{% trans 'Fats' %}</b>
</td>
<td style="text-align: right">{{ recipe.nutrition.fats|floatformat:2 }}</td>
<td>g</td>
</tr>
<tr>
<tr>
<td style="padding-top: 8px!important; ">
<b>{% trans 'Proteins' %}</b>
</td>
<td style="text-align: right">{{ recipe.nutrition.proteins|floatformat:2 }}</td>
<td>g</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
{% if recipe.nutrition.source %}
Source: {{ recipe.nutrition.source }}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<div v-if="recipe !== undefined && recipe.steps.length > 0">
<hr>
<h3>{% trans 'Instructions' %}</h3>
@@ -257,6 +303,7 @@
<div class="col-md-6">
<table class="table table-sm">
<template v-for="i in recipe.steps[{{ forloop.counter0 }}].ingredients">
<!-- TODO duplicate code remove -->
<template v-if="i.is_header">
<tr>
@@ -279,9 +326,12 @@
<template v-if="i.no_amount">
<span>&#x2063;</span>
</template>
<template v-if="!i.no_amount && i.unit">
<span>[[roundDecimals(i.amount * ingredient_factor)]]</span>
[[i.unit.name]]
<template v-if="!i.no_amount">
<span v-html="calculateAmount(i.amount)"></span>
{# Allow for amounts without units, such as "2 eggs" #}
<template v-if="i.unit">
[[i.unit.name]]
</template>
</template>
</label>
</div>
@@ -299,12 +349,9 @@
</td>
<td style="vertical-align: middle!important;">
<template v-if="i.note">
<a class="btn btn-light btn-sm d-print-none" tabindex="-1"
data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
v-bind:data-content="i.note">
<i class="fas fa-info"></i>
</a>
<b-button v-b-popover.hover="i.note"
class="btn btn-sm d-print-none"><i
class="fas fa-info"></i></b-button>
<div class="d-none d-print-block">
<i class="far fa-comment-alt"></i> [[i.note]]
</div>
@@ -469,6 +516,13 @@
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
{% if user_servings %}
const recipe_servings = {{ user_servings|floatformat:0 }}
{% else %}
const recipe_servings = {{ recipe.servings }}
{% endif %}
let app = new Vue({
delimiters: ['[[', ']]'],
el: '#id_base_container',
@@ -476,9 +530,16 @@
recipe: undefined,
has_ingredients: false,
has_times: false,
ingredient_factor: 1,
servings: recipe_servings,
},
computed: {
ingredient_factor: function () {
return this.servings / recipe_servings
},
shopping_url: function () {
return `{% url 'view_shopping' %}?r=[${this.recipe.id},${this.servings}]`
},
},
mounted: function () {
this.loadRecipe()
},
@@ -487,7 +548,7 @@
this.$http.get("{% url 'api:recipe-detail' recipe.pk %}" {% if share %}
+ "?share={{ share }}"{% endif %}).then((response) => {
this.recipe = response.data;
this.loading = false
this.loading = false;
for (let step of this.recipe.steps) {
if (step.ingredients.length > 0) {
@@ -496,25 +557,25 @@
if (step.time !== 0) {
this.has_times = true
}
this.$set(step, 'time_finished', undefined)
this.$set(step, 'time_finished', undefined);
for (let i of step.ingredients) {
this.$set(i, 'checked', false)
}
}
}).catch((err) => {
this.error = err.data
this.loading = false
this.error = err.data;
this.loading = false;
console.log(err)
})
},
roundDecimals: function (num) {
let decimals = {% if request.user.userpreference.ingredient_decimals %}
{{ request.user.userpreference.ingredient_decimals }} {% else %} 2 {% endif %}
{{ request.user.userpreference.ingredient_decimals }} {% else %} 2; {% endif %}
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`);
},
updateTimes: function (step) {
let time_diff_first = 0
let time_diff_first = 0;
for (let s of this.recipe.steps) {
if (this.recipe.steps.indexOf(s) < this.recipe.steps.indexOf(step)) {
time_diff_first += s.time
@@ -523,27 +584,34 @@
this.recipe.steps[0].time_finished = moment(step.time_finished).subtract(time_diff_first, 'minutes').format(moment.HTML5_FMT.DATETIME_LOCAL);
let time_diff = 0
let time_diff = 0;
for (let s of this.recipe.steps) {
s.time_finished = moment(this.recipe.steps[0].time_finished).add(time_diff, 'minutes').format(moment.HTML5_FMT.DATETIME_LOCAL);
time_diff += s.time
}
}
},
calculateAmount: function (amount) {
{% if request.user.userpreference.use_fractions %}
let return_string = ''
let fraction = frac.cont((amount * this.ingredient_factor), 9, true)
if (fraction[0] > 0) {
return_string += fraction[0]
}
if (fraction[1] > 0) {
return_string += ` <sup>${(fraction[1])}</sup>&frasl;<sub>${(fraction[2])}</sub>`
}
return return_string
{% else %}
return this.roundDecimals(amount * this.ingredient_factor)
{% endif %}
},
}
});
// Bootstrap component functions
$(function () {
$('[data-toggle="popover"]').popover()
});
$('.popover-dismiss').popover({
trigger: 'focus'
});
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}

View File

@@ -1,6 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans 'Login' %}{% endblock %}
{% block content %}
{% if form.errors %}

View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}
{% block title %}{% trans 'Register' %}{% endblock %}
{% block content %}
<h3>{% trans 'Create your Account' %}</h3>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create User' %}
</button>
</form>
{% endblock %}

View File

@@ -12,12 +12,12 @@
{% block content %}
<h1>{% trans 'Setup' %}</h1>
<p>{% blocktrans %}To start using this application you must first create a superuser.{% endblocktrans %}</p>
<p>{% blocktrans %}To start using this application you must first create a superuser account.{% endblocktrans %}</p>
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create Superuser' %}</button>
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create Superuser account' %}</button>
</form>
{% endblock %}

View File

@@ -4,61 +4,702 @@
{% load static %}
{% load i18n %}
{% block title %}{% trans "Cookbook" %}{% endblock %}
{% block title %}{% trans "Shopping List" %}{% endblock %}
{% block extra_head %}
{{ form.media }}
{% include 'include/vue_base.html' %}
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}">
<script src="{% static 'js/vue-multiselect.min.js' %}"></script>
<script src="{% static 'js/Sortable.min.js' %}"></script>
<script src="{% static 'js/vuedraggable.umd.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
{% endblock %}
{% block content %}
<h2><i class="fas fa-shopping-cart"></i> {% trans 'Shopping List' %}</h2>
<form action="{% url 'view_shopping' %}" method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-sync-alt"></i> {% trans 'Load' %}</button>
</form>
<br/>
<br/>
<div class="row">
<div class="col col-md-12">
<!--// @formatter:off-->
<textarea id="id_list" class="form-control" rows="{{ ingredients|length|add:1 }}">{% for i in ingredients %}{% if markdown_format %}- [ ] {% endif %}{{ i.amount.normalize }} {{ i.unit }} {{ i.food.name }}&#13;&#10;{% endfor %}</textarea>
<!--// @formatter:on-->
<div class="col col-md-9">
<h2>{% trans 'Shopping List' %}</h2>
</div>
</div>
<br/>
<div class="row">
<div class="col col-md-12 text-center">
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
data-placement="right" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
class="far fa-copy"></i></button>
<div class="col col-mdd-3 text-right">
<b-form-checkbox switch size="lg" v-model="edit_mode"
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
</div>
</div>
<script type="text/javascript">
function copy() {
let list = $('#id_list');
<template v-if="shopping_list !== undefined">
list.select();
<div class="text-center" v-if="loading">
<i class="fas fa-spinner fa-spin fa-8x"></i>
</div>
<div v-else-if="edit_mode">
<div class="row">
<div class="col col-md-6">
$('#id_btn_copy').attr('data-original-title','{% trans 'Copied!' %}').tooltip('show');
<div class="card">
<div class="card-header">
<i class="fa fa-search"></i> {% trans 'Search' %}
</div>
<div class="card-body">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
placeholder="{% trans 'Search Recipe' %}">
<ul class="list-group" style="margin-top: 8px">
<li class="list-group-item" v-for="x in recipes">
<div class="row flex-row" style="padding-left: 0.5vw; padding-right: 0.5vw">
<div class="flex-column flex-fill my-auto"><a v-bind:href="getRecipeUrl(x.id)"
target="_blank"
rel="nofollow norefferer">[[x.name]]</a>
</div>
<div class="flex-column align-self-end">
<button class="btn btn-outline-primary shadow-none"
@click="addRecipeToList(x)"><i
class="fa fa-plus"></i></button>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
document.execCommand("copy");
}
<div class="col col-md-6">
<div class="card">
<div class="card-header">
<i class="fa fa-shopping-cart"></i> {% trans 'Shopping Recipes' %}
</div>
<div class="card-body">
<template v-if="shopping_list.recipes.length < 1">
{% trans 'No recipes selected' %}
</template>
<template v-else>
<div class="row flex-row my-auto" v-for="x in shopping_list.recipes"
style="margin-top: 1vh!important;">
<div class="flex-column align-self-start " style="margin-right: 0.4vw">
<button class="btn btn-outline-danger" @click="removeRecipeFromList(x)"><i
class="fa fa-trash"></i></button>
</div>
<div class="flex-grow-1 flex-column my-auto"><a v-bind:href="getRecipeUrl(x.recipe)"
target="_blank"
rel="nofollow norefferer">[[x.recipe_name]]</a>
</div>
<div class="flex-column align-self-end ">
<div class="input-group input-group-sm my-auto">
<div class="input-group-prepend">
<button class="text-muted btn btn-outline-primary shadow-none"
@click="((x.servings - 1) > 0) ? x.servings -= 1 : 1">-
</button>
</div>
<input class="form-control" type="number" v-model="x.servings">
<div class="input-group-append">
<button class="text-muted btn btn-outline-primary shadow-none"
@click="x.servings += 1">
+
</button>
</div>
</div>
</div>
function resetTooltip() {
setTimeout(function () {
$('#id_btn_copy').attr('data-original-title','{% trans 'Copy list to clipboard' %}');
}, 300);
}
</div>
</template>
</div>
</div>
</div>
</div>
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
<table class="table table-sm table-striped" style="margin-top: 1vh">
<tbody is="draggable" group="people" :list="display_entries" tag="tbody" :empty-insert-threshold="10"
handle=".handle" @sort="sortEntries()">
<tr v-for="(element, index) in display_entries" :key="element.id">
<!--<td class="handle"><i class="fas fa-sort"></i></td>-->
<td>[[element.amount]]</td>
<td>[[element.unit.name]]</td>
<td>[[element.food.name]]</td>
<td>
<button class="btn btn-sm btn-outline-danger" v-if="element.list_recipe === null"
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)">
<i class="fa fa-trash"></i></button>
</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col col-md-3">
<input class="form-control" type="number" placeholder="{% trans 'Amount' %}"
v-model="new_entry.amount" ref="new_entry_amount">
</div>
<div class="col col-md-4">
<multiselect
v-tabindex
ref="unit"
v-model="new_entry.unit"
:options="units"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select Unit' %}"
tag-placeholder="{% trans 'Create' %}"
select-label="{% trans 'Select' %}"
:taggable="true"
@tag="addUnitType"
label="name"
track-by="name"
:multiple="false"
:loading="units_loading"
@search-change="searchUnits">
</multiselect>
</div>
<div class="col col-md-4">
<multiselect
v-tabindex
ref="food"
v-model="new_entry.food"
:options="foods"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select Food' %}"
tag-placeholder="{% trans 'Create' %}"
select-label="{% trans 'Select' %}"
:taggable="true"
@tag="addFoodType"
label="name"
track-by="name"
:multiple="false"
:loading="foods_loading"
@search-change="searchFoods">
</multiselect>
</div>
<div class="col col-md-1 my-auto text-right">
<button class="btn btn-success btn-lg" @click="addEntry()"><i class="fa fa-plus"></i>
</button>
</div>
</div>
<div class="row">
<div class="col" style="text-align: right; margin-top: 1vh">
<div class="form-group form-check form-group-lg">
<input class="form-check-input" style="zoom:1.3;" type="checkbox"
v-model="shopping_list.finished" id="id_finished">
<label class="form-check-label" style="zoom:1.3;"
for="id_finished"> {% trans 'Finished' %}</label>
</div>
</div>
</div>
<div class="row">
<div class="col" style="margin-top: 1vh">
<multiselect
v-tabindex
v-model="shopping_list.shared"
:options="users"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select User' %}"
select-label="{% trans 'Select' %}"
label="username"
track-by="id"
:multiple="true"
:loading="users_loading"
@search-change="searchUsers">
</multiselect>
</div>
</div>
</div>
<div v-else>
{% if request.user.userpreference.shopping_auto_sync > 0 %}
<div class="row" v-if="!onLine">
<div class="col col-md-12">
<div class="alert alert-warning" role="alert">
{% trans 'You are offline, shopping list might not syncronize.' %}
</div>
</div>
</div>
{% endif %}
<div class="row" style="margin-top: 8px">
<div class="col col-md-12">
<table class="table">
<tr v-for="x in display_entries">
<template v-if="!x.checked">
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
@change="entryChecked(x)">
</td>
<td>[[x.amount]]</td>
<td>[[x.unit.name]]</td>
<td>[[x.food.name]]</td>
</template>
</tr>
<tr v-for="x in display_entries" class="text-muted">
<template v-if="x.checked">
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
@change="entryChecked(x)">
</td>
<td>[[x.amount]]</td>
<td>[[x.unit.name]]</td>
<td>[[x.food.name]]</td>
</template>
</tr>
</table>
</div>
</div>
</div>
<div class="row" style="margin-top: 2vh">
<div class="col" style="text-align: right">
<b-button class="btn btn-info" v-b-modal.id_modal_export><i
class="fas fa-file-export"></i> {% trans 'Export' %}</b-button>
<button class="btn btn-success" @click="updateShoppingList()" v-if="edit_mode"><i
class="fas fa-save"></i> {% trans 'Save' %}
</button>
</div>
</div>
<b-modal id="id_modal_export" title="{% trans 'Copy/Export' %}">
<div class="row">
<div class="col col-12">
<label>
{% trans 'List Prefix' %}
<input class="form-control" v-model="export_text_prefix">
</label>
</div>
</div>
<div class="row">
<div class="col col-12">
<b-form-textarea class="form-control" max-rows="8" v-model="export_text">
</b-form-textarea>
</div>
</div>
</b-modal>
</template>
{% endblock %}
{% block script %}
<script src="{% url 'javascript-catalog' %}"></script>
<script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
Vue.component('vue-multiselect', window.VueMultiselect.default)
let app = new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
delimiters: ['[[', ']]'],
el: '#id_base_container',
data: {
shopping_list_id: {% if shopping_list_id %}{{ shopping_list_id }}{% else %}null{% endif %},
loading: true,
edit_mode: false,
export_text_prefix: '', //TODO add userpreference
recipe_query: '',
recipes: [],
shopping_list: undefined,
new_entry: {
unit: undefined,
amount: undefined,
food: undefined,
},
foods: [],
foods_loading: false,
units: [],
units_loading: false,
users: [],
users_loading: false,
onLine: navigator.onLine,
},
directives: {
tabindex: {
inserted(el) {
el.setAttribute('tabindex', 0);
}
}
},
computed: {
servings_cache() {
let cache = {}
this.shopping_list.recipes.forEach((r) => {
cache[r.id] = r.servings;
})
return cache
},
display_entries() {
let entries = []
//TODO merge multiple ingredients of same unit
this.shopping_list.entries.forEach(element => {
let item = {}
Object.assign(item, element);
if (item.list_recipe !== null) {
item.amount = item.amount * this.servings_cache[item.list_recipe]
}
item.unit = ((element.unit !== undefined && element.unit !== null) ? element.unit : {'name': ''})
entries.push(item)
});
return entries
},
export_text() {
let text = ''
for (let e of this.display_entries.filter(item => item.checked === false)) {
text += `${this.export_text_prefix}${e.amount} ${e.unit.name} ${e.food.name} \n`
}
return text
}
},
/*
watch: {
recipe: {
deep: true,
handler() {
this.recipe_changed = this.recipe_changed !== undefined;
}
}
},
created() {
window.addEventListener('beforeunload', this.warnPageLeave)
},
*/
mounted: function () {
this.loadShoppingList()
{% if recipes %}
this.loading = true
this.edit_mode = true
let loadingRecipes = []
{% for r in recipes %}
loadingRecipes.push(this.loadInitialRecipe({{ r.recipe }}, {{ r.servings }}))
{% endfor %}
Promise.allSettled(loadingRecipes).then(() => {
this.loading = false
})
{% endif %}
{% if request.user.userpreference.shopping_auto_sync > 0 %}
setInterval(() => {
if ((this.shopping_list_id !== null) && !this.edit_mode && window.navigator.onLine) {
this.loadShoppingList(true)
}
}, {% widthratio request.user.userpreference.shopping_auto_sync 1 1000 %})
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
{% endif %}
this.searchUsers('')
},
methods: {
updateOnlineStatus(e) {
const {
type
} = e;
this.onLine = type === 'online';
},
/*
warnPageLeave: function (event) {
if (this.recipe_changed) {
event.returnValue = ''
return ''
}
},
*/
makeToast: function (title, message, variant = null) {
//TODO remove duplicate function in favor of central one
this.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-top-center',
solid: true
})
},
loadInitialRecipe: function (recipe, servings) {
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
this.addRecipeToList(response.data, servings)
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
loadShoppingList: function (autosync = false) {
if (this.shopping_list_id) {
this.$http.get("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list_id) + ((autosync) ? '?autosync=true' : '')).then((response) => {
if (!autosync) {
this.shopping_list = response.body
this.loading = false
} else {
let check_map = {}
for (let e of response.body.entries) {
check_map[e.id] = {checked: e.checked}
}
for (let se of this.shopping_list.entries) {
if (check_map[se.id] !== undefined) {
se.checked = check_map[se.id].checked
}
}
}
if (this.shopping_list.entries.length === 0) {
this.edit_mode = true
}
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
} else {
this.shopping_list = {
"recipes": [],
"entries": [],
"entries_display": [],
"shared": [{% for u in request.user.userpreference.plan_share.all %}
{'id': {{ u.pk }}, 'username': '{{ u.get_user_name }}'},
{% endfor %}],
"created_by": 1
}
this.loading = false
if (this.shopping_list.entries.length === 0) {
this.edit_mode = true
}
}
},
updateShoppingList: function () {
this.loading = true
let recipe_promises = []
for (let i in this.shopping_list.recipes) {
if (this.shopping_list.recipes[i].created) {
console.log('updating recipe', this.shopping_list.recipes[i])
recipe_promises.push(this.$http.post("{% url 'api:shoppinglistrecipe-list' %}", this.shopping_list.recipes[i], {}).then((response) => {
let old_id = this.shopping_list.recipes[i].id
console.log("list recipe create respose ", response.body)
this.$set(this.shopping_list.recipes, i, response.body)
for (let e of this.shopping_list.entries.filter(item => item.list_recipe === old_id)) {
console.log("found recipe updating ID")
e.list_recipe = this.shopping_list.recipes[i].id
}
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
}))
}
}
Promise.allSettled(recipe_promises).then(() => {
console.log("proceeding to update shopping list", this.shopping_list)
if (this.shopping_list_id === null) {
return this.$http.post("{% url 'api:shoppinglist-list' %}", this.shopping_list, {}).then((response) => {
console.log(response)
this.makeToast(gettext('Updated'), gettext('Object created successfully!'), 'success')
this.loading = false
this.shopping_list = response.body
this.shopping_list_id = this.shopping_list.id
window.history.pushState('shopping_list', '{% trans 'Shopping List' %}', "{% url 'view_shopping' 123456 %}".replace('123456', this.shopping_list_id));
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), '{% trans 'There was an error creating a resource!' %}' + err.bodyText, 'danger')
this.loading = false
})
} else {
return this.$http.put("{% url 'api:shoppinglist-detail' shopping_list_id %}", this.shopping_list, {}).then((response) => {
console.log(response)
this.shopping_list = response.body
this.makeToast(gettext('Updated'), gettext('Changes saved successfully!'), 'success')
this.loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
this.loading = false
})
}
})
},
sortEntries: function () {
this.display_entries.forEach((item, index) => {
})
console.log("IMPLEMENT ME", this.display_entries)
},
entryChecked: function (entry) {
console.log("checked entry: ", entry)
this.shopping_list.entries.forEach((item) => {
if (item.id === entry.id) { //TODO unwrap once same entries are merged
item.checked = entry.checked
this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
this.loading = false
})
}
})
},
addEntry: function () {
if (this.new_entry.food !== undefined) {
this.shopping_list.entries.push({
'list_recipe': null,
'food': this.new_entry.food,
'unit': this.new_entry.unit,
'amount': parseFloat(this.new_entry.amount),
'order': 0,
'checked': false
})
this.new_entry = {
unit: undefined,
amount: undefined,
food: undefined,
}
this.$refs.new_entry_amount.focus();
} else {
this.makeToast(gettext('Error'), gettext('Please enter a valid food'), 'danger')
}
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?limit=5&internal=true"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
this.recipes = []
return
}
this.$http.get(url).then((response) => {
this.recipes = response.data;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getRecipeUrl: function (id) { //TODO generic function that can be reused else were
return '{% url 'view_recipe' 123456 %}'.replace('123456', id)
},
addRecipeToList: function (recipe, servings = 1) {
let slr = {
"created": true,
"id": Math.random() * 1000,
"recipe": recipe.id,
"recipe_name": recipe.name,
"servings": servings,
}
this.shopping_list.recipes.push(slr)
for (let s of recipe.steps) {
for (let i of s.ingredients) {
if (!i.is_header && i.food !== null) {
this.shopping_list.entries.push({
'list_recipe': slr.id,
'food': i.food,
'unit': i.unit,
'amount': i.amount,
'order': 0
})
}
}
}
},
removeRecipeFromList: function (slr) {
this.shopping_list.entries = this.shopping_list.entries.filter(item => item.list_recipe !== slr.id)
this.shopping_list.recipes = this.shopping_list.recipes.filter(item => item !== slr)
},
searchKeywords: function (query) {
this.keywords_loading = true
this.$http.get("{% url 'api:keyword-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.keywords = response.data;
this.keywords_loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchUnits: function (query) { //TODO move to central component
this.units_loading = true
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.units = response.data;
this.units_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchFoods: function (query) { //TODO move to central component
this.foods_loading = true
this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.foods = response.data
this.foods_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
addFoodType: function (tag, index) { //TODO move to central component
let new_food = {'name': tag}
this.foods.push(new_food)
this.new_entry.food = new_food
},
addUnitType: function (tag, index) { //TODO move to central component
let new_unit = {'name': tag}
this.units.push(new_unit)
this.new_entry.unit = new_unit
},
searchUsers: function (query) { //TODO move to central component
this.users_loading = true
this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.users = response.data
this.users_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
},
beforeDestroy() {
window.removeEventListener('online', this.updateOnlineStatus);
window.removeEventListener('offline', this.updateOnlineStatus);
}
});
</script>
{% endblock %}

View File

@@ -10,7 +10,43 @@
{% block content %}
<h1>{% trans 'System Information' %}</h1>
<h1>{% trans 'System' %}</h1>
<br/>
<br/>
<br/>
<div class="row">
<div class="col-md-6">
<h3>{% trans 'Invite Links' %}</h3>
<a href="{% url 'list_invite_link' %}" class="btn btn-success">{% trans 'Show Links' %}</a>
</div>
<!--
<div class="col-md-6">
<h3>{% trans 'Backup & Restore' %}</h3>
<a href="{% url 'api_backup' %}" class="btn btn-success">{% trans 'Download Backup' %}</a>
<br/> <br/>
<div class="alert alert-danger" role="alert">
⚠️ Backups simply create a so called fixture. Fixtures are json files containing all your data (WITHOUT
MEDIA FILES) <br>
They can be imported into django by running <code style="color: white">manage.py loaddata [fixture-name]</code> <br>
It is planned to provide a better way of backing up and restoring data but it is not yet implemented.<br><br>
⚠️<b>Please make sure to setup a solid backup strategy on your server to save the Database and the <code style="color: white">mediafiles</code>
directory</b>⚠️
</div>
</div>
-->
</div>
<br/>
<br/>
<br/>
<br/>
<h3>{% trans 'System Information' %}</h3>
{% blocktrans %}
Django Recipes is an open source free software application. It can be found on
@@ -45,7 +81,8 @@
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if secret_key %}
{% blocktrans %}
You do not have a <code>SECRET_KEY</code> configured in your <code>.env</code> file. Django defaulted to the standard key
You do not have a <code>SECRET_KEY</code> configured in your <code>.env</code> file. Django defaulted to the
standard key
provided with the installation which is publicly know and insecure! Please set
<code>SECRET_KEY</code> int the <code>.env</code> configuration file.
{% endblocktrans %}

View File

@@ -80,7 +80,7 @@
<div class="col-md-1">
<input class="form-control" v-model="i.amount">
</div>
<div class="col-md-5">
<div class="col-md-4">
<table class="table-layout:fixed">
<col width="95%"/>
@@ -119,7 +119,7 @@
</div>
<div class="col-md-5">
<div class="col-md-4">
<multiselect v-tabindex
ref="ingredient"
@@ -143,6 +143,10 @@
</multiselect>
</div>
<div class="col-md-2">
<input type="text" placeholder="{% trans 'Note' %}" class="form-control"
v-model="i.note">
</div>
<div class="col-md-1">
<button class="btn btn-outline-danger btn-lg" type="button"
@click="deleteIngredient(i)" tabindex="-1"><i
@@ -191,13 +195,15 @@
</div>
<div class="form-group">
<label for="id_all_keywords">{% trans 'All Keywords' %}</label><br/>
{% trans 'All Keywords' %}<br/>
<input id="id_all_keywords" type="checkbox"
v-model="all_keywords"> {% trans 'Import all Keywords not only the ones already existing.' %}
v-model="all_keywords"> <label
for="id_all_keywords">{% trans 'Import all keywords, not only the ones already existing.' %}</label>
</div>
<div class="form-group">
<button type="button" class="btn btn-success" @click="importRecipe()" :disabled="importing_recipe">{% trans 'Import' %}</button>
<button type="button" class="btn btn-success" @click="importRecipe()"
:disabled="importing_recipe">{% trans 'Import' %}</button>
</div>
<br/>
@@ -243,6 +249,7 @@
</div>
<script src="{% url 'javascript-catalog' %}"></script>
<script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
@@ -280,6 +287,7 @@
this.searchKeywords('')
this.searchUnits('')
this.searchIngredients('')
},
methods: {
makeToast: function (title, message, variant = null) {
@@ -295,19 +303,19 @@
this.recipe_data = undefined
this.error = undefined
this.loading = true
this.$http.get("{% url 'api_recipe_from_url' 12345 %}".replace(/12345/, this.remote_url)).then((response) => {
this.$http.post("{% url 'api_recipe_from_url' %}", {'url': this.remote_url}, {emulateJSON: true}).then((response) => {
this.recipe_data = response.data;
this.loading = false
}).catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
importRecipe: function () {
if (this.importing_recipe) {
this.makeToast('{% trans 'Error' %}', '{% trans 'Already importing the selected recipe, please wait!' %}', 'danger')
this.makeToast(gettext('Error'), gettext('Already importing the selected recipe, please wait!'), 'danger')
return;
}
this.importing_recipe = true
@@ -316,7 +324,7 @@
window.location.href = response.data
}).catch((err) => {
console.log(err);
this.makeToast('{% trans 'Error' %}', '{% trans 'An error occurred while trying to import this recipe!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('An error occurred while trying to import this recipe!') + err.bodyText, 'danger')
})
},
deleteIngredient: function (i) {
@@ -345,7 +353,9 @@
},
openUnitSelect: function (id) {
let index = id.replace('unit_', '')
this.$set(app.$refs.unit[index].$data, 'search', this.recipe_data.recipeIngredient[index].unit.text)
if (this.recipe_data.recipeIngredient[index].unit !== null) {
this.$set(app.$refs.unit[index].$data, 'search', this.recipe_data.recipeIngredient[index].unit.text)
}
},
openIngredientSelect: function (id) {
let index = id.replace('ingredient_', '')
@@ -358,7 +368,7 @@
this.keywords_loading = false
}).catch((err) => {
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchUnits: function (query) {
@@ -367,7 +377,7 @@
this.units = response.data.results;
if (this.recipe_data !== undefined) {
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
if (x.unit.text !== '') {
if (x.unit !== null && x.unit.text !== '') {
this.units = this.units.filter(item => item.text !== x.unit.text)
this.units.push(x.unit)
}
@@ -376,7 +386,7 @@
this.units_loading = false
}).catch((err) => {
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchIngredients: function (query) {
@@ -395,7 +405,7 @@
this.ingredients_loading = false
}).catch((err) => {
console.log(err)
this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger')
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
}

View File

@@ -7,7 +7,7 @@ from django.urls import reverse, NoReverseMatch
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import get_model_name
from cookbook.models import get_model_name, Space
from recipes import settings
register = template.Library()
@@ -46,13 +46,15 @@ def recipe_rating(recipe, user):
rating = recipe.cooklog_set.filter(created_by=user).aggregate(Avg('rating'))
if rating['rating__avg']:
rating_stars = ''
rating_stars = '<span style="display: inline-block;">'
for i in range(int(rating['rating__avg'])):
rating_stars = rating_stars + '<i class="fas fa-star fa-xs"></i>'
if rating['rating__avg'] % 1 >= 0.5:
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>'
rating_stars += '</span>'
return rating_stars
else:
return ''
@@ -69,6 +71,11 @@ def recipe_last(recipe, user):
return ''
@register.simple_tag
def message_of_the_day():
return Space.objects.first().message
@register.simple_tag
def is_debug():
return settings.DEBUG

View File

@@ -0,0 +1,26 @@
import json
from django.contrib import auth
from django.db.models import ProtectedError
from django.urls import reverse
from cookbook.models import Storage, Sync, Keyword, ShoppingList, Recipe
from cookbook.tests.views.test_views import TestViews
class TestApiShopping(TestViews):
def setUp(self):
super(TestApiShopping, self).setUp()
self.internal_recipe = Recipe.objects.create(
name='Test',
internal=True,
created_by=auth.get_user(self.user_client_1)
)
def test_shopping_view_permissions(self):
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 200), (self.user_client_1, 200),
(self.user_client_2, 200), (self.admin_client_1, 200), (self.superuser_client, 200)],
reverse('api:recipe-detail', args={self.internal_recipe.id}))
# TODO add tests for editing

View File

@@ -0,0 +1,27 @@
import json
from django.contrib import auth
from django.db.models import ProtectedError
from django.urls import reverse
from cookbook.models import Storage, Sync, Keyword, ShoppingList
from cookbook.tests.views.test_views import TestViews
class TestApiShopping(TestViews):
def setUp(self):
super(TestApiShopping, self).setUp()
self.list_1 = ShoppingList.objects.create(created_by=auth.get_user(self.user_client_1))
self.list_2 = ShoppingList.objects.create(created_by=auth.get_user(self.user_client_2))
def test_shopping_view_permissions(self):
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 404), (self.admin_client_1, 404), (self.superuser_client, 200)],
reverse('api:shoppinglist-detail', args={self.list_1.id}))
self.list_1.shared.add(auth.get_user(self.user_client_2))
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 200), (self.admin_client_1, 404), (self.superuser_client, 200)],
reverse('api:shoppinglist-detail', args={self.list_1.id}))
# TODO add tests for editing

View File

@@ -1,5 +1,6 @@
import json
from cookbook.helper.ingredient_parser import parse_ingredient
from cookbook.helper.recipe_url_import import get_from_html
from cookbook.tests.test_setup import TestBase
@@ -8,21 +9,79 @@ class TestEditsRecipe(TestBase):
def test_ld_json(self):
test_list = [
{'file': 'cookbook/tests/resources/websites/ld_json_1.html', 'result_length': 3128},
{'file': 'cookbook/tests/resources/websites/ld_json_2.html', 'result_length': 1450},
{'file': 'cookbook/tests/resources/websites/ld_json_3.html', 'result_length': 1545},
{'file': 'cookbook/tests/resources/websites/ld_json_4.html', 'result_length': 1657},
{'file': 'cookbook/tests/resources/websites/ld_json_invalid.html', 'result_length': 115},
{'file': 'cookbook/tests/resources/websites/ld_json_itemList.html', 'result_length': 3131},
{'file': 'cookbook/tests/resources/websites/ld_json_multiple.html', 'result_length': 1546},
{'file': 'cookbook/tests/resources/websites/micro_data_1.html', 'result_length': 1022},
{'file': 'cookbook/tests/resources/websites/micro_data_2.html', 'result_length': 1384},
{'file': 'cookbook/tests/resources/websites/micro_data_3.html', 'result_length': 1100},
{'file': 'cookbook/tests/resources/websites/micro_data_4.html', 'result_length': 4231},
{'file': 'cookbook/tests/resources/websites/ld_json_1.html', 'result_length': 3218},
{'file': 'cookbook/tests/resources/websites/ld_json_2.html', 'result_length': 1510},
{'file': 'cookbook/tests/resources/websites/ld_json_3.html', 'result_length': 1629},
{'file': 'cookbook/tests/resources/websites/ld_json_4.html', 'result_length': 1729},
{'file': 'cookbook/tests/resources/websites/ld_json_itemList.html', 'result_length': 3200},
{'file': 'cookbook/tests/resources/websites/ld_json_multiple.html', 'result_length': 1606},
{'file': 'cookbook/tests/resources/websites/micro_data_1.html', 'result_length': 1079},
{'file': 'cookbook/tests/resources/websites/micro_data_2.html', 'result_length': 1429},
{'file': 'cookbook/tests/resources/websites/micro_data_3.html', 'result_length': 1148},
{'file': 'cookbook/tests/resources/websites/micro_data_4.html', 'result_length': 4396},
]
for test in test_list:
with open(test['file'], 'rb') as file:
print(f'Testing {test["file"]} expecting length {test["result_length"]}')
parsed_content = json.loads(get_from_html(file.read(), 'test_url').content)
self.assertEqual(len(str(parsed_content)), test['result_length'])
file.close()
def test_ingredient_parser(self):
expectations = {
"2¼ l Wasser": (2.25, "l", "Wasser", ""),
"2¼l Wasser": (2.25, "l", "Wasser", ""),
"3l Wasser": (3, "l", "Wasser", ""),
"4 l Wasser": (4, "l", "Wasser", ""),
"½l Wasser": (0.5, "l", "Wasser", ""),
"⅛ Liter Sauerrahm": (0.125, "Liter", "Sauerrahm", ""),
"5 Zwiebeln": (5, "", "Zwiebeln", ""),
"3 Zwiebeln, gehackt": (3, "", "Zwiebeln", "gehackt"),
"5 Zwiebeln (gehackt)": (5, "", "Zwiebeln", "gehackt"),
"1 Zwiebel(n)": (1, "", "Zwiebel(n)", ""),
"4 1/2 Zwiebeln": (4.5, "", "Zwiebeln", ""),
"4 ½ Zwiebeln": (4.5, "", "Zwiebeln", ""),
"etwas Mehl": (0, "", "etwas Mehl", ""),
"Öl zum Anbraten": (0, "", "Öl zum Anbraten", ""),
"n. B. Knoblauch, zerdrückt": (0, "", "n. B. Knoblauch", "zerdrückt"),
"Kräuter, mediterrane (Oregano, Rosmarin, Basilikum)": (
0, "", "Kräuter, mediterrane", "Oregano, Rosmarin, Basilikum"),
"600 g Kürbisfleisch (Hokkaido), geschält, entkernt und geraspelt": (
600, "g", "Kürbisfleisch (Hokkaido)", "geschält, entkernt und geraspelt"),
"Muskat": (0, "", "Muskat", ""),
"200 g Mehl, glattes": (200, "g", "Mehl", "glattes"),
"1 Ei(er)": (1, "", "Ei(er)", ""),
"1 Prise(n) Salz": (1, "Prise(n)", "Salz", ""),
"etwas Wasser, lauwarmes": (0, "", "etwas Wasser", "lauwarmes"),
"Strudelblätter, fertige, für zwei Strudel": (0, "", "Strudelblätter", "fertige, für zwei Strudel"),
"barrel-aged Bourbon": (0, "", "barrel-aged Bourbon", ""),
"golden syrup": (0, "", "golden syrup", ""),
"unsalted butter, for greasing": (0, "", "unsalted butter", "for greasing"),
"unsalted butter , for greasing": (0, "", "unsalted butter", "for greasing"), # trim
"1 small sprig of fresh rosemary": (1, "small", "sprig of fresh rosemary", ""),
# does not always work perfectly!
"75 g fresh breadcrumbs": (75, "g", "fresh breadcrumbs", ""),
"4 acorn squash , or onion squash (600-800g)": (4, "acorn", "squash , or onion squash", "600-800g"),
"1 x 250 g packet of cooked mixed grains , such as spelt and wild rice": (
1, "x", "250 g packet of cooked mixed grains", "such as spelt and wild rice"),
"1 big bunch of fresh mint , (60g)": (1, "big", "bunch of fresh mint ,", "60g"),
"1 large red onion": (1, "large", "red onion", ""),
# "2-3 TL Curry": (), # idk what it should use here either
"1 Zwiebel gehackt": (1, "Zwiebel", "gehackt", ""),
"1 EL Kokosöl": (1, "EL", "Kokosöl", ""),
"0.5 paket jäst (à 50 g)": (0.5, "paket", "jäst", "à 50 g"),
"ägg": (0, "", "ägg", ""),
"50 g smör eller margarin": (50, "g", "smör eller margarin", ""),
"3,5 l Wasser": (3.5, "l", "Wasser", ""),
"3.5 l Wasser": (3.5, "l", "Wasser", "")
}
# for German you could say that if an ingredient does not have an amount and it starts with a lowercase letter, then that is a unit ("etwas", "evtl.")
# does not apply to English tho
errors = 0
count = 0
for key, val in expectations.items():
count += 1
parsed = parse_ingredient(key)
self.assertNotEqual(val, parsed)

View File

@@ -1,16 +0,0 @@
<script type="application/ld+json"> {
"@context": "https://schema.org/",
"@type": "Recipe",
"name": "Selbstgemachter Schokopudding",
"author": "AP",
"image": "https://www.lidl-kochen.de/images/recipe/64590/selbstgemachter-schokopudding-144479.jpg",
"description": "Rezept für Selbstgemachter Schokopudding » Über 245x nachgekocht » 20min Zubereitung » 7 Zutaten » 473 kcal/Portion
",
"keywords": "Schokolade, Milch, Schlagsahne, Kuvertüre, schnell, einfach, günstig, lecker, leicht, Snack, glutenfrei, vegetarisch, unter 500 kcal, Sommer, Winter, Herbst, Frühling, Deutschland, Kinder, Familie, für Singles, für Studenten, kochen, ohne Backofen, für Paare, fürs Büro, für die Arbeit, beliebte Rezepte, Dessert", "recipeCuisine": "Deutschland", "cookTime": "PT20M",
"nutrition": {
"@type": "NutritionInformation",
"calories": "473 Kalorie"
},
"recipeInstructions": [{"@type":"HowToStep","text":"Schokolade fein hacken. In einem Topf 400 ml Milch, 150 g Sahne, Zucker, Salz und Schokolade langsam bei kleiner bis mittlerer Stufe unter Rühren erhitzen, bis die Schokolade geschmolzen ist. Anschließend aufkochen, dabei weiter rühren. \r\n\r\n"},{"@type":"HowToStep","text":"Übrige kalte Milch und Stärke in einer Schüssel mit einem Schneebesen gründlich verrühren. Kochende Milch vom Herd nehmen, angerührte Stärke einrühren und Topf zurück auf den Herd setzen. Unter Rühren ca. 1 Min. aufkochen, bis der Pudding dickflüssig ist. Vom Herd nehmen, in eine mit kaltem Wasser ausgespülte Schüssel füllen und erkalten lassen. \r\n\r\n"},{"@type":"HowToStep","text":"In einem hohen Gefäß übrige Sahne mit einem Handrührgerät mit Schneebesen steif schlagen. Pudding mit Sahne und Schokostreuseln garnieren und servieren.\r\n\r\nGuten Appetit!"}],
"recipeIngredient": ["Kuvertüre, zartbitter 150 g","Milch 450 ml","Schlagsahne 200 g","Zucker 75 g","Salz Prise","Speisestärke 30 g","Schokoladenstreusel, Zartbitter 2 EL"],
"recipeCategory": "Snack, Dessert, Andere" }</script>

View File

@@ -18,24 +18,27 @@ router.register(r'keyword', api.KeywordViewSet)
router.register(r'unit', api.UnitViewSet)
router.register(r'food', api.FoodViewSet)
router.register(r'step', api.StepViewSet)
router.register(r'recipe', api.RecipeViewSet)
router.register(r'ingredient', api.IngredientViewSet)
router.register(r'meal-plan', api.MealPlanViewSet)
router.register(r'meal-type', api.MealTypeViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
router.register(r'view-log', api.ViewLogViewSet)
urlpatterns = [
path('', views.index, name='index'),
path('setup/', views.setup, name='view_setup'),
path('signup/<slug:token>', views.signup, name='view_signup'),
path('system/', views.system, name='view_system'),
path('search/', views.search, name='view_search'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
path('shopping/', views.shopping_list, name='view_shopping'),
path('shopping/<int:pk>', views.shopping_list, name='view_shopping'),
path('settings/', views.user_settings, name='view_settings'),
path('history/', views.history, name='view_history'),
path('test/', views.test, name='view_test'),
@@ -70,8 +73,9 @@ urlpatterns = [
path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
path('api/sync_all/', api.sync_all, name='api_sync'),
path('api/log_cooking/<int:recipe_id>/', api.log_cooking, name='api_log_cooking'),
path('api/plan-ical/<slug:html_week>/', api.get_plan_ical, name='api_get_plan_ical'),
path('api/recipe-from-url/<path:url>/', api.recipe_from_url, name='api_recipe_from_url'),
path('api/plan-ical/<slug:from_date>/<slug:to_date>/', api.get_plan_ical, name='api_get_plan_ical'),
path('api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'),
path('api/backup/', api.get_backup, name='api_backup'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'),
@@ -90,7 +94,7 @@ urlpatterns = [
]
generic_models = (Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Comment, RecipeBookEntry, Keyword, Food)
generic_models = (Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink)
for m in generic_models:
py_name = get_model_name(m)

View File

@@ -9,10 +9,13 @@ from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None
from django.contrib import messages
from django.contrib.auth.models import User
from django.core import management
from django.core.files import File
from django.db.models import Q
from django.http import HttpResponse, FileResponse, JsonResponse
from django.shortcuts import redirect
from django.utils import timezone, dateformat
from django.utils.formats import date_format
from django.utils.translation import gettext as _
from django.views.generic.base import View
from icalendar import Calendar, Event
@@ -23,13 +26,14 @@ from rest_framework.parsers import JSONParser, FileUploadParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.viewsets import ViewSetMixin
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest, CustomIsShare
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest, CustomIsShare, CustomIsShared
from cookbook.helper.recipe_url_import import get_from_html
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword, Unit, SyncLog
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword, Unit, SyncLog, ShoppingListRecipe, ShoppingList, ShoppingListEntry
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer, IngredientSerializer, FoodSerializer, StepSerializer, \
KeywordSerializer, RecipeImageSerializer, StorageSerializer, SyncSerializer, SyncLogSerializer, UnitSerializer
KeywordSerializer, RecipeImageSerializer, StorageSerializer, SyncSerializer, SyncLogSerializer, UnitSerializer, ShoppingListSerializer, ShoppingListRecipeSerializer, ShoppingListEntrySerializer, ShoppingListEntryCheckedSerializer, \
ShoppingListAutoSyncSerializer
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
@@ -100,8 +104,12 @@ class StandardFilterMixin(ViewSetMixin):
queryset = queryset.filter(name__icontains=query)
limit = self.request.query_params.get('limit', None)
random = self.request.query_params.get('random', False)
if limit is not None:
queryset = queryset[:int(limit)]
if random:
queryset = queryset.random(int(limit))
else:
queryset = queryset[:int(limit)]
return queryset
@@ -146,19 +154,24 @@ class MealPlanViewSet(viewsets.ModelViewSet):
list:
optional parameters
- **html_week**: filter for a calendar week (format 2020-W24 as html input type week)
- **from_date**: filter from (inclusive) a certain date onward
- **to_date**: filter upward to (inclusive) certain date
"""
queryset = MealPlan.objects.all()
serializer_class = MealPlanSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated] # TODO fix permissions
def get_queryset(self):
queryset = MealPlan.objects.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).distinct().all()
week = self.request.query_params.get('html_week', None)
if week is not None:
y, w = week.replace('-W', ' ').split()
queryset = queryset.filter(date__week=w, date__year=y)
from_date = self.request.query_params.get('from_date', None)
if from_date is not None:
queryset = queryset.filter(date__gte=from_date)
to_date = self.request.query_params.get('to_date', None)
if to_date is not None:
queryset = queryset.filter(date__lte=to_date)
return queryset
@@ -199,7 +212,16 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = Recipe.objects.all()
serializer_class = RecipeSerializer
permission_classes = [CustomIsShare | CustomIsGuest] # TODO split read and write permission for meal plan guest
# TODO write extensive tests for permissions
def get_queryset(self):
internal = self.request.query_params.get('internal', None)
if internal:
self.queryset = self.queryset.filter(internal=True)
return super().get_queryset()
# TODO write extensive tests for permissions
@decorators.action(
detail=True,
@@ -229,6 +251,39 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return Response(serializer.errors, 400)
class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
queryset = ShoppingListRecipe.objects.all()
serializer_class = ShoppingListRecipeSerializer
permission_classes = [CustomIsUser, ] # TODO add custom validation
# TODO custom get qs
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
queryset = ShoppingListEntry.objects.all()
serializer_class = ShoppingListEntrySerializer
permission_classes = [CustomIsOwner, ] # TODO add custom validation
# TODO custom get qs
class ShoppingListViewSet(viewsets.ModelViewSet):
queryset = ShoppingList.objects.all()
serializer_class = ShoppingListSerializer
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
if self.request.user.is_superuser:
return self.queryset
return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).all()
def get_serializer_class(self):
autosync = self.request.query_params.get('autosync', None)
if autosync:
return ShoppingListAutoSyncSerializer
return self.serializer_class
class ViewLogViewSet(viewsets.ModelViewSet):
queryset = ViewLog.objects.all()
serializer_class = ViewLogSerializer
@@ -318,11 +373,14 @@ def log_cooking(request, recipe_id):
@group_required('user')
def get_plan_ical(request, html_week):
def get_plan_ical(request, from_date, to_date):
queryset = MealPlan.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).distinct().all()
y, w = html_week.replace('-W', ' ').split()
queryset = queryset.filter(date__week=w, date__year=y)
if from_date is not None:
queryset = queryset.filter(date__gte=from_date)
if to_date is not None:
queryset = queryset.filter(date__lte=to_date)
cal = Calendar()
@@ -336,13 +394,15 @@ def get_plan_ical(request, html_week):
cal.add_component(event)
response = FileResponse(io.BytesIO(cal.to_ical()))
response["Content-Disposition"] = f'attachment; filename=meal_plan_{html_week}.ics'
response["Content-Disposition"] = f'attachment; filename=meal_plan_{from_date}-{to_date}.ics'
return response
@group_required('user')
def recipe_from_url(request, url):
def recipe_from_url(request):
url = request.POST['url']
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'}
try:
response = requests.get(url, headers=headers)
@@ -352,3 +412,16 @@ def recipe_from_url(request, url):
if response.status_code == 403:
return JsonResponse({'error': True, 'msg': _('The requested page refused to provide any information (Status Code 403).')}, status=400)
return get_from_html(response.text, url)
def get_backup(request):
if not request.user.is_superuser:
return HttpResponse('', status=403)
buf = io.StringIO()
management.call_command('dumpdata', exclude=['contenttypes', 'auth'], stdout=buf)
response = FileResponse(buf.getvalue())
response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json'
return response

View File

@@ -3,9 +3,10 @@ from datetime import datetime
from io import BytesIO
import requests
from PIL import Image
from PIL import Image, UnidentifiedImageError
from django.contrib import messages
from django.core.files import File
from django.db.transaction import atomic
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import redirect, render
@@ -94,6 +95,7 @@ def batch_edit(request):
@group_required('user')
@atomic
def import_url(request):
if request.method == 'POST':
data = json.loads(request.body)
@@ -120,35 +122,44 @@ def import_url(request):
recipe.keywords.add(k)
for ing in data['recipeIngredient']:
f, f_created = Food.objects.get_or_create(name=ing['ingredient']['text'])
if ing['unit']:
u, u_created = Unit.objects.get_or_create(name=ing['unit']['text'])
else:
u = Unit.objects.get(name=request.user.userpreference.default_unit)
ingredient = Ingredient()
ingredient.food, f_created = Food.objects.get_or_create(name=ing['ingredient']['text'])
if ing['unit'] and ing['unit']['text'] != '':
ingredient.unit, u_created = Unit.objects.get_or_create(name=ing['unit']['text'])
# TODO properly handle no_amount recipes
if isinstance(ing['amount'], str):
try:
ing['amount'] = float(ing['amount'].replace(',', '.'))
ingredient.amount = float(ing['amount'].replace(',', '.'))
except ValueError:
# TODO return proper error
ingredient.no_amount = True
pass
elif isinstance(ing['amount'], float) or isinstance(ing['amount'], int):
ingredient.amount = ing['amount']
ingredient.note = ing['note'] if 'note' in ing else ''
step.ingredients.add(Ingredient.objects.create(food=f, unit=u, amount=ing['amount']))
ingredient.save()
step.ingredients.add(ingredient)
print(ingredient)
if data['image'] != '':
response = requests.get(data['image'])
img = Image.open(BytesIO(response.content))
if 'image' in data and data['image'] != '':
try:
response = requests.get(data['image'])
img = Image.open(BytesIO(response.content))
# todo move image processing to dedicated function
basewidth = 720
wpercent = (basewidth / float(img.size[0]))
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
# todo move image processing to dedicated function
basewidth = 720
wpercent = (basewidth / float(img.size[0]))
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
im_io = BytesIO()
img.save(im_io, 'PNG', quality=70)
recipe.image = File(im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png')
recipe.save()
im_io = BytesIO()
img.save(im_io, 'PNG', quality=70)
recipe.image = File(im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png')
recipe.save()
except UnidentifiedImageError:
pass
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))

View File

@@ -9,7 +9,7 @@ from django.views.generic import DeleteView
from cookbook.helper.permission_helper import group_required, GroupRequiredMixin, OwnerRequiredMixin
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeBook, \
RecipeBookEntry, MealPlan, Food
RecipeBookEntry, MealPlan, Food, InviteLink
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
@@ -129,7 +129,7 @@ class RecipeBookEntryDelete(GroupRequiredMixin, DeleteView):
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
if not (obj.book.created_by == request.user or request.user.is_superuser):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(RecipeBookEntryDelete, self).dispatch(request, *args, **kwargs)
@@ -148,3 +148,14 @@ class MealPlanDelete(OwnerRequiredMixin, DeleteView):
context = super(MealPlanDelete, self).get_context_data(**kwargs)
context['title'] = _("Meal-Plan")
return context
class InviteLinkDelete(OwnerRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = InviteLink
success_url = reverse_lazy('list_invite_link')
def get_context_data(self, **kwargs):
context = super(InviteLinkDelete, self).get_context_data(**kwargs)
context['title'] = _("Invite Link")
return context

View File

@@ -239,27 +239,33 @@ def edit_ingredients(request):
if units_form.is_valid():
new_unit = units_form.cleaned_data['new_unit']
old_unit = units_form.cleaned_data['old_unit']
recipe_ingredients = Ingredient.objects.filter(unit=old_unit).all()
for i in recipe_ingredients:
i.unit = new_unit
i.save()
if new_unit != old_unit:
recipe_ingredients = Ingredient.objects.filter(unit=old_unit).all()
for i in recipe_ingredients:
i.unit = new_unit
i.save()
old_unit.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
old_unit.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
else:
messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!'))
food_form = FoodMergeForm(request.POST, prefix=FoodMergeForm.prefix)
if food_form.is_valid():
new_food = food_form.cleaned_data['new_food']
old_food = food_form.cleaned_data['old_food']
ingredients = Ingredient.objects.filter(food=old_food).all()
for i in ingredients:
i.food = new_food
i.save()
if new_food != old_food:
ingredients = Ingredient.objects.filter(food=old_food).all()
for i in ingredients:
i.food = new_food
i.save()
old_food.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Foods merged!'))
old_food.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Foods merged!'))
else:
messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!'))
if success:
units_form = UnitMergeForm()

View File

@@ -23,7 +23,7 @@ def import_recipe(request):
form = ImportForm(request.POST)
if form.is_valid():
try:
data = json.loads(form.cleaned_data['recipe'])
data = json.loads(re.sub(r'"id":([0-9])+,', '', form.cleaned_data['recipe']))
sr = RecipeSerializer(data=data)
if sr.is_valid():

View File

@@ -1,13 +1,16 @@
from datetime import datetime
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.db.models.functions import Lower
from django.shortcuts import render
from django.utils.translation import gettext as _
from django_tables2 import RequestConfig
from cookbook.filters import IngredientFilter
from cookbook.filters import IngredientFilter, ShoppingListFilter
from cookbook.helper.permission_helper import group_required
from cookbook.models import Keyword, SyncLog, RecipeImport, Storage, Food
from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable, IngredientTable
from cookbook.models import Keyword, SyncLog, RecipeImport, Storage, Food, ShoppingList, InviteLink
from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable, IngredientTable, ShoppingListTable, InviteLinkTable
@group_required('user')
@@ -45,9 +48,27 @@ def food(request):
return render(request, 'generic/list_template.html', {'title': _("Ingredients"), 'table': table, 'filter': f})
@group_required('user')
def shopping_list(request):
f = ShoppingListFilter(request.GET, queryset=ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).all().order_by('finished', 'created_at'))
table = ShoppingListTable(f.qs)
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'generic/list_template.html', {'title': _("Shopping Lists"), 'table': table, 'filter': f, 'create_url': 'view_shopping'})
@group_required('admin')
def storage(request):
table = StorageTable(Storage.objects.all())
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'generic/list_template.html', {'title': _("Storage Backend"), 'table': table, 'create_url': 'new_storage'})
@group_required('admin')
def invite_link(request):
table = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None).all())
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'generic/list_template.html', {'title': _("Invite Links"), 'table': table, 'create_url': 'new_invite_link'})

View File

@@ -9,9 +9,9 @@ from django.utils.translation import gettext as _
from django.views.generic import CreateView
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \
RecipeBookForm, MealPlanForm
RecipeBookForm, MealPlanForm, InviteLinkForm
from cookbook.helper.permission_helper import GroupRequiredMixin, group_required
from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan, ShareLink, MealType, Step
from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan, ShareLink, MealType, Step, InviteLink
class RecipeCreate(GroupRequiredMixin, CreateView):
@@ -162,3 +162,21 @@ class MealPlanCreate(GroupRequiredMixin, CreateView):
context['default_recipe'] = Recipe.objects.get(pk=int(recipe))
return context
class InviteLinkCreate(GroupRequiredMixin, CreateView):
groups_required = ['admin']
template_name = "generic/new_template.html"
model = InviteLink
form_class = InviteLinkForm
def form_valid(self, form):
obj = form.save(commit=False)
obj.created_by = self.request.user
obj.save()
return HttpResponseRedirect(reverse('list_invite_link'))
def get_context_data(self, **kwargs):
context = super(InviteLinkCreate, self).get_context_data(**kwargs)
context['title'] = _("Invite Link")
return context

View File

@@ -1,13 +1,13 @@
import copy
import os
from datetime import datetime, timedelta
from uuid import UUID
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash, authenticate
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.db.models import Q, Avg
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
@@ -83,7 +83,8 @@ def recipe_view(request, pk, share=None):
if request.method == "POST":
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to perform this action!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to perform this action!'))
return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': recipe.pk, 'share': share}))
comment_form = CommentForm(request.POST, prefix='comment')
@@ -110,13 +111,19 @@ def recipe_view(request, pk, share=None):
comment_form = CommentForm()
bookmark_form = RecipeBookEntryForm()
user_servings = None
if request.user.is_authenticated:
if not ViewLog.objects.filter(recipe=recipe).filter(created_by=request.user).filter(created_at__gt=(timezone.now() - timezone.timedelta(minutes=5))).exists():
user_servings = CookLog.objects.filter(recipe=recipe, created_by=request.user,
servings__gt=0).all().aggregate(Avg('servings'))['servings__avg']
if request.user.is_authenticated:
if not ViewLog.objects.filter(recipe=recipe).filter(created_by=request.user).filter(
created_at__gt=(timezone.now() - timezone.timedelta(minutes=5))).exists():
ViewLog.objects.create(recipe=recipe, created_by=request.user)
return render(request, 'recipe_view.html',
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form,
'bookmark_form': bookmark_form, 'share': share})
'bookmark_form': bookmark_form, 'share': share, 'user_servings': user_servings})
@group_required('user')
@@ -158,50 +165,25 @@ def meal_plan_entry(request, pk):
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
same_day_plan = MealPlan.objects.filter(date=plan.date).exclude(pk=plan.pk).filter(Q(created_by=request.user) | Q(shared=request.user)).order_by('meal_type').all()
same_day_plan = MealPlan.objects.filter(date=plan.date).exclude(pk=plan.pk).filter(
Q(created_by=request.user) | Q(shared=request.user)).order_by('meal_type').all()
return render(request, 'meal_plan_entry.html', {'plan': plan, 'same_day_plan': same_day_plan})
@group_required('user')
def shopping_list(request):
markdown_format = True
def shopping_list(request, pk=None):
raw_list = request.GET.getlist('r')
if request.method == "POST":
form = ShoppingForm(request.POST)
if form.is_valid():
recipes = form.cleaned_data['recipe']
markdown_format = form.cleaned_data['markdown_format']
else:
recipes = []
else:
raw_list = request.GET.getlist('r')
recipes = []
for r in raw_list:
r = r.replace('[', '').replace(']', '')
if re.match(r'^([0-9])+,([0-9])+[.]*([0-9])*$', r):
rid, multiplier = r.split(',')
if recipe := Recipe.objects.filter(pk=int(rid)).first():
recipes.append({'recipe': recipe.id, 'multiplier': multiplier})
recipes = []
for r in raw_list:
if re.match(r'^([1-9])+$', r):
if Recipe.objects.filter(pk=int(r)).exists():
recipes.append(int(r))
markdown_format = False
form = ShoppingForm(initial={'recipe': recipes, 'markdown_format': False})
ingredients = []
for r in recipes:
for s in r.steps.all():
for ri in s.ingredients.exclude(unit__name__contains='Special:').all():
index = None
for x, ig in enumerate(ingredients):
if ri.food == ig.food and ri.unit == ig.unit:
index = x
if index:
ingredients[index].amount = ingredients[index].amount + ri.amount
else:
ingredients.append(ri)
return render(request, 'shopping_list.html', {'ingredients': ingredients, 'recipes': recipes, 'form': form, 'markdown_format': markdown_format})
return render(request, 'shopping_list.html', {'shopping_list_id': pk, 'recipes': recipes})
@group_required('guest')
@@ -228,6 +210,12 @@ def user_settings(request):
up.plan_share.set(form.cleaned_data['plan_share'])
up.ingredient_decimals = form.cleaned_data['ingredient_decimals']
up.comments = form.cleaned_data['comments']
up.use_fractions = form.cleaned_data['use_fractions']
up.shopping_auto_sync = form.cleaned_data['shopping_auto_sync']
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
up.save()
if 'user_name_form' in request.POST:
@@ -251,7 +239,9 @@ def user_settings(request):
if (api_token := Token.objects.filter(user=request.user).first()) is None:
api_token = Token.objects.create(user=request.user)
return render(request, 'settings.html', {'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form, 'api_token': api_token})
return render(request, 'settings.html',
{'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form,
'api_token': api_token})
@group_required('guest')
@@ -263,20 +253,24 @@ def history(request):
@group_required('admin')
def system(request):
postgres = False if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' else True
postgres = False if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' or
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql') else True
secret_key = False if os.getenv('SECRET_KEY') else True
return render(request, 'system.html', {'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres, 'version': VERSION_NUMBER, 'ref': BUILD_REF, 'secret_key': secret_key})
return render(request, 'system.html',
{'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres,
'version': VERSION_NUMBER, 'ref': BUILD_REF, 'secret_key': secret_key})
def setup(request):
if User.objects.count() > 0 or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS:
messages.add_message(request, messages.ERROR, _('The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.'))
messages.add_message(request, messages.ERROR, _(
'The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.'))
return HttpResponseRedirect(reverse('login'))
if request.method == 'POST':
form = SuperUserForm(request.POST)
form = UserCreateForm(request.POST)
if form.is_valid():
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']:
form.add_error('password', _('Passwords dont match!'))
@@ -296,11 +290,59 @@ def setup(request):
for m in e:
form.add_error('password', m)
else:
form = SuperUserForm()
form = UserCreateForm()
return render(request, 'setup.html', {'form': form})
def signup(request, token):
try:
token = UUID(token, version=4)
except ValueError:
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!'))
return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.method == 'POST':
form = UserCreateForm(request.POST)
if link.username != '':
data = dict(form.data)
data['name'] = link.username
form.data = data
if form.is_valid():
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']:
form.add_error('password', _('Passwords dont match!'))
else:
user = User(
username=form.cleaned_data['name'],
)
try:
validate_password(form.cleaned_data['password'], user=user)
user.set_password(form.cleaned_data['password'])
user.save()
messages.add_message(request, messages.SUCCESS, _('User has been created, please login!'))
link.used_by = user
link.save()
user.groups.add(link.group)
return HttpResponseRedirect(reverse('login'))
except ValidationError as e:
for m in e:
form.add_error('password', m)
else:
form = UserCreateForm()
if link.username != '':
form.fields['name'].initial = link.username
form.fields['name'].disabled = True
return render(request, 'registration/signup.html', {'form': form, 'link': link})
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('index'))
def markdown_info(request):
return render(request, 'markdown_info.html', {})

View File

@@ -1,15 +0,0 @@
This is a further example combining the power of nginx with the reverse proxy authentication service, [Authelia](https://github.com/authelia/authelia).
Please refer to the appropriate documentation on how to setup the reverse proxy, authentication, and networks.
Ensure users have been configured for Authelia, and that the endpoint that recipes is pointed to is protected, but available.
There is a good guide to the other additional files that need to be added to your Nginx set up at the [Authelia Docs](https://docs.authelia.com/deployment/supported-proxies/nginx.html).
Remember to add the appropriate environment variables to `.env` file:
```
VIRTUAL_HOST=
LETSENCRYPT_HOST=
LETSENCRYPT_EMAIL=
PROXY_HEADER=
```

View File

@@ -1,43 +0,0 @@
version: "3"
services:
db_recipes:
restart: always
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
networks:
- default
web_recipes:
image: vabene1111/recipes
restart: always
env_file:
- ./.env
volumes:
- ./staticfiles:/opt/recipes/staticfiles
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
networks:
- default
nginx_recipes:
image: nginx:mainline-alpine
restart: always
env_file:
- ./.env
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
networks:
- default
- nginx-proxy
networks:
default:
nginx-proxy:
external:
name: nginx-proxy

View File

@@ -1,37 +0,0 @@
server {
listen 80;
server_name localhost;
client_max_body_size 16M;
# serve static files
location /static/ {
alias /static/;
}
# serve media files
location /media/ {
alias /media/;
}
# Authelia endpoint for authentication requests
include /config/nginx/auth.conf;
# pass requests for dynamic content to gunicorn
location / {
proxy_set_header Host $host;
proxy_pass http://web_recipes:8080;
# Ensure Authelia is specifically required for this endpoint
# This line is important as it will return a 401 error if the user doesn't have access
include /config/nginx/authelia.conf;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header REMOTE-USER $user;
}
# Required to allow user to logout of authentication from within Recipes
# Ensure the <auth_endpoint> below is changed to actual the authentication url
location /accounts/logout/ {
return 301 http://<auth_endpoint>/logout
}
}

View File

@@ -1,11 +0,0 @@
This is a docker compose example when using [jwilder's nginx reverse proxy](https://github.com/jwilder/docker-gen)
in combination with [jrcs's letsencrypt companion](https://hub.docker.com/r/jrcs/letsencrypt-nginx-proxy-companion/).
Please refer to the appropriate documentation on how to setup the reverse proxy and networks.
Remember to add the appropriate environment variables to `.env` file:
```
VIRTUAL_HOST=
LETSENCRYPT_HOST=
LETSENCRYPT_EMAIL=
```

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