Compare commits

...

619 Commits

Author SHA1 Message Date
vabene1111
11678431e1 playing with validation 2025-04-26 23:35:12 +02:00
vabene1111
75c03706cb even smaller 2025-04-26 23:24:25 +02:00
vabene1111
7232c9373f make success snackbar less obstrusive 2025-04-26 23:23:50 +02:00
vabene1111
06920807c6 meal plan type name in SLE trunction indicator + improved click area in SLE 2025-04-26 23:12:22 +02:00
vabene1111
ccae32f21c use proper local format for all date pickers 2025-04-26 23:02:29 +02:00
vabene1111
eaad4e21e4 search page saves filters, tweaked reset 2025-04-26 23:00:02 +02:00
vabene1111
6b4b65109d added search page to side nav 2025-04-26 22:46:31 +02:00
vabene1111
441e9ea887 fixed model select create indicator in dark mode 2025-04-26 22:46:21 +02:00
vabene1111
25c369d400 cleanup in frontend localization system 2025-04-26 22:40:53 +02:00
vabene1111
491b5beded fixed using correct file for fallback locale 2025-04-26 22:32:02 +02:00
vabene1111
13c26d199b even more changes 2025-04-26 21:47:49 +02:00
vabene1111
a02722a525 PDF js trial and error 2025-04-26 21:43:47 +02:00
vabene1111
2553bea835 possible pathing fix 2025-04-26 21:39:52 +02:00
vabene1111
302528256c fixed external recipe viewer 2025-04-26 18:53:14 +02:00
vabene1111
ca28a44743 fixed rating half increments 2025-04-26 17:59:58 +02:00
vabene1111
32fd6e3827 fixed number input precision 2025-04-26 17:53:46 +02:00
vabene1111
ec4fa50012 fixed search page 2025-04-26 17:48:59 +02:00
vabene1111
885a316955 Merge branch 'feature/vue3' of https://github.com/vabene1111/recipes into feature/vue3 2025-04-25 16:50:05 +02:00
vabene1111
cb1d45b625 small fixes 2025-04-25 16:50:01 +02:00
vabene1111
41d4728c89 Merge pull request #3637 from wilmardo/setup-status-root-env
feat: introduce STATIC_ROOT env var
2025-04-25 16:11:26 +02:00
vabene1111
4a9082b70c small tweaks 2025-04-18 13:48:41 +02:00
vabene1111
c1e56920ec prepared test view 2025-04-18 13:31:15 +02:00
vabene1111
fe64da0841 fixed adding recipes with headers to shopping 2025-04-18 13:29:29 +02:00
vabene1111
6bf605f98e show info when AI key is not configured 2025-04-18 13:28:37 +02:00
vabene1111
e18b0ad049 disable ai import if no key is set 2025-04-18 13:23:47 +02:00
vabene1111
d7f37e8293 update dependencies and improved/fixed meal plan date picker 2025-04-18 13:13:13 +02:00
vabene1111
f576aa34e4 fixed use servings when adding recipe to meal plan 2025-04-18 10:58:45 +02:00
vabene1111
b08c119e57 Update StepEditor.vue 2025-04-11 16:47:34 +02:00
vabene1111
052219e141 added AI text import 2025-04-11 15:49:11 +02:00
vabene1111
604d18d594 fixed youtube importer by setting defaults 2025-04-11 15:12:51 +02:00
vabene1111
f23d4a4188 fixed missing description in recipe viewer 2025-04-11 14:47:22 +02:00
vabene1111
88f2177e9b fixed multi ingerdient parser 2025-04-11 14:47:09 +02:00
vabene1111
675d7a0647 model select show icon only on mobile for create indicator 2025-04-11 14:12:37 +02:00
vabene1111
781d8845be fixed more tab index problems and number select precision 2025-04-11 14:11:38 +02:00
vabene1111
c2fdf4812c Merge pull request #3645 from mikhail5555/feature/improved-multi-select-for-steps-edit
add small chip indicating if you are selecting existing item or creaing a new one
2025-04-11 14:05:25 +02:00
Mikhail Epifanov
ae1532e509 also add template for the clear button to disable tabindex 2025-04-11 08:48:59 +02:00
Mikhail Epifanov
121d2471a7 only show the new/existing template when creating is actually allowed 2025-04-11 08:21:36 +02:00
Mikhail Epifanov
278342f3f0 add small chip indicating if you are selecting existing item or creating a new one 2025-04-10 22:38:24 +02:00
vabene1111
608526b348 small fixes 2025-04-10 18:04:13 +02:00
wilmarguida
a0ba1ecfae feat: introduce STATIC_ROOT env var
Signed-off-by: wilmarguida <w.denouden@guida.nl>
2025-04-08 18:23:12 +02:00
vabene1111
67f63730a3 changed build for alpha release 2025-04-06 18:12:43 +02:00
vabene1111
eeb3b2e5d5 docs for AI feature 2025-04-06 17:42:17 +02:00
vabene1111
7314572fc0 various fixes and improvements 2025-04-04 22:00:10 +02:00
vabene1111
934d78c50e first draft of help system 2025-04-02 20:37:01 +02:00
vabene1111
431eb7baf7 visiual AI import improvements 2025-04-02 19:18:01 +02:00
vabene1111
e0c8895733 AI import improvements 2025-04-02 18:40:29 +02:00
vabene1111
b18a1d0110 tweaks and changes 2025-04-02 09:01:52 +02:00
vabene1111
838ce6615b translations 2025-04-01 11:42:33 +02:00
vabene1111
e0cc42653d before custom filter transform function 2025-03-31 20:33:08 +02:00
vabene1111
74b940d4eb save and load of custom filter WIP 2025-03-31 20:24:32 +02:00
vabene1111
c03e82f094 various improvements 2025-03-31 16:06:20 +02:00
vabene1111
235c5d6b4a fixed tests, cleanup, frontend fix 2025-03-31 09:45:50 +02:00
vabene1111
42e6e0bc50 concept for custom filter conversion, continue later 2025-03-31 09:20:13 +02:00
vabene1111
f7eabfe458 implemented all filters in frontend 2025-03-31 08:58:32 +02:00
vabene1111
988dcd1522 build proper date transformer for route query binding 2025-03-31 08:48:27 +02:00
vabene1111
22aa0d2cb7 WIP search date based filters 2025-03-30 14:00:29 +02:00
vabene1111
dd1975817e WIP search page 2025-03-30 00:08:25 +01:00
vabene1111
77195718d8 working filter for food/keyword/book 2025-03-29 13:38:53 +01:00
vabene1111
ebd354bc8d search prototype with keyword filters 2025-03-29 12:09:19 +01:00
vabene1111
aa1fa3a40e testing useRouteQuery for SearchPage 2025-03-29 09:09:07 +01:00
vabene1111
2add3b70a4 updated dependencies + added vueUse Router plugin 2025-03-29 09:08:49 +01:00
vabene1111
d38a4a2e7e update search when query param updates 2025-03-29 08:51:54 +01:00
vabene1111
8827e6f453 fixed shopping undo hanging until auto refresh 2025-03-27 18:35:18 +01:00
vabene1111
742297c1fc fixed unsaved change warning after confirming editor close 2025-03-27 17:39:35 +01:00
vabene1111
7ae66a14be Merge branch 'develop' into feature/vue3 2025-03-27 17:17:54 +01:00
vabene1111
0cf89ca33c Merge pull request #3614 from stritti/feature/mobile-apps-update
docs: mobile apps updated
2025-03-27 17:14:03 +01:00
Stephan Strittmatter
68ceada28b chore: mobile apps updated 2025-03-25 11:16:18 +00:00
Vlad
192ca44d89 Translated using Weblate (Ukrainian)
Currently translated at 44.7% (255 of 570 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/uk/
2025-03-23 15:07:19 +00:00
vabene1111
421dab2405 Merge branch 'develop' into feature/vue3 2025-03-22 11:15:18 +01:00
vabene1111
3204aaf161 Merge pull request #3607 from TandoorRecipes/dependabot/pip/gunicorn-23.0.0
Bump gunicorn from 22.0.0 to 23.0.0
2025-03-22 09:33:08 +01:00
dependabot[bot]
6cf6791fe6 Bump gunicorn from 22.0.0 to 23.0.0
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 22.0.0 to 23.0.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/22.0.0...23.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-22 03:20:08 +00:00
vabene1111
0ff16f280e better number inputs 2025-03-21 22:30:26 +01:00
vabene1111
a297e4731c switched to ingredients in property editor 2025-03-21 22:03:13 +01:00
vabene1111
de5ac9181f property view unit conversion/edit 2025-03-21 21:22:32 +01:00
vabene1111
fbab90e954 basic property viewer 2025-03-21 20:28:26 +01:00
vabene1111
05472b5a29 link to recipe in property editor 2025-03-21 19:08:17 +01:00
vabene1111
d6839c5dfa fdc search in property editor 2025-03-21 19:05:06 +01:00
vabene1111
dea4fa000e playing with FDC search integration 2025-03-20 22:09:44 +01:00
vabene1111
c36eaf934f enable food property deletion 2025-03-20 21:33:51 +01:00
vabene1111
4a13cc63ae add calculator to property editore 2025-03-20 21:27:45 +01:00
vabene1111
96e0be0a78 property editor 2025-03-20 21:18:50 +01:00
vabene1111
5f190bdc6c import improvements 2025-03-20 18:26:28 +01:00
vabene1111
73c376427c some fixes 2025-03-20 16:16:09 +01:00
vabene1111
d34f39a9e0 very basic app import working 2025-03-19 19:42:05 +01:00
vabene1111
8ae9de580d improve import stepper 2025-03-19 18:06:51 +01:00
vabene1111
a144f347f8 downgraded dependency for real 2025-03-17 17:28:21 +01:00
vabene1111
f43788d676 update lock 2025-03-17 08:41:53 +01:00
vabene1111
cac00423a2 downgrade again 2025-03-17 07:47:30 +01:00
vabene1111
32429af5e9 testing static import of mealplanpage 2025-03-17 07:43:04 +01:00
vabene1111
792c3a07e3 default view mode grid 2025-03-16 19:50:21 +01:00
vabene1111
2879fa466e search page improvements 2025-03-16 19:39:18 +01:00
vabene1111
9eed6693b4 book recipe open action 2025-03-16 16:37:26 +01:00
vabene1111
c4c6eb3ca6 route lazy loading + route naming improvements 2025-03-16 16:25:55 +01:00
vabene1111
e29f318453 dont run autosync when not on page 2025-03-16 16:11:08 +01:00
vabene1111
8eb2ba9512 applied custom openapi generator fix for omit generation 2025-03-16 15:38:37 +01:00
vabene1111
e9f87bb475 fixed share link generation 2025-03-16 15:22:10 +01:00
vabene1111
8b4e6ac5ae show metadata on recipe view 2025-03-16 15:07:06 +01:00
vabene1111
ee0f652981 shopping / meal plan improvements 2025-03-16 13:36:42 +01:00
vabene1111
7f06f888df ci dependencies 2025-03-16 13:14:41 +01:00
vabene1111
54f43c7938 Merge branch 'develop' into feature/vue3
# Conflicts:
#	.github/workflows/ci.yml
2025-03-16 13:14:15 +01:00
vabene1111
6ebb18a932 more ci dependency tests 2025-03-16 13:11:28 +01:00
vabene1111
7bde1bf44f split migrations, CI denendencies 2025-03-16 13:08:06 +01:00
vabene1111
e00c6459ab ci dependencies 2025-03-16 12:57:15 +01:00
vabene1111
3075ec066f Merge pull request #3595 from TandoorRecipes/dependabot/npm_and_yarn/vue/babel/runtime-7.26.10
Bump @babel/runtime from 7.22.15 to 7.26.10 in /vue
2025-03-16 12:53:55 +01:00
vabene1111
3ddc3c3768 Merge pull request #3594 from TandoorRecipes/dependabot/npm_and_yarn/vue/babel/helpers-7.26.10
Bump @babel/helpers from 7.22.15 to 7.26.10 in /vue
2025-03-16 12:53:46 +01:00
vabene1111
4d8d4af42a added CI dependencies 2025-03-14 14:23:57 +01:00
vabene1111
1fd0028351 shopping list fixes 2025-03-14 14:07:33 +01:00
vabene1111
f59c5ae16e Merge branch 'develop' into feature/vue3
# Conflicts:
#	cookbook/serializer.py
2025-03-14 13:53:47 +01:00
vabene1111
7b7b726ec5 migration to store space and created_by on ShoppingListRecipe
ATTENTION: This deletes all old shopping list recipes that do not have entries associated with it. This should not matter but just for the record
2025-03-14 13:51:49 +01:00
vabene1111
9c1d161785 wip 2025-03-14 11:39:31 +01:00
vabene1111
5f542e9ce6 Merge branch 'develop' into feature/vue3
# Conflicts:
#	Dockerfile
2025-03-14 11:21:30 +01:00
vabene1111
5484506bc3 build requirements 2025-03-14 10:41:03 +01:00
vabene1111
2e71bc4505 Merge branch 'develop' into feature/vue3
# Conflicts:
#	requirements.txt
2025-03-14 10:32:22 +01:00
vabene1111
136f05b886 requirments for django allauth 2025-03-14 10:31:42 +01:00
dependabot[bot]
83cf03a10b Bump @babel/runtime from 7.22.15 to 7.26.10 in /vue
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.22.15 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 09:17:29 +00:00
dependabot[bot]
7b56719716 Bump @babel/helpers from 7.22.15 to 7.26.10 in /vue
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.22.15 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 09:17:26 +00:00
vabene1111
348296763d Merge branch 'develop' 2025-03-14 10:15:46 +01:00
vabene1111
5c7a57ac32 added missing dependency 2025-03-14 10:15:40 +01:00
Jader Moraes
0c0b8ea455 Translated using Weblate (Portuguese (Brazil))
Currently translated at 66.5% (325 of 488 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pt_BR/
2025-03-13 22:58:38 +00:00
Jader Moraes
2f20f43efe Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (570 of 570 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2025-03-13 22:58:38 +00:00
vabene1111
cf6435503c show user files and sub recipes 2025-03-12 20:49:04 +01:00
vabene1111
752e98275d improve search/start page 2025-03-12 20:12:02 +01:00
vabene1111
40ee21a613 Merge branch 'develop' into feature/vue3 2025-03-12 17:54:13 +01:00
Nico G
1662b793a5 Translated using Weblate (Catalan)
Currently translated at 84.8% (414 of 488 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ca/
2025-03-12 09:58:39 +00:00
vabene1111
49d1946f29 shopping input test 2025-03-02 08:32:58 +01:00
vabene1111
7c448a5362 fixed tabs hiding on mobile in editors 2025-03-02 07:48:04 +01:00
vabene1111
06c40b6c80 fixed SLI grouping entries with different delay/check state 2025-03-02 07:41:58 +01:00
vabene1111
63f24e75b3 added created_by scroller to start page 2025-03-02 07:12:24 +01:00
vabene1111
87f4068736 fixed home redirect and added another random horizontal scroler 2025-03-02 06:58:48 +01:00
vabene1111
d23139cd8a playing around 2025-02-17 21:18:56 +01:00
vabene1111
f59a450448 playing with custom filter 2025-02-17 21:12:43 +01:00
vabene1111
e14bb78ed1 WIP custom filter usage 2025-02-17 20:48:04 +01:00
vabene1111
2c89add187 query params advanced search 2025-02-15 12:50:54 +01:00
vabene1111
0461c57cf3 usable books page 2025-02-15 09:50:40 +01:00
vabene1111
09a76a2057 books page 2025-02-14 16:43:18 +01:00
vabene1111
741220aefe model editors default prop 2025-02-14 16:43:12 +01:00
vabene1111
2917d6ffcf remove imghdr import 2025-02-12 17:29:48 +01:00
vabene1111
6c73ca5b45 fixed architecture check 2025-02-12 10:46:02 +01:00
vabene1111
79658e2f7c print arch 2025-02-12 10:41:20 +01:00
vabene1111
f335c3f755 add correct command 2025-02-12 09:57:54 +01:00
vabene1111
86fe129eba packages 2025-02-12 09:47:11 +01:00
vabene1111
081104e655 missing && 2025-02-12 09:43:42 +01:00
vabene1111
96d997a817 install rust for arm runs 2025-02-12 09:17:57 +01:00
vabene1111
b0821a9c6b trying with build-base 2025-02-11 14:52:18 +01:00
vabene1111
f178790709 only binary 2025-02-11 14:33:18 +01:00
vabene1111
425b71984a updated psycopg2 2025-02-11 14:09:06 +01:00
vabene1111
3cb2cf5333 changed docker python version and print pip debug 2025-02-11 13:04:44 +01:00
vabene1111
9c0dc64a47 baisc implementation of books WIP 2025-02-10 17:35:14 +01:00
vabene1111
4a9bd3626e switched to litellm 2025-02-10 17:25:55 +01:00
vabene1111
23a561e8cd added simple comment view 2025-02-09 15:25:52 +01:00
vabene1111
2a2f1033d0 try installing grpcio via package 2025-02-09 12:30:26 +01:00
vabene1111
4234c5b35f allow opening shared recipes and redirect old urls 2025-02-09 12:18:06 +01:00
vabene1111
1b09234e91 fixed import page share intent 2025-02-09 09:32:16 +01:00
vabene1111
9b5878faae antoher grpcio test 2025-02-09 09:24:36 +01:00
vabene1111
e27f4c5768 added build base for grcpio build 2025-02-07 15:22:48 +01:00
vabene1111
a1f1bc19e5 updated alpine and added linux-headers 2025-02-06 20:26:59 +01:00
vabene1111
f6983993d5 fixed model select slots taking up space 2025-02-06 18:11:47 +01:00
vabene1111
a868860b7d lots of shopping list improvements 2025-02-06 17:51:42 +01:00
vabene1111
75eff42329 downgrade grpcio 2025-02-01 14:33:20 +01:00
vabene1111
d0703f9639 support fraction display 2025-02-01 08:51:13 +01:00
vabene1111
7b2ade5fcd update wheel and setup tools 2025-02-01 08:51:04 +01:00
vabene1111
28c839a59d potentially fixed ingredient render component 2025-02-01 08:38:35 +01:00
vabene1111
1ead724af6 Merge branch 'develop' into feature/vue3
# Conflicts:
#	recipes/settings.py
#	requirements.txt
2025-02-01 08:17:14 +01:00
vabene1111
3880f205b6 various fixes 2025-01-30 10:29:21 +01:00
vabene1111
fcbc4cb792 screen wake lock 2025-01-29 20:28:40 +01:00
vabene1111
f5837fdbd2 Merge branch 'develop' into feature/vue3 2025-01-29 20:22:04 +01:00
vabene1111
e0b414d8e9 playing with AI import 2025-01-20 09:40:22 +01:00
vabene1111
a56c7c29a6 disable quickimport if no data is present 2025-01-19 19:35:54 +01:00
vabene1111
54f2e26f93 quick import 2025-01-19 19:32:30 +01:00
vabene1111
842f5bed81 improved import page actions 2025-01-19 19:28:26 +01:00
vabene1111
1e5c656e51 fixed RecipeImageSerializer 2025-01-19 19:28:19 +01:00
vabene1111
539e01f97d improved shopping line item dialog 2025-01-19 19:16:31 +01:00
vabene1111
503a4881fe Merge branch 'develop' into feature/vue3 2025-01-19 18:44:14 +01:00
vabene1111
04181fdda7 update nextcloud import docs 2025-01-19 18:44:01 +01:00
vabene1111
316fd65ab8 fixed another test 2025-01-19 18:43:52 +01:00
vabene1111
e8cbd91baf delete obsolete tests + fixes 2025-01-19 18:35:23 +01:00
vabene1111
cba80aeac9 fixed serializer and step editor 2025-01-18 14:46:19 +01:00
vabene1111
efa9e8aa3b ingredient step sorting dialog and headline option on desktop 2025-01-18 14:32:20 +01:00
vabene1111
d294341926 fixed list groups 2025-01-18 13:28:14 +01:00
vabene1111
35dce661bf Merge branch 'develop' into feature/vue3
# Conflicts:
#	requirements.txt
2025-01-18 12:49:25 +01:00
vabene1111
1d2bfb4462 Merge branch 'develop' into feature/vue3 2025-01-18 11:33:57 +01:00
vabene1111
749f2bff02 fixed path reload matching 2025-01-16 07:39:46 +01:00
vabene1111
cd8f8bb90c fixed manifest and added v3 redirect 2025-01-14 19:17:52 +01:00
vabene1111
3a031bbbaf shortcut icons 2025-01-14 19:03:20 +01:00
vabene1111
f4b1acb757 changed paths to have vue3 as standard 2025-01-14 18:18:13 +01:00
vabene1111
3e55e04fbd tweak limit warnings 2025-01-13 17:56:56 +01:00
vabene1111
1822a62e14 allow ingredient delete during import 2025-01-13 17:24:07 +01:00
vabene1111
2f991b5557 small fixes 2025-01-13 17:15:16 +01:00
vabene1111
827efbd783 improter improvements + nav 2025-01-05 20:28:12 +01:00
vabene1111
9573a68bfe Merge branch 'develop' into feature/vue3 2025-01-05 16:06:55 +01:00
vabene1111
c75d265686 many editor improvements (and more) 2025-01-04 17:59:56 +01:00
vabene1111
c691d6028b removed debug logs 2025-01-04 16:09:37 +01:00
vabene1111
c073512b17 model list page table fixes 2025-01-04 16:01:28 +01:00
vabene1111
dc827df95c ModelListView Pagination 2025-01-04 15:20:10 +01:00
vabene1111
b8db54d198 first steps to advanced search 2025-01-03 23:14:16 +01:00
vabene1111
dc58b42f68 improved startpage 2025-01-03 22:14:38 +01:00
vabene1111
9eca4673cd small tweaks 2025-01-03 20:49:12 +01:00
vabene1111
ac41a55d4f ingredient editor improvements 2025-01-02 18:02:54 +01:00
vabene1111
741c05a519 Basic ingredient editor 2025-01-02 14:56:18 +01:00
vabene1111
ac2d78f7a5 food editor model append to body 2025-01-02 11:12:52 +01:00
vabene1111
a4a93c5f4a refresh list after merge 2025-01-02 11:11:13 +01:00
vabene1111
05e507278e merge & automate 2025-01-02 11:07:05 +01:00
vabene1111
cab1641b33 fully generic merge dialog 2025-01-02 10:44:31 +01:00
vabene1111
9e99edf6f6 shopping fixes 2025-01-02 09:39:47 +01:00
vabene1111
2841e086df improved ingredient table 2025-01-02 09:34:12 +01:00
vabene1111
00ae511076 various meal plan fixes 2025-01-02 09:21:52 +01:00
vabene1111
f97d8ffdfd shopping load with pagination 2025-01-01 13:44:07 +01:00
vabene1111
34f337290c fixed comment 2025-01-01 10:07:49 +01:00
vabene1111
8159838fc3 warn before leaving model edit page 2025-01-01 10:07:27 +01:00
vabene1111
60f2494eae allow router back while snackbar is running 2025-01-01 08:56:29 +01:00
vabene1111
303f999b74 test include async results in global search 2025-01-01 08:51:34 +01:00
vabene1111
f83daf154f removed tests for no longer existing signal 2025-01-01 08:22:49 +01:00
vabene1111
55ee75fed1 ingredient checkboxes and table style 2024-12-31 16:44:10 +01:00
vabene1111
846a4796e2 recipe view as model chain and ingredient checkbox 2024-12-31 15:23:44 +01:00
vabene1111
545c464f4f very very basic merge dialog working 2024-12-30 18:43:10 +01:00
vabene1111
fabf0c28e3 WIP merge button 2024-12-30 17:07:38 +01:00
vabene1111
e219f7e07c fixed SLE create 2024-12-30 14:49:34 +01:00
vabene1111
830ce14c65 Merge branch 'develop' into feature/vue3
# Conflicts:
#	.gitignore
#	recipes/settings.py
2024-12-30 14:48:10 +01:00
vabene1111
c1bfa563a3 shopping from meal plan edit 2024-12-28 23:26:22 +01:00
vabene1111
bf4fc9a7aa basic shopping view in MealPlanEditor 2024-12-28 21:56:54 +01:00
vabene1111
4312b5ad65 mobile ingredient edit improvements 2024-12-28 18:36:26 +01:00
vabene1111
6fc078090a improved shopping 2024-12-28 15:24:07 +01:00
vabene1111
a4548abc19 shopping line item number formatting 2024-12-28 15:13:37 +01:00
vabene1111
1a39066b45 Revert "removed more unused stuff"
This reverts commit a0ac379b3c.
2024-12-28 15:11:34 +01:00
vabene1111
9cf1435130 Revert "removed dependency from settings"
This reverts commit f5cb393138.
2024-12-28 15:11:30 +01:00
vabene1111
e895bda80b Revert "remove unneeded dependencies"
This reverts commit b93f8e5e95.
2024-12-28 15:11:27 +01:00
vabene1111
882f18ac29 open edit page after model creation 2024-12-28 15:09:07 +01:00
vabene1111
ebfe5eee6d re added for now 2024-12-28 14:50:54 +01:00
vabene1111
afac08dc01 remove dependency import 2024-12-28 14:45:58 +01:00
vabene1111
a0ac379b3c removed more unused stuff 2024-12-28 14:41:00 +01:00
vabene1111
f5cb393138 removed dependency from settings 2024-12-28 14:33:06 +01:00
vabene1111
b93f8e5e95 remove unneeded dependencies 2024-12-28 14:17:31 +01:00
vabene1111
38c8e1b84f display external recipes 2024-12-28 14:15:44 +01:00
vabene1111
275c879e62 search and recipe improvements 2024-12-28 13:05:48 +01:00
vabene1111
cde632241b meal plan and recipe editor improvements 2024-12-28 12:55:20 +01:00
vabene1111
ea1e47e579 Merge branch 'develop' into feature/vue3 2024-12-28 11:23:12 +01:00
vabene1111
07b399cd80 ignore build staticfiles gitignore 2024-12-28 08:35:28 +01:00
vabene1111
ed20cf3df4 ignore build staticfiles 2024-12-28 08:35:19 +01:00
vabene1111
8a4e32f05e disable templating for now 2024-12-23 15:18:55 +01:00
vabene1111
5a8b4fb4ce minor tweaks 2024-12-23 14:49:07 +01:00
vabene1111
65034e523f fixed global search dialog manual select 2024-12-23 14:43:34 +01:00
vabene1111
0c6637406a layout fixes 2024-12-23 14:10:38 +01:00
vabene1111
4b0d022db1 meal plan tweaks 2024-12-23 13:53:44 +01:00
vabene1111
7a2b3c2d2e some styling stuff 2024-12-23 12:28:42 +01:00
vabene1111
205eae785e working add to shopping dialog 2024-12-23 12:07:28 +01:00
vabene1111
e0223e0c5c wip shopping dialog 2024-12-22 18:36:56 +01:00
vabene1111
d611391bea recipe context actions 2024-12-22 17:18:47 +01:00
vabene1111
0c547353cd add to shopping from meal plan editor 2024-12-22 15:36:58 +01:00
vabene1111
5ce859f267 space switcher 2024-12-22 14:06:55 +01:00
vabene1111
8e0da93476 fixed model switcher 2024-12-22 13:55:04 +01:00
vabene1111
3f7c22cfe0 fixed image import in URL import 2024-12-22 13:18:46 +01:00
vabene1111
55ee575c9c file upload in UserFileEditor and RecipeEditor 2024-12-22 13:11:48 +01:00
vabene1111
de6627ab32 Merge branch 'develop' into feature/vue3 2024-12-22 12:13:09 +01:00
vabene1111
9b586ae709 Merge branch 'develop' into feature/vue3 2024-12-22 12:05:53 +01:00
vabene1111
64a43d3d40 basic user file upload working 2024-12-21 16:03:13 +01:00
vabene1111
679957b48c vue 3 permission generally set to guest 2024-12-21 10:33:55 +01:00
vabene1111
0399763888 new recipe fix 2024-12-19 21:22:39 +01:00
vabene1111
3e3780028b improved code quality of start page scrollers 2024-12-19 21:16:15 +01:00
vabene1111
1c9d19fc76 fixed navigating with global search 2024-12-19 20:21:00 +01:00
vabene1111
ecdb1e9fee fixed userpreference test and added user info to recipe endpoint 2024-12-19 20:08:13 +01:00
vabene1111
b9e5126ab4 recipe view tweaks 2024-12-19 20:03:52 +01:00
vabene1111
a34fddc866 improved URL import 2024-12-19 19:35:21 +01:00
vabene1111
9b3bfd3d1c url import fully functioning 2024-12-12 18:27:21 +01:00
vabene1111
74000551e6 fix recipe import test 2024-12-12 18:09:50 +01:00
vabene1111
325ee16d39 importer stuff 2024-12-12 17:26:01 +01:00
vabene1111
37761bf6e7 split, merge and auto sort steps in importer 2024-12-11 22:01:21 +01:00
vabene1111
ad57ce7790 import stuff 2024-12-11 20:56:27 +01:00
vabene1111
f1f907ee33 fixed error in account settings 2024-12-08 22:04:06 +01:00
vabene1111
e3f20459dd basic url importer working 2024-12-08 21:54:14 +01:00
vabene1111
da567a9d6c small improvements 2024-12-08 17:16:38 +01:00
vabene1111
01a4fb57df moved food property to seperate component and re-used that for the recipe editor 2024-12-08 16:47:03 +01:00
vabene1111
01d52143e8 recipe editor 2024-12-05 18:00:13 +01:00
vabene1111
35461ba926 cleanup 2024-12-04 18:26:00 +01:00
vabene1111
b9f49cad45 ingredient from string API 2024-12-04 18:21:19 +01:00
vabene1111
d7487c6d5c step editor WIP 2024-12-04 17:39:22 +01:00
vabene1111
15122387f4 lots of improvements to recipe editor 2024-12-04 16:55:50 +01:00
vabene1111
931064a695 playing with a step timeline 2024-12-01 16:25:57 +01:00
vabene1111
49c8a5a375 recipe editor in model editor system basics 2024-12-01 16:02:32 +01:00
vabene1111
ab9f9701d8 general layout improvements 2024-12-01 15:35:40 +01:00
vabene1111
da4abcfce2 shopping recipe scaling 2024-12-01 14:46:31 +01:00
vabene1111
4149549c88 shopping supermarket quick edit 2024-12-01 14:27:08 +01:00
vabene1111
fa8cd4a2f0 shopping cleanup 2024-12-01 13:16:45 +01:00
vabene1111
423dc7a6bf lots of shopping tweaks 2024-12-01 12:32:16 +01:00
vabene1111
557954a259 added stats 2024-11-30 11:41:46 +01:00
vabene1111
38b72c8c72 shopping list recipes 2024-11-30 11:34:35 +01:00
vabene1111
3f395e816f working shopping list 2024-11-30 11:18:54 +01:00
vabene1111
6dcc77243a small fix 2024-11-28 15:31:05 +01:00
vabene1111
110ffe52b7 several shopping fixes 2024-11-27 16:52:48 +01:00
vabene1111
2fab51ea9b Merge branch 'develop' into feature/vue3
# Conflicts:
#	cookbook/views/api.py
2024-11-27 15:57:39 +01:00
vabene1111
425f00860f Merge branch 'develop' into feature/vue3
# Conflicts:
#	docs/features/authentication.md
2024-11-27 15:56:28 +01:00
vabene1111
d91a3080d5 autosync WIP 2024-11-25 18:36:11 +01:00
vabene1111
0485e5192b shopping list improvements 2024-11-25 16:50:56 +01:00
vabene1111
7531c83379 shopping list delay fix and category change implemented 2024-11-19 16:32:26 +01:00
vabene1111
3aca96148d Merge branch 'develop' into feature/vue3
# Conflicts:
#	cookbook/views/api.py
#	cookbook/views/views.py
#	requirements.txt
2024-11-12 16:45:21 +01:00
vabene1111
ba401877e8 strange 2024-10-27 14:40:33 +01:00
vabene1111
77748a951b various improvements for shopping line item dialog 2024-10-23 18:23:48 +02:00
vabene1111
4692526e48 shopping line item dialog WIP 2024-10-22 18:31:43 +02:00
vabene1111
eb4b8555c2 closable card title component 2024-10-22 18:31:36 +02:00
vabene1111
6a12ca78d1 recipe view shopping 2024-10-20 14:00:19 +02:00
vabene1111
2ed53f8e50 lots of shopping improvements 2024-10-20 12:47:44 +02:00
vabene1111
615e3bfa3d ugly but working shopping list 2024-10-17 20:41:39 +02:00
vabene1111
e406bdbc2c vuetify 2024-10-17 17:15:44 +02:00
vabene1111
7fd402aade lots of work on shopping store 2024-10-16 20:58:56 +02:00
vabene1111
e8522a4a6d disable model editor buttons when loadign 2024-10-16 19:16:59 +02:00
vabene1111
eb872ddbf6 settings cleanup 2024-10-14 17:53:36 +02:00
vabene1111
1b1c1bdd5e fixed missing locale key and min shopping interval 2024-10-14 17:39:40 +02:00
vabene1111
b2f950ebe4 Merge branch 'develop' into feature/vue3 2024-10-14 17:20:17 +02:00
vabene1111
8501dcc34e some user file editor bits 2024-10-14 17:18:14 +02:00
vabene1111
9fd1d76fd8 supermarket editor done 2024-10-14 16:52:01 +02:00
vabene1111
e0f8d1ea9e moved to category relation instead of category 2024-10-13 17:48:55 +02:00
vabene1111
be886b108c supermarket category editor improvements 2024-10-13 13:24:00 +02:00
vabene1111
cbcddfbcd1 meal plan and model editors
- changed signature to options object
- added ability to set defaults
- meal plan clickable item creation
2024-10-11 17:45:47 +02:00
vabene1111
7d531d18d4 new model editor activators 2024-10-10 21:42:37 +02:00
vabene1111
eee8ed70e7 meal plan editor tweaks 2024-10-10 21:13:58 +02:00
vabene1111
4f425fb99a MealPlan editor in new editor system 2024-10-08 19:23:20 +02:00
vabene1111
9395a456f0 model list page url case sensitivity 2024-10-08 18:50:16 +02:00
vabene1111
a8256b461a model edit improvements 2024-10-08 18:45:41 +02:00
vabene1111
3494bce2b8 Merge branch 'develop' into feature/vue3 2024-10-08 18:26:42 +02:00
vabene1111
b6b3e83c1e disable selects for now 2024-10-08 07:46:37 +02:00
vabene1111
7d47fcf4e9 async component loading for model editor components 2024-10-08 07:44:17 +02:00
vabene1111
b857c9e4d9 fixed saving page item number in model list 2024-10-08 07:09:43 +02:00
vabene1111
25de4326d2 fixed some things but page param still broken 2024-10-07 21:11:21 +02:00
vabene1111
257f4f2b5b no longer build arm/v7 for tandoor 2 2024-10-06 11:14:13 +02:00
vabene1111
e4a6bd0a1f made ModelEditor setupState function properly handle async requests 2024-10-05 07:51:48 +02:00
vabene1111
4ab0fbf36b WIP supermarket category editor 2024-10-05 07:26:42 +02:00
vabene1111
d1379935b7 most model editors present 2024-10-03 00:01:39 +02:00
vabene1111
64c5fe3157 model editor stuff 2024-10-02 22:28:59 +02:00
vabene1111
ddf977f665 various model editor functions 2024-10-02 20:33:35 +02:00
vabene1111
a3ee2fb69c made food editor generic as well 2024-09-28 13:15:11 +02:00
vabene1111
143eafa24a updated model editors to composable and base template 2024-09-28 13:07:46 +02:00
vabene1111
67fff17f06 composable for model editor functions WIP 2024-09-26 21:53:04 +02:00
vabene1111
5ada8e529c changed access token editor to use generic model api 2024-09-26 21:22:19 +02:00
vabene1111
22a3654dfd model select stuff 2024-09-26 16:05:54 +02:00
vabene1111
9a94c650da model related stuff 2024-09-26 15:02:07 +02:00
vabene1111
ddaeb054d0 added v3 beta setting 2024-09-26 07:35:35 +02:00
vabene1111
0a3611b94a use HTML 5 history mode for vue frontend 2024-09-25 16:45:11 +02:00
vabene1111
0db439c80d fixed test for real 2024-09-25 16:44:57 +02:00
vabene1111
99103bc419 Revert "fixed space test to reflect new logic"
This reverts commit fcf9f30af0.
2024-09-25 16:43:41 +02:00
vabene1111
fcf9f30af0 fixed space test to reflect new logic 2024-09-25 16:28:36 +02:00
vabene1111
beab927f64 more models and model select with new system 2024-09-25 16:23:02 +02:00
vabene1111
91f2f34cd3 improved generic type 2024-09-24 16:00:31 +02:00
vabene1111
a44f61b507 WIP model list page, broke models system 2024-09-23 17:37:48 +02:00
vabene1111
fc567d2fbb added some test code and python script for openapi generation 2024-09-23 14:05:27 +02:00
vabene1111
12e0c7fd7b updated openapi generator 2024-09-23 13:57:37 +02:00
vabene1111
7e77e56e7f updated api client for latest model change 2024-09-23 10:34:37 +02:00
vabene1111
46706c633e Merge branch 'develop' into feature/vue3 2024-09-23 10:28:01 +02:00
vabene1111
ea8ab582eb all food editor functions implemented 2024-09-22 20:07:12 +02:00
vabene1111
73e4f22256 food editor WIP 2024-09-22 12:31:59 +02:00
vabene1111
a173e66a59 first draft of model list view and edit pages 2024-09-21 19:02:42 +02:00
vabene1111
cad75408c3 mealplan settings including meal type 2024-09-21 14:48:33 +02:00
vabene1111
cd93d9c697 basic meal plan settings 2024-09-21 14:17:13 +02:00
vabene1111
ce869950c3 space settings and message 2024-09-21 13:36:36 +02:00
vabene1111
55bd417105 settings and model dialogs 2024-09-21 12:46:54 +02:00
vabene1111
ed4592ae0c api page + model edit dialog 2024-09-21 11:12:56 +02:00
vabene1111
7c30204dd7 basic model editor workflow done 2024-09-19 07:52:57 +02:00
vabene1111
b52d61b6db working on model editor 2024-09-12 07:58:12 +02:00
vabene1111
1bed90b804 start of model edit dialogs 2024-09-10 08:55:28 +02:00
vabene1111
5eabf6869d api settings component 2024-09-09 18:54:43 +02:00
vabene1111
4ee5d348bf fixed endpoint descriptions 2024-09-09 18:43:29 +02:00
vabene1111
252a7207f6 work on settings component 2024-09-09 18:41:25 +02:00
vabene1111
abc2dc8437 Merge branch 'develop' into feature/vue3 2024-09-09 17:18:39 +02:00
vabene1111
27fe267181 fixed snackbar queued 2024-09-03 10:08:16 +02:00
vabene1111
5f184693ca space settings 2024-09-02 17:48:59 +02:00
vabene1111
6087668e26 Merge branch 'develop' into feature/vue3 2024-09-02 16:40:59 +02:00
vabene1111
830a385bda tweaked file input 2024-08-24 10:09:46 +02:00
vabene1111
c92c19d9b2 basic working file selector 2024-08-24 09:59:48 +02:00
vabene1111
8fe6f5c141 fixed space test and space api endpoint 2024-08-24 09:18:57 +02:00
vabene1111
39ab8b00c4 Merge branch 'develop' into feature/vue3 2024-08-24 08:29:04 +02:00
vabene1111
d3164d3e0d minor work on file uploader 2024-08-23 13:57:01 +02:00
vabene1111
e6af0e3845 very basic file list 2024-08-21 16:26:21 +02:00
vabene1111
0eabf1f7c4 central cookie function 2024-08-21 16:26:13 +02:00
vabene1111
91111c7d74 first file upload working 2024-08-21 16:17:03 +02:00
vabene1111
7397f4c381 space settings and file dialog 2024-08-20 15:17:06 +02:00
vabene1111
02ffb727d5 space settings base page 2024-08-20 13:50:58 +02:00
vabene1111
4ba769a49e updated space api endpoint to list all spaces + addtional /current/ endpoint 2024-08-20 13:49:12 +02:00
vabene1111
ad71804b70 fixed denisty on space member list 2024-08-20 12:34:23 +02:00
vabene1111
1bf9696a27 fixed models 2024-08-20 12:24:32 +02:00
vabene1111
b5e0055876 Merge branch 'develop' into feature/vue3
# Conflicts:
#	cookbook/views/api.py
#	vue/src/utils/models.js
2024-08-20 11:54:19 +02:00
vabene1111
6a3534da76 basic space member settings 2024-08-05 20:47:10 +02:00
vabene1111
105a5f2bdc fixed message list dialog icons 2024-08-05 20:15:03 +02:00
vabene1111
45543e9669 disable pagination for localization viewset 2024-08-05 07:51:16 +02:00
vabene1111
36bc96f192 more work on settings 2024-08-04 20:04:35 +02:00
vabene1111
516b345807 settings page WIP 2024-08-04 16:59:56 +02:00
vabene1111
4a6d542965 first idea of a settings page 2024-08-01 16:28:24 +02:00
vabene1111
fb9bbafd6e link changed 2024-08-01 15:24:18 +02:00
vabene1111
0e44243e55 Merge branch 'develop' into feature/vue3
# Conflicts:
#	recipes/settings.py
#	requirements.txt
2024-08-01 15:03:40 +02:00
vabene1111
65b451b465 improving the start page layout 2024-07-31 18:56:43 +02:00
vabene1111
abff1136f9 imrpvoed recipe card design 2024-07-31 18:43:10 +02:00
vabene1111
3630172723 mybe this time 2024-07-31 17:49:58 +02:00
vabene1111
d241ee3cde maybe fix build issues 2024-07-31 17:42:24 +02:00
vabene1111
6994f6bc1d removed ununsed imports 2024-07-31 17:02:27 +02:00
vabene1111
25147f84ec cleaned up locale loading 2024-07-31 17:02:06 +02:00
vabene1111
c02523ee51 lazy load locale based on user settings 2024-07-31 16:59:02 +02:00
vabene1111
8a4ffc5e0c working on locale (not yet fully working) 2024-07-21 07:03:21 +02:00
vabene1111
5b3445a5b5 user pref store tweaks 2024-07-20 13:47:39 +02:00
vabene1111
b05bff0fa0 basic user preference store 2024-07-20 13:44:36 +02:00
vabene1111
51f0f943f7 meal plan dialog 2024-07-20 13:15:24 +02:00
vabene1111
11b42470b6 mealplandialog date range working and moving 2024-07-20 10:02:32 +02:00
vabene1111
71e5e32206 meal plan dialog 2024-07-19 15:39:27 +02:00
vabene1111
6f44c8ba17 almost nothing and broken 2024-07-18 20:03:06 +02:00
vabene1111
45a6564e17 added FAB to editor 2024-07-10 17:39:20 +02:00
vabene1111
65641c1256 added small user menu 2024-07-09 07:52:00 +02:00
vabene1111
930e686cbd Merge branch 'develop' into feature/vue3
# Conflicts:
#	.env.template
#	cookbook/views/views.py
#	requirements.txt
2024-07-09 07:26:11 +02:00
vabene1111
08309a6f04 Merge pull request #3161 from fliiiix/bugfix/strip-env-list
Setttings: strip whitespace between entries
2024-06-11 17:20:55 +02:00
fliiiix
10b4c3da05 Use getenv default instead of or syntax 2024-06-11 12:35:21 +02:00
fliiiix
6257f6ffb7 Refactor extract settings to helper functions 2024-06-11 12:34:06 +02:00
fliiiix
80f8a524ef Strip whitespace in comma lists
This makes configurations valid which contain one or more
extra spaces.

Example:
CSRF_TRUSTED_ORIGINS = "https://foo.bar.example, http://foo.example"

Would be invalid.
2024-06-11 12:34:06 +02:00
vabene1111
6f3fc2fcab fixed message list dialog sorting 2024-05-22 13:07:01 +02:00
vabene1111
6a8b2b6338 playing with AI image recognition 2024-05-18 14:29:42 +02:00
vabene1111
fd1c6d718e allow create in model select and fixed some stuff 2024-05-07 07:57:45 +02:00
vabene1111
5f2e683b6b Merge pull request #3143 from smilerz/feature/vue3
updated Vue2 pages to work with drf_spectacular schemas
2024-05-02 16:50:22 +02:00
smilerz
5f4283ca3f update MealPlan queryset filtering to use dates instead of datetime 2024-05-01 12:03:10 -05:00
smilerz
9df03a73d9 updated Vue2 pages to work with drf_spectacular schemas 2024-05-01 11:02:27 -05:00
vabene1111
6d813ebb2f meal plan stuff 2024-05-01 15:37:33 +02:00
vabene1111
f961413e94 api client update 2024-05-01 13:47:25 +02:00
vabene1111
17693b90b5 Merge branch 'develop' into feature/vue3 2024-05-01 13:45:37 +02:00
vabene1111
c11dd8bcae cleanup vueform 2024-05-01 12:48:17 +02:00
vabene1111
ef3913d91f meal plan stuff 2024-05-01 11:14:39 +02:00
vabene1111
569b7e78fe models, messages and multiselects 2024-05-01 10:04:19 +02:00
vabene1111
32b75250dc Merge pull request #3131 from smilerz/feature/vue3
rename property apis
2024-04-30 10:04:07 +02:00
smilerz
743fcbcfc1 Merge branch 'feature/vue3' of github.com:smilerz/recipes into feature/vue3 2024-04-29 13:07:04 -05:00
smilerz
748db37a1b update Recipe Search, Edit and View to work with updated API 2024-04-29 13:06:00 -05:00
smilerz
a75650c045 Merge branch 'TandoorRecipes:feature/vue3' into feature/vue3 2024-04-29 09:34:30 -05:00
smilerz
ce9c469acc rename food-property and food-property-type 2024-04-29 09:33:42 -05:00
vabene1111
dc8958bee1 added all watcher tasks 2024-04-27 19:03:03 +02:00
vabene1111
e405aab310 no longer split schema in request/response 2024-04-27 10:40:09 +02:00
vabene1111
846c3e36cc Merge branch 'develop' into feature/vue3 2024-04-27 10:37:44 +02:00
vabene1111
b42a444a67 Merge pull request #3126 from smilerz/feature/vue3
legacy API
2024-04-27 09:19:47 +02:00
smilerz
4771f890cb add ability to generate legacy API with legacy naming scheme
regenerate legacy API
2024-04-26 12:21:34 -05:00
vabene1111
991ff88767 Merge pull request #3123 from smilerz/feature/vue3
Feature/vue3
2024-04-26 16:28:40 +02:00
smilerz
4fa7155fc3 delete extraneous openapi files 2024-04-25 07:31:51 -05:00
smilerz
9e49652f80 deleted openapi models at root of project 2024-04-25 07:28:02 -05:00
smilerz
931865b61f restore original openapi update.bat 2024-04-24 13:44:21 -05:00
smilerz
f0088b256e delete openapi generated files from root of vue3 2024-04-24 13:37:54 -05:00
smilerz
a047d36bf1 update tests to reflect API changes 2024-04-24 12:26:05 -05:00
smilerz
ebcc814abf remove deprecated api endpoints
api/log_cooking
api/plan-ical/
api/backup/
2024-04-24 12:24:19 -05:00
smilerz
b83f3a291b update documentation 2024-04-24 12:23:35 -05:00
smilerz
e4ff6ed732 adding TZ to default .env.template 2024-04-24 12:16:53 -05:00
smilerz
a67b084b52 regenerate API 2024-04-23 10:35:45 -05:00
smilerz
dd3f38fe75 Merge remote-tracking branch 'upstream/feature/vue3' into feature/vue3 2024-04-23 09:47:09 -05:00
smilerz
3184deb00e change reset_food_inheritance from GET to POST 2024-04-23 09:32:18 -05:00
smilerz
2847721584 update tests to reflect pagination and API changes 2024-04-23 09:18:48 -05:00
vabene1111
894a298f45 maybe better without vueform ? 2024-04-22 20:48:46 +02:00
vabene1111
17610663c1 playing with vueform 2024-04-22 20:18:09 +02:00
vabene1111
12cf9da8fc fixed api client generation 2024-04-22 19:33:09 +02:00
vabene1111
c47e46263c mostly composition now 2024-04-21 16:06:28 +02:00
vabene1111
e040a10096 moved many compoents to composition API 2024-04-21 15:58:31 +02:00
vabene1111
ce6c43fb62 shopping stuff 2024-04-21 11:49:07 +02:00
vabene1111
faf025d2c4 first shopping items showing 2024-04-21 11:42:49 +02:00
vabene1111
8e50742372 meal plan improvements 2024-04-21 08:07:47 +02:00
vabene1111
b0533f2f9b Merge branch 'develop' into feature/vue3
# Conflicts:
#	requirements.txt
2024-04-21 07:15:46 +02:00
smilerz
fd0d5813fb update SpaceFilterSerializer to handle both lists and QuerySets 2024-04-19 09:00:19 -05:00
smilerz
8cd79cdc20 update SpaceFilterSerializer to reflect paginated ViewSets provide
a list instead of a queryset
2024-04-19 08:37:27 -05:00
smilerz
d3abe4db3e updated tests to reflect API changes in pagination 2024-04-19 07:43:29 -05:00
smilerz
71765f3542 change function based views to @api_view
implement schemas on all @api_view
2024-04-18 17:52:29 -05:00
smilerz
1e326fe414 remove apis no longer in use 2024-04-18 17:52:04 -05:00
smilerz
f06b3e8af0 add enum to filters that have Choice fields 2024-04-18 16:25:02 -05:00
smilerz
4edd729850 add schema parameters for all list endpoinst with filters 2024-04-18 15:55:16 -05:00
smilerz
8412aa19fb paginate all list endpoints or explicitly mark as pagination_disabled 2024-04-18 15:18:51 -05:00
smilerz
5f0eb73927 regenerate vue typescript API 2024-04-18 10:59:05 -05:00
smilerz
fd8411b475 regenerate fetch API 2024-04-18 10:56:15 -05:00
smilerz
f312f6028d ensure that all schema fields are typed correctly 2024-04-18 10:54:45 -05:00
smilerz
f401b0f635 add test to validate all list endpoints are paginated
add test to valoidate that all endpoints have a schema
2024-04-18 10:52:41 -05:00
vabene1111
e22c89a0e9 basics of drag and drop 2024-04-15 21:03:24 +02:00
vabene1111
e5691d0e98 meal plan style improvements 2024-04-15 20:12:50 +02:00
vabene1111
4266029e84 somewhat broken initial meal plan version 2024-04-14 22:23:45 +02:00
vabene1111
849856df26 Merge branch 'develop' into feature/vue3
# Conflicts:
#	cookbook/views/api.py
2024-04-14 21:24:43 +02:00
vabene1111
e398fc0b38 Merge branch 'develop' into feature/vue3
# Conflicts:
#	.gitignore
#	docs/contribute.md
#	recipes/settings.py
#	requirements.txt
2024-04-11 18:00:42 +02:00
vabene1111
514c4106b1 basic calendar 2024-04-07 13:29:00 +02:00
vabene1111
3cf89aca10 playing with calendars 2024-04-07 12:54:05 +02:00
vabene1111
9bf8b615dc re added basic recipe activity 2024-04-07 09:28:11 +02:00
vabene1111
cb8dd3bc99 sizing and navs 2024-04-07 08:29:21 +02:00
vabene1111
1cd9caef4a step display tweaks 2024-04-07 07:56:58 +02:00
vabene1111
1025829123 proper timer component 2024-04-07 07:15:25 +02:00
vabene1111
019a931b99 structure cleanup 2024-04-07 06:27:59 +02:00
vabene1111
8398193a51 Merge branch 'develop' into feature/vue3 2024-04-05 21:18:06 +02:00
smilerz
f560365ded stub out schema completeness tests 2024-04-05 08:15:01 -05:00
vabene1111
ebc2902450 Merge branch 'develop' into feature/vue3
# Conflicts:
#	requirements.txt
2024-04-04 20:58:44 +02:00
vabene1111
dfd9f7b066 posprocessing hook for DRF 2024-03-30 11:01:37 +01:00
vabene1111
cb98b6723f first version of meal plan diaglo 2024-03-29 20:08:48 +01:00
vabene1111
dcf7d44d72 vueform mealplan dialog 2024-03-29 17:18:30 +01:00
vabene1111
369c460837 playing with vueforms 2024-03-29 14:38:22 +01:00
smilerz
e99ff005d6 added test to check for API pagination 2024-03-28 17:39:06 -05:00
vabene1111
c1d6e98349 playing with meal plan edit dialog 2024-03-28 16:44:20 +01:00
smilerz
497a4bbeb9 Merge pull request #3071 from smilerz/feature/vue3
rebuild APIs and vue apps
2024-03-28 09:44:14 -05:00
smilerz
f47c806ebd rebuild APIs and vue apps 2024-03-28 09:43:03 -05:00
smilerz
bb2a0eb642 Merge pull request #3070 from smilerz/feature/vue3
fix formatting on ci.yml
2024-03-28 09:18:11 -05:00
smilerz
28fd1917ec fix formatting on ci.yml 2024-03-28 09:16:41 -05:00
smilerz
cf0f7482f1 Merge pull request #3068 from smilerz/feature/vue3
Feature/vue3
2024-03-28 08:01:41 -05:00
smilerz
2475eadab9 Merge remote-tracking branch 'upstream/feature/vue3' into feature/vue3 2024-03-28 07:59:44 -05:00
smilerz
97cf2e6372 regen axios api 2024-03-28 07:47:50 -05:00
smilerz
2c12ce3edf added permissions to docs/api
added swagger api view
added authentication method to openapi schema
added logo to docs/api
fixed tree and merge schemas
2024-03-27 15:45:51 -05:00
smilerz
daf343c5fd convert TreeMixin and FuzzyFilterMixin to swagger schemas 2024-03-27 11:51:57 -05:00
smilerz
31eacad5fb WIP 2024-03-27 11:13:58 -05:00
smilerz
54e147ce8e regenerate openapi 2024-03-27 08:38:38 -05:00
smilerz
b217ac7ae7 update contribute.md with vue3 and typescript-fetch instructions 2024-03-27 08:38:38 -05:00
smilerz
0727a52b8a make update.bat executable on linux 2024-03-27 08:38:37 -05:00
smilerz
2c24017e96 remove custom QueryParams with extend_schema decorator 2024-03-27 08:38:37 -05:00
smilerz
0f5d37fc7c apply PK only update to NestedWritableSerializer 2024-03-27 08:38:36 -05:00
vabene1111
2b3e2039b8 playing with the timer 2024-03-27 08:38:36 -05:00
vabene1111
748518f567 number scaler 2024-03-27 08:38:36 -05:00
vabene1111
91980d91e8 lots of visual improvements 2024-03-27 08:38:35 -05:00
vabene1111
630f2fbf4e improved recipe card 2024-03-27 08:38:35 -05:00
vabene1111
394d7d73ed first overlay working 2024-03-27 08:38:34 -05:00
vabene1111
49a437b103 fixed meal plan window filter condition 2024-03-27 08:38:34 -05:00
c0mputerguru
15b38241da Add drf-spectacular parameter/response schema for meal plan retrieve ical API 2024-03-27 08:38:33 -05:00
c0mputerguru
e02594ba83 Add CSRF trusted origins to support github codespaces. 2024-03-27 08:38:33 -05:00
vabene1111
061fbfff65 meal plan in its own component 2024-03-27 08:38:32 -05:00
vabene1111
a5aa6d74b5 card skeleton loader improvement 2024-03-27 08:38:32 -05:00
vabene1111
8a9e150f64 recipe card 2024-03-27 08:38:32 -05:00
vabene1111
44a68bab71 meal plan store and start page widget 2024-03-27 08:38:31 -05:00
vabene1111
facbe08e20 start page taking shape 2024-03-27 08:38:31 -05:00
vabene1111
728bb76a43 improved start page 2024-03-27 08:38:30 -05:00
Anand Patel
bdd9ff796a Introduce ical action on MealPlanViewSet to expose ical format for listing meal plans. 2024-03-27 08:38:30 -05:00
vabene1111
77a46a4ef6 working on select components 2024-03-27 08:37:01 -05:00
vabene1111
a4225769f6 working on model select 2024-03-27 08:37:00 -05:00
vabene1111
cf74187be1 playing with generic select 2024-03-27 08:37:00 -05:00
vabene1111
454a05986c editor improvements 2024-03-27 08:37:00 -05:00
vabene1111
fa2fcf4f08 only apply keybind when visible 2024-03-27 08:36:59 -05:00
vabene1111
44d7f18428 more recipe editor 2024-03-27 08:36:59 -05:00
vabene1111
da60b4a097 improved global search dialog 2024-03-27 08:36:58 -05:00
vabene1111
40d460b458 servings 2024-03-27 08:36:58 -05:00
vabene1111
86652a8f1f cleanup 2024-03-27 08:36:57 -05:00
vabene1111
e4caf4169f first working number scaling 2024-03-27 08:36:57 -05:00
vabene1111
5cd7538ed5 markdown editor 2024-03-27 08:36:57 -05:00
vabene1111
bfbe19b49b very basic editor 2024-03-27 08:36:56 -05:00
vabene1111
45f8d2b1c8 general UI tweaks 2024-03-27 08:36:56 -05:00
vabene1111
8006d7663c start page 2024-03-27 08:36:55 -05:00
vabene1111
17f875863c working search with flat endpoint 2024-03-27 08:36:55 -05:00
vabene1111
4a8ad3db1e drf spectacular 2024-03-27 08:36:27 -05:00
vabene1111
71261fc767 basics of shopping 2024-03-27 08:35:17 -05:00
vabene1111
58d1c94b79 search tweaking 2024-03-27 08:35:16 -05:00
vabene1111
761b974aa5 recipe view timer 2024-03-27 08:35:16 -05:00
vabene1111
bd96a29200 v3 update 2024-03-27 08:35:15 -05:00
vabene1111
1427dd989f search 2024-03-27 08:35:15 -05:00
vabene1111
b4281aaf83 reverted .ts change in old vue 2024-03-27 08:35:15 -05:00
vabene1111
418821d8d3 saerch dialogh 2024-03-27 08:34:49 -05:00
vabene1111
22968495fd playing with search 2024-03-27 08:34:22 -05:00
vabene1111
ab3de1871c added some cooklog stuff 2024-03-27 08:34:22 -05:00
vabene1111
f691da53d7 steps 2024-03-27 08:34:21 -05:00
vabene1111
25887c0595 steps overview in recipe view 2024-03-27 08:34:21 -05:00
vabene1111
644be2d59b some more things in recipe view 2024-03-27 08:34:20 -05:00
vabene1111
3f880ef304 playing with navs 2024-03-27 08:34:20 -05:00
vabene1111
851cb28714 first few pieces 2024-03-27 08:34:19 -05:00
vabene1111
92b7439969 some basic views 2024-03-27 08:34:19 -05:00
vabene1111
257e886d87 more basics 2024-03-27 08:34:19 -05:00
vabene1111
89c6964e30 basic app sceleton 2024-03-27 08:34:18 -05:00
vabene1111
bb6356cfa8 first vue 3 commit 2024-03-27 08:34:18 -05:00
vabene1111
8728865b97 added captcha option to password reset form 2024-03-27 08:30:37 -05:00
vabene1111
62bfc6f7b0 added additional rate limiting to password reset 2024-03-27 08:29:42 -05:00
smilerz
54ad7db2c1 regenerate openapi 2024-03-27 08:20:26 -05:00
smilerz
ccbbebccef update contribute.md with vue3 and typescript-fetch instructions 2024-03-27 08:19:53 -05:00
smilerz
0a5b707fec make update.bat executable on linux 2024-03-27 08:19:27 -05:00
smilerz
fb3473459d remove custom QueryParams with extend_schema decorator 2024-03-27 08:18:50 -05:00
smilerz
d6929e5cf9 apply PK only update to NestedWritableSerializer 2024-03-27 08:17:20 -05:00
vabene1111
3207b69874 playing with the timer 2024-03-26 07:52:43 +01:00
vabene1111
bd317858bf number scaler 2024-03-23 21:57:42 +01:00
vabene1111
9d4c26fd29 lots of visual improvements 2024-03-23 21:11:59 +01:00
vabene1111
f0d0550251 improved recipe card 2024-03-22 16:30:37 +01:00
vabene1111
f132eacb83 first overlay working 2024-03-22 09:15:33 +01:00
vabene1111
b75e3a7848 fixed meal plan window filter condition 2024-03-21 16:14:50 +01:00
vabene1111
79a7e60cfc Merge branch 'feature/vue3' of https://github.com/vabene1111/recipes into feature/vue3 2024-03-21 14:59:51 +01:00
vabene1111
673c660d26 Merge pull request #3038 from c0mputerguru/ical-default-dates-new-api
Add ical action on MealPlanViewSet to expose filterable ical API in restful manner
2024-03-21 14:55:02 +01:00
vabene1111
8f03899302 Merge branch 'master' into feature/vue3 2024-03-21 14:54:25 +01:00
c0mputerguru
8ccd4b5045 Add drf-spectacular parameter/response schema for meal plan retrieve ical API 2024-03-18 17:41:12 +00:00
c0mputerguru
5b3207bc24 Add CSRF trusted origins to support github codespaces. 2024-03-18 17:33:02 +00:00
c0mputerguru
57314c56c8 Merge branch 'feature/vue3' into ical-default-dates-new-api 2024-03-18 09:34:25 -07:00
vabene1111
4cd1e0a4a5 meal plan in its own component 2024-03-18 16:26:40 +01:00
vabene1111
83e9a2bbfb card skeleton loader improvement 2024-03-18 15:58:48 +01:00
vabene1111
46d2b7730e recipe card 2024-03-18 15:46:34 +01:00
vabene1111
b7c2b5c294 meal plan store and start page widget 2024-03-17 11:45:39 +01:00
vabene1111
7b9d140e74 start page taking shape 2024-03-17 09:58:26 +01:00
vabene1111
0c6850d498 improved start page 2024-03-16 22:51:15 +01:00
Anand Patel
ffe02bf210 Introduce ical action on MealPlanViewSet to expose ical format for listing meal plans. 2024-03-16 05:28:16 +00:00
Anand Patel
e78dd305f3 Add pytest-xdist to allow debugging tests in vscode. 2024-03-16 04:15:46 +00:00
vabene1111
e12c83faf1 working on select components 2024-03-15 22:38:52 +01:00
vabene1111
05102d3842 Merge branch 'develop' into feature/vue3 2024-03-15 19:11:24 +01:00
vabene1111
18767c54ce working on model select 2024-03-11 19:46:37 +01:00
vabene1111
09dc35228f playing with generic select 2024-03-10 13:00:39 +01:00
vabene1111
312c215813 Merge branch 'develop' into feature/vue3
# Conflicts:
#	cookbook/urls.py
2024-03-09 14:34:44 +01:00
vabene1111
95583dbe2c editor improvements 2024-03-06 21:44:55 +01:00
vabene1111
cf20b22404 only apply keybind when visible 2024-03-06 20:29:41 +01:00
vabene1111
73ba5a591d more recipe editor 2024-03-05 21:46:50 +01:00
vabene1111
82ebeacea9 improved global search dialog 2024-03-05 20:43:18 +01:00
vabene1111
c0c71c3967 servings 2024-03-05 17:16:07 +01:00
vabene1111
aa5a87a1fc cleanup 2024-03-05 16:48:03 +01:00
vabene1111
302faa193a first working number scaling 2024-03-05 16:40:10 +01:00
vabene1111
9fe3eeb823 Merge branch 'master' into feature/vue3
# Conflicts:
#	requirements.txt
2024-03-05 14:45:53 +01:00
vabene1111
e0f7ce5de9 markdown editor 2024-03-02 10:15:39 +01:00
vabene1111
76eecedfb5 very basic editor 2024-03-02 08:52:29 +01:00
vabene1111
8f216a2791 general UI tweaks 2024-03-02 07:58:58 +01:00
vabene1111
75e3d826a0 Merge branch 'develop' into feature/vue3
# Conflicts:
#	.github/workflows/ci.yml
#	.gitignore
#	requirements.txt
2024-03-02 07:41:07 +01:00
vabene1111
a57b8f6081 start page 2024-02-29 21:28:44 +01:00
vabene1111
1cac34d2a0 working search with flat endpoint 2024-02-29 20:08:37 +01:00
vabene1111
e47bdd043e drf spectacular 2024-02-29 16:34:13 +01:00
vabene1111
521c71733a basics of shopping 2024-02-28 21:38:55 +01:00
vabene1111
963273853f search tweaking 2024-02-28 21:18:45 +01:00
vabene1111
c0c26a5a20 recipe view timer 2024-02-28 21:04:43 +01:00
vabene1111
b7533457de Merge branch 'develop' into feature/vue3 2024-02-28 20:21:15 +01:00
vabene1111
388a7ceb16 v3 update 2024-02-28 17:58:27 +01:00
vabene1111
8391365d05 search 2024-02-28 17:55:11 +01:00
vabene1111
6a7c3b472a Merge branch 'develop' into feature/vue3
# Conflicts:
#	recipes/settings.py
2024-02-28 17:25:14 +01:00
vabene1111
f9efe44e1d reverted .ts change in old vue 2024-02-28 17:17:35 +01:00
vabene1111
7817ed2f7e Merge branch 'develop' into feature/vue3
# Conflicts:
#	recipes/settings.py
#	vue/vue.config.js
2024-02-28 17:13:25 +01:00
vabene1111
79c71bd5d9 saerch dialogh 2024-02-26 21:54:38 +01:00
vabene1111
0ddb013e94 Merge branch 'develop' into feature/vue3
# Conflicts:
#	requirements.txt
2024-02-26 16:32:28 +01:00
vabene1111
3b2e75db1d playing with search 2024-02-25 17:10:40 +01:00
vabene1111
3c7fd0fa35 added some cooklog stuff 2024-02-24 13:44:20 +01:00
vabene1111
1e349214fe Merge branch 'develop' into feature/vue3
# Conflicts:
#	.gitignore
2024-02-24 13:18:52 +01:00
vabene1111
e689cef201 steps 2024-02-24 12:44:33 +01:00
vabene1111
f58d9e49d8 steps overview in recipe view 2024-02-24 12:16:24 +01:00
vabene1111
1b8d501208 Merge branch 'develop' into feature/vue3 2024-02-24 11:16:55 +01:00
vabene1111
1842bb7105 some more things in recipe view 2024-02-24 11:14:58 +01:00
vabene1111
d53706bd5d playing with navs 2024-02-22 22:19:00 +01:00
vabene1111
b0e01e13bf first few pieces 2024-02-21 22:06:07 +01:00
vabene1111
5587429475 some basic views 2024-02-21 20:18:54 +01:00
vabene1111
1e6e843e05 more basics 2024-02-21 18:09:51 +01:00
vabene1111
4972418dc5 basic app sceleton 2024-02-20 22:13:45 +01:00
vabene1111
1f39ed9d4e first vue 3 commit 2024-02-20 20:11:51 +01:00
1044 changed files with 242348 additions and 109223 deletions

View File

@@ -20,6 +20,10 @@
"ms-python.python"
]
}
},
"containerEnv": {
"CSRF_TRUSTED_ORIGINS": "http://localhost:8000,http://localhost:8080"
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.

View File

@@ -6,6 +6,9 @@
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
SECRET_KEY=
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
TZ=Europe/Berlin
# allowed hosts (see documentation), should be set to your hostname(s) but might be * (default) for some proxies/providers
# ALLOWED_HOSTS=recipes.mydomain.com

View File

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

View File

@@ -17,7 +17,7 @@ jobs:
# Standard build config
- name: Standard
dockerfile: Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
suffix: ""
continue-on-error: false
steps:
@@ -34,17 +34,17 @@ jobs:
echo VERSION=develop >> $GITHUB_OUTPUT
fi
# Build Vue frontend
# Build Vue 3 frontend
- uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '22'
cache: yarn
cache-dependency-path: vue/yarn.lock
cache-dependency-path: vue3/yarn.lock
- name: Install dependencies
working-directory: ./vue
working-directory: ./vue3
run: yarn install --frozen-lockfile
- name: Build dependencies
working-directory: ./vue
working-directory: ./vue3
run: yarn build
- name: Set up QEMU
@@ -74,8 +74,9 @@ jobs:
flavor: |
latest=false
suffix=${{ matrix.suffix }}
# disable latest for tagged releases while in beta
# type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
tags: |
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
@@ -93,29 +94,29 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
notify-stable:
name: Notify Stable
runs-on: ubuntu-latest
needs: build-container
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Set tag name
run: |
# Strip "refs/tags/" prefix
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
# Send stable discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
# notify-stable:
# name: Notify Stable
# runs-on: ubuntu-latest
# needs: build-container
# if: startsWith(github.ref, 'refs/tags/')
# steps:
# - name: Set tag name
# run: |
# # Strip "refs/tags/" prefix
# echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
# # Send stable discord notification
# - name: Discord notification
# env:
# DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
# uses: Ilshidur/action-discord@0.3.2
# with:
# args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
notify-beta:
name: Notify Beta
runs-on: ubuntu-latest
needs: build-container
if: github.ref == 'refs/heads/beta'
if: startsWith(github.ref, 'refs/tags/')
steps:
# Send beta discord notification
- name: Discord notification
@@ -123,4 +124,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 The BETA Image has been updated! 🥳'
args: '🚀 The Tandoor 2 Image has been updated! 🥳'

View File

@@ -11,12 +11,11 @@ jobs:
matrix:
python-version: ["3.10"]
node-version: ["18"]
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: libsasl2-dev python3-dev libldap2-dev libssl-dev
packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
version: 1.0
# Setup python & dependencies

9
.gitignore vendored
View File

@@ -75,8 +75,10 @@ cookbook/static/vue
vue/webpack-stats.json
/docker-compose.override.yml
vue/node_modules
plugins
vue3/node_modules
/recipes/plugins
vetur.config.js
cookbook/static/vue
vue/webpack-stats.json
cookbook/templates/sw.js
vue/.yarn
vue3/.vite
@@ -85,4 +87,5 @@ vue3/.vite
vetur.config.js
venv/
.idea/easy-i18n.xml
cookbook/static/vue3
cookbook/static/vue3
vue3/node_modules

6
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

2
.idea/recipes.iml generated
View File

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

62
.idea/watcherTasks.xml generated
View File

@@ -5,7 +5,7 @@
<option name="arguments" value="-m flake8 $FilePath$ --config $ContentRoot$\.flake8" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ALWAYS" />
<option name="exitCodeBehavior" value="NEVER" />
<option name="fileExtension" value="py" />
<option name="immediateSync" value="false" />
<option name="name" value="Flake8 Watcher" />
@@ -27,5 +27,65 @@
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="false">
<option name="arguments" value="-m isort $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="py" />
<option name="immediateSync" value="false" />
<option name="name" value="isort Watcher" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$PyInterpreterDirectory$/python" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="false">
<option name="arguments" value="-m yapf -i $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="NEVER" />
<option name="fileExtension" value="py" />
<option name="immediateSync" value="false" />
<option name="name" value="YAPF" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$PyInterpreterDirectory$/python" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="false">
<option name="arguments" value="--cwd $ProjectFileDir$\vue prettier -w --config $ProjectFileDir$\.prettierrc $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="*" />
<option name="immediateSync" value="true" />
<option name="name" value="Prettier" />
<option name="output" value="" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="yarn" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="Prettier" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
</component>
</project>

2
.vscode/launch.json vendored
View File

@@ -24,7 +24,7 @@
"console": "integratedTerminal",
"env": {
// coverage and pytest can't both be running at the same time
"PYTEST_ADDOPTS": "--no-cov"
"PYTEST_ADDOPTS": "--no-cov -n 0"
},
"django": true,
"justMyCode": true

202
.vscode/tasks.json vendored
View File

@@ -1,75 +1,131 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Migrations",
"type": "shell",
"command": "python3 manage.py migrate",
},
{
"label": "Collect Static Files",
"type": "shell",
"command": "python3 manage.py collectstatic",
"dependsOn": ["Yarn Build"],
},
{
"label": "Setup Dev Server",
"dependsOn": ["Run Migrations", "Yarn Build"],
},
{
"label": "Run Dev Server",
"type": "shell",
"dependsOn": ["Setup Dev Server"],
"command": "python3 manage.py runserver",
},
{
"label": "Yarn Install",
"type": "shell",
"command": "yarn install",
"options": {
"cwd": "${workspaceFolder}/vue"
}
},
{
"label": "Yarn Serve",
"type": "shell",
"command": "yarn serve",
"dependsOn": ["Yarn Install"],
"options": {
"cwd": "${workspaceFolder}/vue"
}
},
{
"label": "Yarn Build",
"type": "shell",
"command": "yarn build",
"dependsOn": ["Yarn Install"],
"options": {
"cwd": "${workspaceFolder}/vue"
},
"group": "build",
},
{
"label": "Setup Tests",
"dependsOn": ["Run Migrations", "Collect Static Files"],
},
{
"label": "Run all pytests",
"type": "shell",
"command": "python3 -m pytest cookbook/tests",
"dependsOn": ["Setup Tests"],
"group": "test",
},
{
"label": "Setup Documentation Dependencies",
"type": "shell",
"command": "pip install mkdocs-material mkdocs-include-markdown-plugin",
},
{
"label": "Serve Documentation",
"type": "shell",
"command": "mkdocs serve",
"dependsOn": ["Setup Documentation Dependencies"],
"version": "2.0.0",
"tasks": [
{
"label": "Run Migrations",
"type": "shell",
"command": "python3 manage.py migrate"
},
{
"label": "Collect Static Files",
"type": "shell",
"command": "python3 manage.py collectstatic",
"dependsOn": ["Yarn Build"]
},
{
"label": "Setup Dev Server",
"dependsOn": ["Run Migrations", "Yarn Build"]
},
{
"label": "Run Dev Server",
"type": "shell",
"dependsOn": ["Setup Dev Server"],
"command": "python3 manage.py runserver"
},
{
"label": "Yarn Install",
"dependsOn": ["Yarn Install - Vue", "Yarn Install - Vue3"]
},
{
"label": "Yarn Install - Vue",
"type": "shell",
"command": "yarn install --force",
"options": {
"cwd": "${workspaceFolder}/vue"
}
]
}
},
{
"label": "Yarn Install - Vue3",
"type": "shell",
"command": "yarn install --force",
"options": {
"cwd": "${workspaceFolder}/vue3"
}
},
{
"label": "Generate API",
"dependsOn": ["Generate API - Vue", "Generate API - Vue3"]
},
{
"label": "Generate API - Vue",
"type": "shell",
"command": "openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8000/openapi/",
"options": {
"cwd": "${workspaceFolder}/vue/src/utils/openapi"
}
},
{
"label": "Generate API - Vue3",
"type": "shell",
"command": "openapi-generator-cli generate -g typescript-fetch -i http://127.0.0.1:8000/openapi/",
"options": {
"cwd": "${workspaceFolder}/vue3/src/openapi"
}
},
{
"label": "Yarn Serve",
"type": "shell",
"command": "yarn serve",
"dependsOn": ["Yarn Install - Vue"],
"options": {
"cwd": "${workspaceFolder}/vue"
}
},
{
"label": "Vite Serve",
"type": "shell",
"command": "vite",
"dependsOn": ["Yarn Install - Vue3"],
"options": {
"cwd": "${workspaceFolder}/vue3"
}
},
{
"label": "Yarn Build",
"dependsOn": ["Yarn Build - Vue", "Vite Build - Vue3"],
"group": "build"
},
{
"label": "Yarn Build - Vue",
"type": "shell",
"command": "yarn build",
"dependsOn": ["Yarn Install - Vue"],
"options": {
"cwd": "${workspaceFolder}/vue"
},
"group": "build"
},
{
"label": "Vite Build - Vue3",
"type": "shell",
"command": "vite build",
"dependsOn": ["Yarn Install - Vue3"],
"options": {
"cwd": "${workspaceFolder}/vue3"
},
"group": "build"
},
{
"label": "Setup Tests",
"dependsOn": ["Run Migrations", "Collect Static Files"]
},
{
"label": "Run all pytests",
"type": "shell",
"command": "python3 -m pytest cookbook/tests",
"dependsOn": ["Setup Tests"],
"group": "test"
},
{
"label": "Setup Documentation Dependencies",
"type": "shell",
"command": "pip install mkdocs-material mkdocs-include-markdown-plugin"
},
{
"label": "Serve Documentation",
"type": "shell",
"command": "mkdocs serve",
"dependsOn": ["Setup Documentation Dependencies"]
}
]
}

View File

@@ -1,4 +1,4 @@
FROM python:3.12-alpine3.19
FROM python:3.13-alpine3.21
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git
@@ -21,14 +21,19 @@ RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
# remove Development dependencies from requirements.txt
RUN sed -i '/# Development/,$d' requirements.txt
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
venv/bin/pip install wheel==0.42.0 && \
venv/bin/pip install setuptools_rust==1.9.0 && \
venv/bin/pip debug -v && \
venv/bin/pip install wheel==0.45.1 && \
venv/bin/pip install setuptools_rust==1.10.2 && \
if [ `apk --print-arch` = "aarch64" ]; then \
curl https://sh.rustup.rs -sSf | sh -s -- -y; \
fi &&\
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps

View File

@@ -10,7 +10,7 @@ from treebeard.forms import movenodeform_factory
from cookbook.managers import DICTIONARY
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
@@ -103,6 +103,13 @@ class ConnectorConfigAdmin(admin.ModelAdmin):
admin.site.register(ConnectorConfig, ConnectorConfigAdmin)
class CustomFilterAdmin(admin.ModelAdmin):
list_display = ('id', 'type', 'name')
admin.site.register(CustomFilter, CustomFilterAdmin)
class SyncAdmin(admin.ModelAdmin):
list_display = ('storage', 'path', 'active', 'last_checked')
search_fields = ('storage__name', 'path')

View File

@@ -0,0 +1,102 @@
# custom processing for schema
# reason: DRF writable nested needs ID's to decide if a nested object should be created or updated
# the API schema/client make ID's read only by default and strips them entirely in request objects (with COMPONENT_SPLIT_REQUEST enabled)
# change the schema to make IDs optional but writable so they are included in the request
def custom_postprocessing_hook(result, generator, request, public):
for c in result['components']['schemas'].keys():
# handle schemas used by the client to do requests on the server
if 'properties' in result['components']['schemas'][c] and 'id' in result['components']['schemas'][c]['properties']:
# make ID field not read only so it's not stripped from the request on the client
result['components']['schemas'][c]['properties']['id']['readOnly'] = False
# make ID field not required
if 'required' in result['components']['schemas'][c] and 'id' in result['components']['schemas'][c]['required']:
result['components']['schemas'][c]['required'].remove('id')
return result
# TODO remove below once legacy API has been fully deprecated
from drf_spectacular.openapi import AutoSchema # noqa: E402 isort: skip
import functools # noqa: E402 isort: skip
import re # noqa: E402 isort: skip
class LegacySchema(AutoSchema):
operation_id_base = None
@functools.cached_property
def path(self):
path = re.sub(pattern=self.path_prefix, repl='', string=self.path, flags=re.IGNORECASE)
# remove path variables
return re.sub(pattern=r'\{[\w\-]+\}', repl='', string=path)
def get_operation_id(self):
"""
Compute an operation ID from the view type and get_operation_id_base method.
"""
method_name = getattr(self.view, 'action', self.method.lower())
if self._is_list_view():
action = 'list'
elif method_name not in self.method_mapping:
action = self._to_camel_case(method_name)
else:
action = self.method_mapping[self.method.lower()]
name = self.get_operation_id_base(action)
return action + name
def get_operation_id_base(self, action):
"""
Compute the base part for operation ID from the model, serializer or view name.
"""
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
if self.operation_id_base is not None:
name = self.operation_id_base
# Try to deduce the ID from the view's model
elif model is not None:
name = model.__name__
# Try with the serializer class name
elif self.get_serializer() is not None:
name = self.get_serializer().__class__.__name__
if name.endswith('Serializer'):
name = name[:-10]
# Fallback to the view name
else:
name = self.view.__class__.__name__
if name.endswith('APIView'):
name = name[:-7]
elif name.endswith('View'):
name = name[:-4]
# Due to camel-casing of classes and `action` being lowercase, apply title in order to find if action truly
# comes at the end of the name
if name.endswith(action.title()): # ListView, UpdateAPIView, ThingDelete ...
name = name[:-len(action)]
if action == 'list' and not name.endswith('s'): # listThings instead of listThing
name += 's'
return name
def get_serializer(self):
view = self.view
if not hasattr(view, 'get_serializer'):
return None
try:
return view.get_serializer()
except Exception:
return None
def _to_camel_case(self, snake_str):
components = snake_str.split('_')
# We capitalize the first letter of each component except the first one
# with the 'title' method and join them together.
return components[0] + ''.join(x.title() for x in components[1:])

View File

@@ -6,8 +6,8 @@ class StyleTreeprocessor(Treeprocessor):
def run_processor(self, node):
for child in node:
if child.tag == "table":
child.set("class", "table table-bordered")
# if child.tag == "table":
# child.set("class", "markdown-body")
if child.tag == "img":
child.set("class", "img-fluid")
self.run_processor(child)

View File

@@ -6,6 +6,8 @@ from cookbook.models import (Food, FoodProperty, Property, PropertyType, Superma
SupermarketCategory, SupermarketCategoryRelation, Unit, UnitConversion)
import re
from recipes.settings import DEBUG
class OpenDataImportResponse:
total_created = 0
@@ -367,12 +369,28 @@ class OpenDataImporter:
create_list.append({'data': obj_dict})
if self.update_existing and len(update_list) > 0:
model_type.objects.bulk_update(update_list, field_list)
od_response.total_updated += len(update_list)
try:
model_type.objects.bulk_update(update_list, field_list)
od_response.total_updated += len(update_list)
except Exception:
if DEBUG:
print('========= LOAD FOOD FAILED ============')
print(update_list)
print(existing_data_names)
print(existing_data_slugs)
traceback.print_exc()
if len(create_list) > 0:
Food.load_bulk(create_list, None)
od_response.total_created += len(create_list)
try:
Food.load_bulk(create_list, None)
od_response.total_created += len(create_list)
except Exception:
if DEBUG:
print('========= LOAD FOOD FAILED ============')
print(create_list)
print(existing_data_names)
print(existing_data_slugs)
traceback.print_exc()
# --------------- PROPERTY STUFF -----------------------
model_type = Property

View File

@@ -160,18 +160,15 @@ class GroupRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR,
_('You are not logged in and therefore cannot view this page!'))
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass

View File

@@ -44,13 +44,17 @@ class FoodPropertyHelper:
if i.food is not None:
conversions = uch.get_conversions(i)
for pt in property_types:
# if a property could be calculated with an actual value
found_property = False
# if food has a value for the given property type (no matter if conversion is possible)
has_property_value = False
if i.food.properties_food_amount == 0 or i.food.properties_food_unit is None: # if food is configured incorrectly
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': None}
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
computed_properties[pt.id]['missing_value'] = True
else:
for p in i.food.properties.all():
if p.property_type == pt and p.property_amount is not None:
has_property_value = True
for c in conversions:
if c.unit == i.food.properties_food_unit:
found_property = True
@@ -60,10 +64,12 @@ class FoodPropertyHelper:
if not found_property:
if i.amount == 0: # don't count ingredients without an amount as missing
computed_properties[pt.id]['missing_value'] = computed_properties[pt.id]['missing_value'] or False # don't override if another food was already missing
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': 0}
else:
computed_properties[pt.id]['missing_value'] = True
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': None}
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
if has_property_value and i.unit is not None:
computed_properties[pt.id]['food_values'][i.food.id]['missing_conversion'] = {'base_unit': {'id': i.unit.id, 'name': i.unit.name}, 'converted_unit': {'id': i.food.properties_food_unit.id, 'name': i.food.properties_food_unit.name}}
return computed_properties
@@ -74,5 +80,5 @@ class FoodPropertyHelper:
if key in d and d[key]['value']:
d[key]['value'] += value
else:
d[key] = {'id': food.id, 'food': food.name, 'value': value}
d[key] = {'id': food.id, 'food': {'id': food.id, 'name': food.name}, 'value': value}
return d

View File

@@ -49,7 +49,11 @@ class RecipeSearch():
self._search_prefs = SearchPreference()
self._string = self._params.get('query').strip(
) if self._params.get('query', None) else None
self._rating = self._params.get('rating', None)
self._rating_gte = self._params.get('rating_gte', None)
self._rating_lte = self._params.get('rating_lte', None)
self._keywords = {
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
'and': self._params.get('keywords_and', None),
@@ -70,20 +74,36 @@ class RecipeSearch():
}
self._steps = self._params.get('steps', None)
self._units = self._params.get('units', None)
# TODO add created by
# TODO image exists
self._sort_order = self._params.get('sort_order', None)
self._internal = str2bool(self._params.get('internal', None))
self._random = str2bool(self._params.get('random', False))
self._sort_order = self._params.get('sort_order', None)
if self._sort_order == 'random':
self._random = True
self.sort_order = None
else:
self._random = str2bool(self._params.get('random', False))
self._new = str2bool(self._params.get('new', False))
self._num_recent = int(self._params.get('num_recent', 0))
self._include_children = str2bool(
self._params.get('include_children', None))
self._timescooked = self._params.get('timescooked', None)
self._cookedon = self._params.get('cookedon', None)
self._timescooked_gte = self._params.get('timescooked_gte', None)
self._timescooked_lte = self._params.get('timescooked_lte', None)
self._createdon = self._params.get('createdon', None)
self._createdon_gte = self._params.get('createdon_gte', None)
self._createdon_lte = self._params.get('createdon_lte', None)
self._updatedon = self._params.get('updatedon', None)
self._viewedon = self._params.get('viewedon', None)
self._updatedon_gte = self._params.get('updatedon_gte', None)
self._updatedon_lte = self._params.get('updatedon_lte', None)
self._viewedon_gte = self._params.get('viewedon_gte', None)
self._viewedon_lte = self._params.get('viewedon_lte', None)
self._cookedon_gte = self._params.get('cookedon_gte', None)
self._cookedon_lte = self._params.get('cookedon_lte', None)
self._createdby = self._params.get('createdby', None)
self._makenow = self._params.get('makenow', None)
# this supports hidden feature to find recipes missing X ingredients
if isinstance(self._makenow, bool) and self._makenow == True:
@@ -130,16 +150,19 @@ class RecipeSearch():
self._build_sort_order()
self._recently_viewed(num_recent=self._num_recent)
self._cooked_on_filter(cooked_date=self._cookedon)
self._created_on_filter(created_date=self._createdon)
self._updated_on_filter(updated_date=self._updatedon)
self._viewed_on_filter(viewed_date=self._viewedon)
self._favorite_recipes(times_cooked=self._timescooked)
self._cooked_on_filter()
self._created_on_filter()
self._updated_on_filter()
self._viewed_on_filter()
self._created_by_filter(created_by_user_id=self._createdby)
self._favorite_recipes()
self._new_recipes()
self.keyword_filters(**self._keywords)
self.food_filters(**self._foods)
self.book_filters(**self._books)
self.rating_filter(rating=self._rating)
self.rating_filter()
self.internal_filter(internal=self._internal)
self.step_filters(steps=self._steps)
self.unit_filters(units=self._units)
@@ -186,9 +209,9 @@ class RecipeSearch():
else:
order += default_order
order[:] = [Lower('name').asc() if x ==
'name' else x for x in order]
'name' else x for x in order]
order[:] = [Lower('name').desc() if x ==
'-name' else x for x in order]
'-name' else x for x in order]
self.orderby = order
def string_filters(self, string=None):
@@ -227,9 +250,9 @@ class RecipeSearch():
query_filter |= Q(**{"%s" % f: self._string})
self._queryset = self._queryset.filter(query_filter).distinct()
def _cooked_on_filter(self, cooked_date=None):
if self._sort_includes('lastcooked') or cooked_date:
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
def _cooked_on_filter(self):
if self._sort_includes('lastcooked') or self._cookedon_gte or self._cookedon_lte:
lessthan = self._sort_includes('-lastcooked') or self._cookedon_lte
if lessthan:
default = timezone.now() - timedelta(days=100000)
else:
@@ -237,51 +260,44 @@ class RecipeSearch():
self._queryset = self._queryset.annotate(
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
)
if cooked_date is None:
return
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
else:
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
def _created_on_filter(self, created_date=None):
if created_date is None:
return
lessthan = '-' in created_date[:1]
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
else:
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
def _updated_on_filter(self, updated_date=None):
if updated_date is None:
return
lessthan = '-' in updated_date[:1]
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
else:
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
if self._cookedon_lte:
self._queryset = self._queryset.filter(lastcooked__date__lte=self._cookedon_lte).exclude(lastcooked=default)
elif self._cookedon_gte:
self._queryset = self._queryset.filter(lastcooked__date__gte=self._cookedon_gte).exclude(lastcooked=default)
def _viewed_on_filter(self, viewed_date=None):
if self._sort_includes('lastviewed') or viewed_date:
if self._sort_includes('lastviewed') or self._viewedon_gte or self._viewedon_lte:
longTimeAgo = timezone.now() - timedelta(days=100000)
self._queryset = self._queryset.annotate(
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
)
if viewed_date is None:
return
lessthan = '-' in viewed_date[:1]
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
else:
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
if self._viewedon_lte:
self._queryset = self._queryset.filter(lastviewed__date__lte=self._viewedon_lte).exclude(lastviewed=longTimeAgo)
elif self._viewedon_gte:
self._queryset = self._queryset.filter(lastviewed__date__gte=self._viewedon_gte).exclude(lastviewed=longTimeAgo)
def _created_on_filter(self):
if self._createdon:
self._queryset = self._queryset.filter(created_at__date=self._createdon)
elif self._createdon_lte:
self._queryset = self._queryset.filter(created_at__date__lte=self._createdon_lte)
elif self._createdon_gte:
self._queryset = self._queryset.filter(created_at__date__gte=self._createdon_gte)
def _updated_on_filter(self):
if self._updatedon:
self._queryset = self._queryset.filter(updated_at__date__date=self._updatedon)
elif self._updatedon_lte:
self._queryset = self._queryset.filter(updated_at__date__lte=self._updatedon_lte)
elif self._updatedon_gte:
self._queryset = self._queryset.filter(updated_at__date__gte=self._updatedon_gte)
def _created_by_filter(self, created_by_user_id=None):
if created_by_user_id is None:
return
self._queryset = self._queryset.filter(created_by__id=created_by_user_id)
def _new_recipes(self, new_days=7):
# TODO make new days a user-setting
@@ -307,9 +323,9 @@ class RecipeSearch():
)
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked:
less_than = '-' in (str(times_cooked) or []) and not self._sort_includes('-favorite')
def _favorite_recipes(self):
if self._sort_includes('favorite') or self._timescooked or self._timescooked_gte or self._timescooked_lte:
less_than = self._timescooked_lte and not self._sort_includes('-favorite')
if less_than:
default = 1000
else:
@@ -321,15 +337,13 @@ class RecipeSearch():
.values('count')
)
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
if times_cooked is None:
return
if times_cooked == '0':
if self._timescooked:
self._queryset = self._queryset.filter(favorite=0)
elif less_than:
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
else:
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
elif self._timescooked_lte:
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
elif self._timescooked_gte:
self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte))
def keyword_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -407,25 +421,16 @@ class RecipeSearch():
units = [units]
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
def rating_filter(self, rating=None):
if rating or self._sort_includes('rating'):
lessthan = '-' in (rating or [])
reverse = 'rating' in (self._sort_order or []) and '-rating' not in (self._sort_order or [])
if lessthan or reverse:
default = 100
else:
default = 0
# TODO make ratings a settings user-only vs all-users
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
if rating is None:
return
def rating_filter(self):
if self._rating or self._rating_lte or self._rating_gte or self._sort_includes('rating'):
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=0))))
if rating == '0':
self._queryset = self._queryset.filter(rating=0)
elif lessthan:
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
else:
self._queryset = self._queryset.filter(rating__gte=int(rating))
if self._rating:
self._queryset = self._queryset.filter(rating=round(int(self._rating)))
elif self._rating_gte:
self._queryset = self._queryset.filter(rating__gte=int(self._rating_gte))
elif self._rating_lte:
self._queryset = self._queryset.filter(rating__gte=int(self._rating_lte)).exclude(rating=0)
def internal_filter(self, internal=None):
if not internal:
@@ -535,11 +540,11 @@ class RecipeSearch():
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
onhand_filter = (
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
# or substitute food onhand
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
# or substitute food onhand
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
)
makenow_recipes = Recipe.objects.annotate(
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),

View File

@@ -106,14 +106,14 @@ def get_from_scraper(scrape, request):
# assign image
try:
recipe_json['image'] = parse_image(scrape.image()) or None
recipe_json['image_url'] = parse_image(scrape.image()) or None
except Exception:
recipe_json['image'] = None
if not recipe_json['image']:
recipe_json['image_url'] = None
if not recipe_json['image_url']:
try:
recipe_json['image'] = parse_image(scrape.schema.data.get('image')) or ''
recipe_json['image_url'] = parse_image(scrape.schema.data.get('image')) or ''
except Exception:
recipe_json['image'] = ''
recipe_json['image_url'] = ''
# assign keywords
try:
@@ -205,6 +205,7 @@ def get_from_scraper(scrape, request):
except Exception:
pass
recipe_json['properties'] = []
try:
recipe_json['properties'] = get_recipe_properties(request.space, scrape.schema.nutrients())
print(recipe_json['properties'])
@@ -227,6 +228,13 @@ def get_recipe_properties(space, property_data):
"property-proteins": "proteinContent",
"property-fats": "fatContent",
}
serving_size = 1
try:
serving_size = parse_servings(property_data['servingSize'])
except KeyError:
pass
recipe_properties = []
for pt in PropertyType.objects.filter(space=space, open_data_slug__in=list(properties.keys())).all():
for p in list(properties.keys()):
@@ -237,7 +245,7 @@ def get_recipe_properties(space, property_data):
'id': pt.id,
'name': pt.name,
},
'property_amount': parse_servings(property_data[properties[p]]) / parse_servings(property_data['servingSize']),
'property_amount': parse_servings(property_data[properties[p]]) / serving_size,
})
return recipe_properties
@@ -424,9 +432,9 @@ def parse_keywords(keyword_json, request):
if len(kw) != 0:
kw = automation_engine.apply_keyword_automation(kw)
if k := Keyword.objects.filter(name__iexact=kw, space=request.space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
keywords.append({'label': str(k), 'name': k.name, 'id': k.id, 'import_keyword': True})
else:
keywords.append({'label': kw, 'name': kw})
keywords.append({'label': kw, 'name': kw, 'import_keyword': False})
return keywords

View File

@@ -113,7 +113,7 @@ class RecipeShoppingEditor():
if not self.servings:
self.servings = getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', 1.0)
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings)
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings, space=self.space, created_by=self.created_by)
if ingredients:
self._add_ingredients(ingredients=ingredients)
@@ -153,8 +153,9 @@ class RecipeShoppingEditor():
return True
for sle in ShoppingListEntry.objects.filter(list_recipe=self._shopping_list_recipe):
sle.amount = sle.ingredient.amount * Decimal(self._servings_factor)
sle.save()
if sle.ingredient: # TODO temporarily dont scale manual entries until ingredient_amount or some other base amount has been migrated to SLE
sle.amount = sle.ingredient.amount * Decimal(self._servings_factor)
sle.save()
self._shopping_list_recipe.servings = self.servings
self._shopping_list_recipe.save()
return True

View File

@@ -70,7 +70,7 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
parsed_md = md.markdown(
instructions,
extensions=[
'markdown.extensions.fenced_code', TableExtension(),
'markdown.extensions.fenced_code', 'markdown.extensions.sane_lists', 'markdown.extensions.nl2br', TableExtension(),
UrlizeExtension(), MarkdownFormatExtension()
]
)

View File

@@ -1,10 +1,10 @@
import imghdr
import json
import re
from io import BytesIO
from zipfile import ZipFile
import requests
from PIL import Image
from django.utils.translation import gettext as _
@@ -128,7 +128,7 @@ class RecetteTek(Integration):
url = file['originalPicture']
if validate_import_url(url):
response = requests.get(url)
if imghdr.what(BytesIO(response.content)) is not None:
if Image.open(BytesIO(response.content)).verify():
self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
else:
raise Exception("Original image failed to download.")

View File

@@ -13,7 +13,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-01 15:04+0200\n"
"PO-Revision-Date: 2025-02-27 10:58+0000\n"
"PO-Revision-Date: 2025-03-12 09:58+0000\n"
"Last-Translator: Nico G <aprops@gmail.com>\n"
"Language-Team: Catalan <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/ca/>\n"
@@ -1795,6 +1795,15 @@ msgid ""
"the management command 'python manage.py rebuildindex'\n"
" "
msgstr ""
" \n"
" La cerca amb trigram i la cerca de text complet es basen en l"
"índex de bases de dades per operar de manera eficaç. \n"
" Podeu reconstruir els índexs de tots els camps de la pàgina d"
"administració de receptes, seleccionant totes les receptes i realitzant l"
"acció “Reconstruir líndex per a la recepta seleccionada”.\n"
" També podeu reconstruir els índexs de la línia de comandaments "
"executant el la comanda 'Python Manage.py RebuildIndex'\n"
" "
#: .\cookbook\templates\settings.html:25
msgid ""
@@ -1904,6 +1913,8 @@ msgid ""
"You can sign in to your account using any of the following third party\n"
" accounts:"
msgstr ""
"Pots accedir al teu compte mitjançant qualsevol dels \n"
" comptes de tercers següents:"
#: .\cookbook\templates\socialaccount\connections.html:52
msgid ""
@@ -1922,26 +1933,27 @@ msgstr "Registre"
#: .\cookbook\templates\socialaccount\login.html:9
#, python-format
msgid "Connect %(provider)s"
msgstr ""
msgstr "Connectar %(provider)s"
#: .\cookbook\templates\socialaccount\login.html:11
#, python-format
msgid "You are about to connect a new third party account from %(provider)s."
msgstr ""
msgstr "Estàs a punt de connectar un nou compte de tercers per a %(provider)s."
#: .\cookbook\templates\socialaccount\login.html:13
#, python-format
msgid "Sign In Via %(provider)s"
msgstr ""
msgstr "Connectar via %(provider)s"
#: .\cookbook\templates\socialaccount\login.html:15
#, python-format
msgid "You are about to sign in using a third party account from %(provider)s."
msgstr ""
"Estàs a punt de connectar fent servir un compte de tercers de %(provider)s."
#: .\cookbook\templates\socialaccount\login.html:20
msgid "Continue"
msgstr ""
msgstr "Continuar"
#: .\cookbook\templates\socialaccount\signup.html:10
#, python-format
@@ -1950,6 +1962,10 @@ msgid ""
" %(provider_name)s account to login to\n"
" %(site_name)s. As a final step, please complete the following form:"
msgstr ""
"Estàs a punt d'utilitzar el teu \n"
" %(provider_name)s compte per connectar a \n"
" %(site_name)s. Per a finalitzar, si us plau completa el següent "
"formulari:"
#: .\cookbook\templates\socialaccount\snippets\provider_list.html:23
#: .\cookbook\templates\socialaccount\snippets\provider_list.html:31
@@ -2003,7 +2019,7 @@ msgstr "Pots ser convidat a un espai existent o crear el teu propi."
#: .\cookbook\templates\space_overview.html:53
msgid "Owner"
msgstr ""
msgstr "Propietari"
#: .\cookbook\templates\space_overview.html:57
#, fuzzy
@@ -2070,6 +2086,11 @@ msgid ""
"script to generate version information (done automatically in docker).\n"
" "
msgstr ""
"\n"
" Necessites executar <code>version.py</code> al teu script "
"d'actualització per generar informació de la versió (feta automàticament a "
"Docker).\n"
" "
#: .\cookbook\templates\system.html:46
msgid "Media Serving"
@@ -2155,7 +2176,7 @@ msgstr ""
#: .\cookbook\templates\system.html:86
msgid "Allowed Hosts"
msgstr ""
msgstr "Allotjaments permesos"
#: .\cookbook\templates\system.html:90
msgid ""

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-01 15:04+0200\n"
"PO-Revision-Date: 2025-03-02 16:58+0000\n"
"Last-Translator: fabio souza jr <fabiosouzajr@gmail.com>\n"
"PO-Revision-Date: 2025-03-13 22:58+0000\n"
"Last-Translator: Jader Moraes <jader-ms@hotmail.com>\n"
"Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/pt_BR/>\n"
"Language: pt_BR\n"
@@ -173,13 +173,12 @@ msgstr ""
"diminuir a qualidade de pesquisa dependendo do idioma em uso"
#: .\cookbook\forms.py:343
#, fuzzy
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr ""
"Campos que devem ser procurados por correspondências parciais (por exemplo, "
"procurar por \"Pie\" retornará \"pie\", \"piece\" e \"soapie\")"
"Campos a serem usados para pesquisa parcial (ex: pesquisar 'Pão' pode "
"retornar 'pão', 'pãozinho' e 'pão de queijo')"
#: .\cookbook\forms.py:344
msgid ""
@@ -758,6 +757,10 @@ msgid ""
"for user %(user_display)s\n"
" ."
msgstr ""
"Por favor, confirme que\n"
" <a href=\"mailto:%(email)s\">%(email)s</a> é um endereço de e-"
"mail do usuário %(user_display)s\n"
" ."
#: .\cookbook\templates\account\email_confirm.html:22
#: .\cookbook\templates\generic\delete_template.html:72

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.2.18 on 2025-03-14 10:50
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', '0219_connectorconfig_supports_description_field'),
]
operations = [
migrations.AddField(
model_name='shoppinglistrecipe',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='shoppinglistrecipe',
name='space',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 4.2.18 on 2025-03-14 10:50
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.db.models import F, Count
from django_scopes import scopes_disabled
def add_space_and_owner_to_shopping_list_recipe(apps, schema_editor):
print('migrating shopping list recipe space attribute, this might take a while ...')
with scopes_disabled():
ShoppingListRecipe = apps.get_model('cookbook', 'ShoppingListRecipe')
# delete all shopping list recipes that do not have entries as those are of no use anyway
ShoppingListRecipe.objects.annotate(entry_count=Count('entries')).filter(entry_count__lte=0).delete()
shopping_list_recipes = ShoppingListRecipe.objects.all().prefetch_related('entries')
update_list = []
for slr in shopping_list_recipes:
if entry := slr.entries.first():
if entry.space and entry.created_by:
slr.space = entry.space
slr.created_by = entry.created_by
update_list.append(slr)
else:
print(slr, 'missing data on entry')
else:
print(slr, 'missing entry')
ShoppingListRecipe.objects.bulk_update(update_list, ['space', 'created_by'], batch_size=500)
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0220_shoppinglistrecipe_created_by_and_more'),
]
operations = [
migrations.RunPython(add_space_and_owner_to_shopping_list_recipe),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 4.2.18 on 2025-03-14 12:41
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', '0221_migrate_shoppinglistrecipe_space_created_by'),
]
operations = [
migrations.AlterField(
model_name='shoppinglistrecipe',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='shoppinglistrecipe',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
]

View File

@@ -126,7 +126,7 @@ class TreeModel(MP_Node):
return None
@property
def full_name(self):
def full_name(self) -> str:
"""
Returns a string representation of a tree node and it's ancestors,
e.g. 'Cuisine > Asian > Chinese > Catonese'.
@@ -912,12 +912,19 @@ class PropertyType(models.Model, PermissionModelMixin, MergeModelMixin):
GOAL = 'GOAL'
OTHER = 'OTHER'
CHOICES = (
(NUTRITION, _('Nutrition')),
(ALLERGEN, _('Allergen')),
(PRICE, _('Price')),
(GOAL, _('Goal')),
(OTHER, _('Other')),
)
name = models.CharField(max_length=128)
unit = models.CharField(max_length=64, blank=True, null=True)
order = models.IntegerField(default=0)
description = models.CharField(max_length=512, blank=True, null=True)
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')),
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
category = models.CharField(max_length=64, choices=CHOICES, null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.IntegerField(null=True, default=None, blank=True)
@@ -1179,31 +1186,18 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=32, blank=True, default='')
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True) # TODO make required after old shoppinglist deprecated
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
mealplan = models.ForeignKey(MealPlan, on_delete=models.CASCADE, null=True, blank=True)
objects = ScopedManager(space='recipe__space')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe.space
objects = ScopedManager(space='space')
def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}'
def get_owner(self):
try:
if not self.entries.exists():
return 'orphan'
else:
return getattr(self.entries.first(), 'created_by', None)
except AttributeError:
return None
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
@@ -1463,19 +1457,21 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
UNIT_REPLACE = 'UNIT_REPLACE'
NAME_REPLACE = 'NAME_REPLACE'
automation_types = (
(FOOD_ALIAS, _('Food Alias')),
(UNIT_ALIAS, _('Unit Alias')),
(KEYWORD_ALIAS, _('Keyword Alias')),
(DESCRIPTION_REPLACE, _('Description Replace')),
(INSTRUCTION_REPLACE, _('Instruction Replace')),
(NEVER_UNIT, _('Never Unit')),
(TRANSPOSE_WORDS, _('Transpose Words')),
(FOOD_REPLACE, _('Food Replace')),
(UNIT_REPLACE, _('Unit Replace')),
(NAME_REPLACE, _('Name Replace')),
)
type = models.CharField(max_length=128,
choices=(
(FOOD_ALIAS, _('Food Alias')),
(UNIT_ALIAS, _('Unit Alias')),
(KEYWORD_ALIAS, _('Keyword Alias')),
(DESCRIPTION_REPLACE, _('Description Replace')),
(INSTRUCTION_REPLACE, _('Instruction Replace')),
(NEVER_UNIT, _('Never Unit')),
(TRANSPOSE_WORDS, _('Transpose Words')),
(FOOD_REPLACE, _('Food Replace')),
(UNIT_REPLACE, _('Unit Replace')),
(NAME_REPLACE, _('Name Replace')),
))
choices=automation_types)
name = models.CharField(max_length=128, default='')
description = models.TextField(blank=True, null=True)

View File

@@ -1,69 +0,0 @@
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.schemas.utils import is_list_view
class QueryParam(object):
def __init__(self, name, description=None, qtype='string', required=False):
self.name = name
self.description = description
self.qtype = qtype
self.required = required
def __str__(self):
return f'{self.name}, {self.qtype}, {self.description}'
class QueryParamAutoSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return super().get_path_parameters(path, method)
parameters = super().get_path_parameters(path, method)
for q in self.view.query_params:
parameters.append({
"name": q.name, "in": "query", "required": q.required,
"description": q.description,
'schema': {'type': q.qtype, },
})
return parameters
class TreeSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return super(TreeSchema, self).get_path_parameters(path, method)
api_name = path.split('/')[2]
parameters = super().get_path_parameters(path, method)
parameters.append({
"name": 'query', "in": "query", "required": False,
"description": 'Query string matched against {} name.'.format(api_name),
'schema': {'type': 'string', },
})
parameters.append({
"name": 'root', "in": "query", "required": False,
"description": 'Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.'.format(
obj=api_name),
'schema': {'type': 'integer', },
})
parameters.append({
"name": 'tree', "in": "query", "required": False,
"description": 'Return all self and children of {} with ID [int].'.format(api_name),
'schema': {'type': 'integer', },
})
return parameters
class FilterSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return super(FilterSchema, self).get_path_parameters(path, method)
api_name = path.split('/')[2]
parameters = super().get_path_parameters(path, method)
parameters.append({
"name": 'query', "in": "query", "required": False,
"description": 'Query string matched against {} name.'.format(api_name),
'schema': {'type': 'string', },
})
return parameters

View File

@@ -4,7 +4,8 @@ from decimal import Decimal
from gettext import gettext as _
from html import escape
from smtplib import SMTPException
from drf_spectacular.utils import extend_schema_field
from django.forms.models import model_to_dict
from django.contrib.auth.models import AnonymousUser, Group, User
from django.core.cache import caches
from django.core.mail import send_mail
@@ -13,7 +14,8 @@ from django.http import BadHeaderError
from django.urls import reverse
from django.utils import timezone
from django_scopes import scopes_disabled
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
from drf_writable_nested import UniqueFieldsMixin
from drf_writable_nested import WritableNestedModelSerializer as WNMS
from oauth2_provider.models import AccessToken
from PIL import Image
from rest_framework import serializers
@@ -36,7 +38,34 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
class WritableNestedModelSerializer(WNMS):
# overload to_internal_value to allow using PK only on nested object
def to_internal_value(self, data):
# iterate through every field on the posted object
for f in list(data):
if f not in self.fields:
continue
elif issubclass(self.fields[f].__class__, serializers.Serializer):
# if the field is a serializer and an integer, assume its an ID of an existing object
if isinstance(data[f], int):
# only retrieve serializer required fields
required_fields = ['id'] + [field_name for field_name, field in self.fields[f].__class__().fields.items() if field.required]
data[f] = model_to_dict(self.fields[f].Meta.model.objects.get(id=data[f]), fields=required_fields)
elif issubclass(self.fields[f].__class__, serializers.ListSerializer):
# if the field is a ListSerializer get dict values of PKs provided
if any(isinstance(x, int) for x in data[f]):
# only retrieve serializer required fields
required_fields = ['id'] + [field_name for field_name, field in self.fields[f].child.__class__().fields.items() if field.required]
# filter values to integer values
pk_data = [x for x in data[f] if isinstance(x, int)]
# merge non-pk values with retrieved values
data[f] = [x for x in data[f] if not isinstance(x, int)] \
+ list(self.fields[f].child.Meta.model.objects.filter(id__in=pk_data).values(*required_fields))
return super().to_internal_value(data)
class ExtendedRecipeMixin(serializers.ModelSerializer):
@@ -47,7 +76,7 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
images = None
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.ReadOnlyField(source='recipe_count')
numrecipe = serializers.IntegerField(source='recipe_count', read_only=True)
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
@@ -57,8 +86,7 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
if str2bool(
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
return fields
except (AttributeError, KeyError):
pass
@@ -92,6 +120,7 @@ class OpenDataModelMixin(serializers.ModelSerializer):
return super().update(instance, validated_data)
@extend_schema_field(float)
class CustomDecimalField(serializers.Field):
"""
Custom decimal field to normalize useless decimal places
@@ -115,6 +144,7 @@ class CustomDecimalField(serializers.Field):
raise ValidationError('A valid number is required')
@extend_schema_field(bool)
class CustomOnHandField(serializers.Field):
def get_attribute(self, instance):
return instance
@@ -123,16 +153,12 @@ class CustomOnHandField(serializers.Field):
if not self.context["request"].user.is_authenticated:
return []
shared_users = []
if c := caches['default'].get(
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
shared_users = c
else:
try:
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
self.context['request'].user.id]
caches['default'].set(
f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
shared_users, timeout=5 * 60)
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
caches['default'].set(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', shared_users, timeout=5 * 60)
# TODO ugly hack that improves API performance significantly, should be done properly
except AttributeError: # Anonymous users (using share links) don't have shared users
pass
@@ -143,34 +169,39 @@ class CustomOnHandField(serializers.Field):
class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data):
if self.context.get('request', None) is None:
return
if (isinstance(data, QuerySet) and data.query.is_sliced):
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
if self.child.Meta.model == User:
# Don't return User details to anonymous users
if isinstance(self.context['request'].user, AnonymousUser):
data = []
else:
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
elif isinstance(data, list):
data = [d for d in data if getattr(d, self.child.Meta.model.get_space_key()[0]) == self.context['request'].space]
else:
data = data.filter(**{'__'.join(data.model.get_space_key()): self.context['request'].space})
data = data.filter(**{'__'.join(self.child.Meta.model.get_space_key()): self.context['request'].space})
return super().to_representation(data)
class UserSerializer(WritableNestedModelSerializer):
display_name = serializers.SerializerMethodField('get_user_label')
@extend_schema_field(str)
def get_user_label(self, obj):
return obj.get_user_display_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username', 'first_name', 'last_name', 'display_name')
read_only_fields = ('username',)
fields = ('id', 'username', 'first_name', 'last_name', 'display_name', 'is_staff', 'is_superuser', 'is_active')
read_only_fields = ('id', 'username', 'display_name', 'is_staff', 'is_superuser', 'is_active')
class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
@@ -183,6 +214,7 @@ class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class Meta:
model = Group
fields = ('id', 'name')
read_only_fields = ('id', 'name')
class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
@@ -202,13 +234,16 @@ class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelSerialize
class UserFileSerializer(serializers.ModelSerializer):
file = serializers.FileField(write_only=True)
created_by = UserSerializer(read_only=True)
file = serializers.FileField(write_only=True, required=False)
file_download = serializers.SerializerMethodField('get_download_link')
preview = serializers.SerializerMethodField('get_preview_link')
@extend_schema_field(serializers.CharField(read_only=True))
def get_download_link(self, obj):
return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
@extend_schema_field(serializers.CharField(read_only=True))
def get_preview_link(self, obj):
try:
Image.open(obj.file.file.file)
@@ -250,18 +285,21 @@ class UserFileSerializer(serializers.ModelSerializer):
class Meta:
model = UserFile
fields = ('id', 'name', 'file', 'file_download', 'preview', 'file_size_kb')
read_only_fields = ('id', 'file_size_kb')
fields = ('id', 'name', 'file', 'file_download', 'preview', 'file_size_kb', 'created_by', 'created_at')
read_only_fields = ('id', 'file_download', 'preview', 'file_size_kb', 'created_by', 'created_at')
extra_kwargs = {"file": {"required": False, }}
class UserFileViewSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True)
file_download = serializers.SerializerMethodField('get_download_link')
preview = serializers.SerializerMethodField('get_preview_link')
@extend_schema_field(str)
def get_download_link(self, obj):
return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk}))
@extend_schema_field(str)
def get_preview_link(self, obj):
try:
Image.open(obj.file.file.file)
@@ -278,11 +316,12 @@ class UserFileViewSerializer(serializers.ModelSerializer):
class Meta:
model = UserFile
fields = ('id', 'name', 'file_download', 'preview')
read_only_fields = ('id', 'file')
fields = ('id', 'name', 'file_download', 'preview', 'file_size_kb', 'created_by', 'created_at')
read_only_fields = ('id', 'file', 'file_download', 'file_size_kb', 'preview', 'created_by', 'created_at')
class SpaceSerializer(WritableNestedModelSerializer):
created_by = UserSerializer(read_only=True)
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
@@ -298,12 +337,15 @@ class SpaceSerializer(WritableNestedModelSerializer):
logo_color_512 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_svg = UserFileViewSerializer(required=False, many=False, allow_null=True)
@extend_schema_field(int)
def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count()
@extend_schema_field(int)
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
@extend_schema_field(float)
def get_file_size_mb(self, obj):
try:
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
@@ -366,15 +408,18 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
class UserPreferenceSerializer(WritableNestedModelSerializer):
user = UserSerializer(read_only=True)
food_inherit_default = serializers.SerializerMethodField('get_food_inherit_defaults')
plan_share = UserSerializer(many=True, allow_null=True, required=False)
shopping_share = UserSerializer(many=True, allow_null=True, required=False)
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
image = UserFileViewSerializer(required=False, allow_null=True, many=False)
@extend_schema_field(FoodInheritFieldSerializer)
def get_food_inherit_defaults(self, obj):
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
@extend_schema_field(bool)
def get_food_children_exist(self, obj):
space = getattr(self.context.get('request', None), 'space', None)
return Food.objects.filter(depth__gt=0, space=space).exists()
@@ -399,6 +444,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients',
'food_children_exist'
)
read_only_fields = ('user',)
class StorageSerializer(SpacedModelSerializer):
@@ -461,22 +507,24 @@ class SyncLogSerializer(SpacedModelSerializer):
class KeywordLabelSerializer(serializers.ModelSerializer):
label = serializers.SerializerMethodField('get_label')
@extend_schema_field(str)
def get_label(self, obj):
return str(obj)
class Meta:
list_serializer_class = SpaceFilterSerializer
model = Keyword
fields = (
'id', 'label',
)
fields = ('id', 'label')
read_only_fields = ('id', 'label')
class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
label = serializers.SerializerMethodField('get_label')
label = serializers.SerializerMethodField('get_label', allow_null=False)
parent = IntegerField(read_only=True)
recipe_filter = 'keywords'
@extend_schema_field(str)
def get_label(self, obj):
return str(obj)
@@ -600,8 +648,9 @@ class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class RecipeSimpleSerializer(WritableNestedModelSerializer):
url = serializers.SerializerMethodField('get_url')
@extend_schema_field(str)
def get_url(self, obj):
return reverse('view_recipe', args=[obj.id])
return f'recipe/{obj.pk}'
def create(self, validated_data):
# don't allow writing to Recipe via this API
@@ -616,6 +665,21 @@ class RecipeSimpleSerializer(WritableNestedModelSerializer):
fields = ('id', 'name', 'url')
class RecipeFlatSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
# don't allow writing to Recipe via this API
return Recipe.objects.get(**validated_data)
def update(self, instance, validated_data):
# don't allow writing to Recipe via this API
return Recipe.objects.get(**validated_data)
class Meta:
model = Recipe
fields = ('id', 'name', 'image')
class FoodSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Food
@@ -625,12 +689,13 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
shopping = serializers.ReadOnlyField(source='shopping_status')
shopping = serializers.CharField(source='shopping_status', read_only=True)
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
food_onhand = CustomOnHandField(required=False, allow_null=True)
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
parent = IntegerField(read_only=True)
properties = PropertySerializer(many=True, allow_null=True, required=False)
properties_food_unit = UnitSerializer(allow_null=True, required=False)
@@ -639,6 +704,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
recipe_filter = 'steps__ingredients__food'
images = ['recipe__image']
@extend_schema_field(bool)
def get_substitute_onhand(self, obj):
if not self.context["request"].user.is_authenticated:
return []
@@ -736,12 +802,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields',
'open_data_slug',
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url', 'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
'food_onhand', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@@ -752,7 +815,9 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
amount = CustomDecimalField()
conversions = serializers.SerializerMethodField('get_conversions')
checked = serializers.BooleanField(read_only=True, default=False, help_text='Just laziness to have a checked field on the frontend API client')
@extend_schema_field(list)
def get_used_in_recipes(self, obj):
used_in = []
for s in obj.step_set.all():
@@ -760,6 +825,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
used_in.append({'id': r.id, 'name': r.name})
return used_in
@extend_schema_field(list)
def get_conversions(self, obj):
if obj.unit and obj.food:
uch = UnitConversionHelper(self.context['request'].space)
@@ -804,12 +870,16 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
@extend_schema_field(str)
def get_instructions_markdown(self, obj):
return obj.get_instruction_render()
@extend_schema_field(serializers.ListField)
def get_step_recipes(self, obj):
return list(obj.recipe_set.values_list('id', flat=True).all())
# couldn't set proper serializer StepRecipeSerializer because of circular reference
@extend_schema_field(serializers.JSONField)
def get_step_recipe_data(self, obj):
# check if root type is recipe to prevent infinite recursion
# can be improved later to allow multi level embedding
@@ -819,8 +889,7 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
class Meta:
model = Step
fields = (
'id', 'name', 'instruction', 'ingredients', 'instructions_markdown',
'time', 'order', 'show_as_header', 'file', 'step_recipe',
'id', 'name', 'instruction', 'ingredients', 'instructions_markdown', 'time', 'order', 'show_as_header', 'file', 'step_recipe',
'step_recipe_data', 'numrecipe', 'show_ingredients_table'
)
@@ -830,9 +899,7 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
class Meta:
model = Recipe
fields = (
'id', 'name', 'steps',
)
fields = ('id', 'name', 'steps')
class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin):
@@ -843,6 +910,7 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
base_amount = CustomDecimalField()
converted_amount = CustomDecimalField()
@extend_schema_field(str)
def get_conversion_name(self, obj):
text = f'{round(obj.base_amount)} {obj.base_unit} '
if obj.food:
@@ -884,6 +952,7 @@ class NutritionInformationSerializer(serializers.ModelSerializer):
class RecipeBaseSerializer(WritableNestedModelSerializer):
# TODO make days of new recipe a setting
@extend_schema_field(bool)
def is_recipe_new(self, obj):
if getattr(obj, 'new_recipe', None) or obj.created_at > (timezone.now() - timedelta(days=7)):
return True
@@ -899,12 +968,12 @@ class CommentSerializer(serializers.ModelSerializer):
class RecipeOverviewSerializer(RecipeBaseSerializer):
keywords = KeywordLabelSerializer(many=True)
new = serializers.SerializerMethodField('is_recipe_new')
recent = serializers.ReadOnlyField()
rating = CustomDecimalField(required=False, allow_null=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True)
keywords = KeywordLabelSerializer(many=True, read_only=True)
new = serializers.SerializerMethodField('is_recipe_new', read_only=True)
recent = serializers.CharField(read_only=True)
rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
created_by = UserSerializer(read_only=True)
def create(self, validated_data):
pass
@@ -919,7 +988,14 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
read_only_fields = ['image', 'created_by', 'created_at']
# TODO having these readonly fields makes "RecipeOverview.ts" (API Client) not generate the RecipeOverviewToJSON second else block which leads to errors when using the api
# TODO find a solution (custom schema?) to have these fields readonly (to save performance) and generate a proper client (two serializers would probably do the trick)
# read_only_fields = ['id', 'name', 'description', 'image', 'keywords', 'working_time',
# 'waiting_time', 'created_by', 'created_at', 'updated_at',
# 'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent']
read_only_fields = ['image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent']
class RecipeSerializer(RecipeBaseSerializer):
@@ -931,7 +1007,9 @@ class RecipeSerializer(RecipeBaseSerializer):
rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
food_properties = serializers.SerializerMethodField('get_food_properties')
created_by = UserSerializer(read_only=True)
@extend_schema_field(serializers.JSONField)
def get_food_properties(self, obj):
fph = FoodPropertyHelper(obj.space) # initialize with object space since recipes might be viewed anonymously
return fph.calculate_recipe_properties(obj)
@@ -939,12 +1017,9 @@ class RecipeSerializer(RecipeBaseSerializer):
class Meta:
model = Recipe
fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings',
'file_path', 'servings_text', 'rating',
'last_cooked',
'private', 'shared',
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
'last_cooked', 'private', 'shared'
)
read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
@@ -967,7 +1042,7 @@ class RecipeImageSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
if 'image' in validated_data and not is_file_type_allowed(validated_data['image'].name, image_only=True):
return None
return super().create( validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
if 'image' in validated_data and not is_file_type_allowed(validated_data['image'].name, image_only=True):
@@ -999,6 +1074,7 @@ class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerialize
class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
created_by = UserSerializer(read_only=True)
shared = UserSerializer(many=True)
filter = CustomFilterSerializer(allow_null=True, required=False)
@@ -1016,9 +1092,11 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
book_content = serializers.SerializerMethodField(method_name='get_book_content', read_only=True)
recipe_content = serializers.SerializerMethodField(method_name='get_recipe_content', read_only=True)
@extend_schema_field(RecipeBookSerializer)
def get_book_content(self, obj):
return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book)
@extend_schema_field(RecipeOverviewSerializer)
def get_recipe_content(self, obj):
return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
@@ -1038,19 +1116,22 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
recipe = RecipeOverviewSerializer(required=False, allow_null=True)
recipe_name = serializers.ReadOnlyField(source='recipe.name')
recipe_name = serializers.CharField(source='recipe.name', read_only=True)
meal_type = MealTypeSerializer()
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
meal_type_name = serializers.CharField(source='meal_type.name', read_only=True) # TODO deprecate once old meal plan was removed
note_markdown = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()
shared = UserSerializer(many=True, required=False, allow_null=True)
shopping = serializers.SerializerMethodField('in_shopping')
addshopping = serializers.BooleanField(write_only=True, required=False)
to_date = serializers.DateTimeField(required=False)
@extend_schema_field(str)
def get_note_markdown(self, obj):
return markdown(obj.note)
@extend_schema_field(bool)
def in_shopping(self, obj):
return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists()
@@ -1060,18 +1141,31 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
if 'to_date' not in validated_data or validated_data['to_date'] is None:
validated_data['to_date'] = validated_data['from_date']
add_to_shopping = False
try:
add_to_shopping = validated_data.pop('addshopping', False)
except KeyError:
pass
mealplan = super().create(validated_data)
if self.context['request'].data.get('addshopping', False) and self.context['request'].data.get('recipe', None):
if add_to_shopping and self.context['request'].data.get('recipe', None):
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
SLR.create(mealplan=mealplan, servings=validated_data['servings'])
return mealplan
def update(self, obj, validated_data):
if sr := ShoppingListRecipe.objects.filter(mealplan=obj.id).first():
SLR = RecipeShoppingEditor(user=obj.created_by, space=obj.space, id=sr.id)
SLR.edit(mealplan=obj, servings=validated_data['servings'])
return super().update(obj, validated_data)
class Meta:
model = MealPlan
fields = (
'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'meal_type_name', 'shopping'
'meal_type_name', 'shopping', 'addshopping'
)
read_only_fields = ('created_by',)
@@ -1087,27 +1181,17 @@ class AutoMealPlanSerializer(serializers.Serializer):
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
recipe_name = serializers.ReadOnlyField(source='recipe.name')
mealplan_note = serializers.ReadOnlyField(source='mealplan.note')
mealplan_from_date = serializers.ReadOnlyField(source='mealplan.from_date')
mealplan_type = serializers.ReadOnlyField(source='mealplan.meal_type.name')
recipe_data = RecipeOverviewSerializer(source='recipe', read_only=True, required=False)
meal_plan_data = MealPlanSerializer(source='mealplan', read_only=True, required=False)
servings = CustomDecimalField()
created_by = UserSerializer(read_only=True)
def get_name(self, obj):
if not isinstance(value := obj.servings, Decimal):
value = Decimal(value)
value = value.quantize(
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
def update(self, instance, validated_data):
# TODO remove once old shopping list
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
SLR = RecipeShoppingEditor(user=self.context['request'].user, space=self.context['request'].space)
SLR.edit_servings(servings=validated_data['servings'], id=instance.id)
@@ -1115,18 +1199,19 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingListRecipe
fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note', 'mealplan_from_date',
'mealplan_type')
read_only_fields = ('id',)
fields = ('id', 'name', 'recipe', 'recipe_data', 'mealplan', 'meal_plan_data', 'servings', 'created_by',)
read_only_fields = ('id', 'created_by',)
class ShoppingListEntrySerializer(WritableNestedModelSerializer):
food = FoodSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True, required=False)
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
list_recipe_data = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserSerializer(read_only=True)
completed_at = serializers.DateTimeField(allow_null=True, required=False)
mealplan_id = serializers.IntegerField(required=False, write_only=True,
help_text='If a mealplan id is given try to find existing or create new ShoppingListRecipe with that meal plan and link entry to it')
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
@@ -1160,31 +1245,56 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
if 'mealplan_id' in validated_data:
if existing_slr := ShoppingListRecipe.objects.filter(mealplan_id=validated_data['mealplan_id'], space=self.context['request'].space).first():
validated_data['list_recipe'] = existing_slr
else:
validated_data['list_recipe'] = ShoppingListRecipe.objects.create(mealplan_id=validated_data['mealplan_id'], space=self.context['request'].space,
created_by=self.context['request'].user)
del validated_data['mealplan_id']
return super().create(validated_data)
def update(self, instance, validated_data):
user = self.context['request'].user
if 'mealplan_id' in validated_data:
del validated_data['mealplan_id']
# update the onhand for food if shopping_add_onhand is True
if user.userpreference.shopping_add_onhand:
if checked := validated_data.get('checked', None):
validated_data['completed_at'] = timezone.now()
instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
elif checked == False:
elif not checked:
instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)
return super().update(instance, validated_data)
class Meta:
model = ShoppingListEntry
fields = (
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked',
'recipe_mealplan',
'created_by', 'created_at', 'updated_at', 'completed_at', 'delay_until'
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked', 'ingredient',
'list_recipe_data', 'created_by', 'created_at', 'updated_at', 'completed_at', 'delay_until', 'mealplan_id'
)
read_only_fields = ('id', 'created_by', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_by', 'created_at')
class ShoppingListEntrySimpleCreateSerializer(serializers.Serializer):
amount = CustomDecimalField()
unit_id = serializers.IntegerField(allow_null=True)
food_id = serializers.IntegerField(allow_null=True)
ingredient_id = serializers.IntegerField(allow_null=True)
class ShoppingListEntryBulkCreateSerializer(serializers.Serializer):
entries = serializers.ListField(child=ShoppingListEntrySimpleCreateSerializer())
class ShoppingListEntryBulkSerializer(serializers.Serializer):
ids = serializers.ListField()
checked = serializers.BooleanField()
timestamp = serializers.DateTimeField(read_only=True, required=False)
# TODO deprecate
@@ -1218,7 +1328,13 @@ class ViewLogSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
view_log = ViewLog.objects.filter(recipe=validated_data['recipe'], created_by=self.context['request'].user, created_at__gt=(timezone.now() - timezone.timedelta(minutes=5)),
space=self.context['request'].space).first()
if not view_log:
view_log = ViewLog.objects.create(recipe=validated_data['recipe'], created_by=self.context['request'].user, space=self.context['request'].space)
return view_log
class Meta:
model = ViewLog
@@ -1279,7 +1395,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
validated_data['space'] = self.context['request'].space
obj = super().create(validated_data)
if obj.email:
if obj.email and EMAIL_HOST != '':
try:
if InviteLink.objects.filter(space=self.context['request'].space,
created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
@@ -1347,9 +1463,13 @@ class AccessTokenSerializer(serializers.ModelSerializer):
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
@extend_schema_field(str)
def get_token(self, obj):
if (timezone.now() - obj.created).seconds < 15:
return obj.token
if obj.scope == 'bookmarklet':
# bookmarklet only tokens are always returned because they have very limited access and are needed for the bookmarklet function to work
return obj.token
return f'tda_************_******_***********{obj.token[len(obj.token) - 4:]}'
class Meta:
@@ -1358,6 +1478,45 @@ class AccessTokenSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'token',)
class LocalizationSerializer(serializers.Serializer):
code = serializers.CharField(max_length=8, read_only=True)
language = serializers.CharField(read_only=True)
class Meta:
fields = '__ALL__'
class ServerSettingsSerializer(serializers.Serializer):
# TODO add all other relevant settings including path/url related ones?
shopping_min_autosync_interval = serializers.CharField()
enable_pdf_export = serializers.BooleanField()
enable_ai_import = serializers.BooleanField()
disable_external_connectors = serializers.BooleanField()
terms_url = serializers.CharField()
privacy_url = serializers.CharField()
imprint_url = serializers.CharField()
hosted = serializers.BooleanField()
debug = serializers.BooleanField()
version = serializers.CharField()
class Meta:
fields = '__ALL__'
read_only_fields = '__ALL__'
class FdcQueryFoodsSerializer(serializers.Serializer):
fdcId = serializers.IntegerField()
description = serializers.CharField()
dataType = serializers.CharField()
class FdcQuerySerializer(serializers.Serializer):
totalHits = serializers.IntegerField()
currentPage = serializers.IntegerField()
totalPages = serializers.IntegerField()
foods = FdcQueryFoodsSerializer(many=True)
# Export/Import Serializers
class KeywordExportSerializer(KeywordSerializer):
@@ -1440,8 +1599,8 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False,
help_text=_("Existing shopping list to update"))
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
ingredients = serializers.ListField(child=serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added.")))
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_(
"Providing a list_recipe ID and servings of 0 will delete that shopping list."))
@@ -1469,3 +1628,77 @@ class RecipeFromSourceSerializer(serializers.Serializer):
url = serializers.CharField(max_length=4096, required=False, allow_null=True, allow_blank=True)
data = serializers.CharField(required=False, allow_null=True, allow_blank=True)
bookmarklet = serializers.IntegerField(required=False, allow_null=True, )
class SourceImportFoodSerializer(serializers.Serializer):
name = serializers.CharField()
class SourceImportUnitSerializer(serializers.Serializer):
name = serializers.CharField()
class SourceImportIngredientSerializer(serializers.Serializer):
amount = serializers.FloatField()
food = SourceImportFoodSerializer()
unit = SourceImportUnitSerializer()
note = serializers.CharField(required=False)
original_text = serializers.CharField()
class SourceImportStepSerializer(serializers.Serializer):
instruction = serializers.CharField()
ingredients = SourceImportIngredientSerializer(many=True)
show_ingredients_table = serializers.BooleanField(default=True)
class SourceImportKeywordSerializer(serializers.Serializer):
id = serializers.IntegerField(allow_null=True)
label = serializers.CharField()
name = serializers.CharField()
import_keyword = serializers.BooleanField(default=True)
class SourceImportPropertyTypeSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
class SourceImportPropertySerializer(serializers.Serializer):
property_type = SourceImportPropertyTypeSerializer(many=False)
property_amount = serializers.FloatField()
class SourceImportRecipeSerializer(serializers.Serializer):
steps = SourceImportStepSerializer(many=True)
internal = serializers.BooleanField(default=True)
source_url = serializers.URLField()
name = serializers.CharField()
description = serializers.CharField(default=None)
servings = serializers.IntegerField(default=1)
servings_text = serializers.CharField(default='')
working_time = serializers.IntegerField(default=0)
waiting_time = serializers.IntegerField(default=0)
image_url = serializers.URLField(default=None)
keywords = SourceImportKeywordSerializer(many=True, default=[])
properties = serializers.ListField(child=SourceImportPropertySerializer(), default=[])
class SourceImportDuplicateSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
class RecipeFromSourceResponseSerializer(serializers.Serializer):
recipe = SourceImportRecipeSerializer(default=None)
recipe_id = serializers.IntegerField(default=None)
images = serializers.ListField(child=serializers.CharField(), default=[], allow_null=False)
error = serializers.BooleanField(default=False)
msg = serializers.CharField(max_length=1024, default='')
duplicates = serializers.ListField(child=SourceImportDuplicateSerializer(), default=[], allow_null=False)
class AiImportSerializer(serializers.Serializer):
file = serializers.FileField(allow_null=True)
text = serializers.CharField(allow_null=True, allow_blank=True)

View File

@@ -120,37 +120,6 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
child.save()
@receiver(post_save, sender=MealPlan)
def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs):
print("MEAL_AUTO_ADD Signal trying to auto add to shopping")
if not instance:
print("MEAL_AUTO_ADD Instance is none")
return
try:
space = instance.get_space()
user = instance.get_owner()
with scope(space=space):
slr_exists = instance.shoppinglistrecipe_set.exists()
if not created and slr_exists:
for x in instance.shoppinglistrecipe_set.all():
# assuming that permissions checks for the MealPlan have happened upstream
if instance.servings != x.servings:
SLR = RecipeShoppingEditor(id=x.id, user=user, space=instance.space)
SLR.edit_servings(servings=instance.servings)
elif not user.userpreference.mealplan_autoadd_shopping or not instance.recipe:
print("MEAL_AUTO_ADD No recipe or no setting")
return
if created:
SLR = RecipeShoppingEditor(user=user, space=space)
SLR.create(mealplan=instance, servings=instance.servings)
print("MEAL_AUTO_ADD Created SLR")
except AttributeError:
pass
@receiver(post_save, sender=Unit)
def clear_unit_cache(sender, instance=None, created=False, **kwargs):
if instance:

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg48" inkscape:export-xdpi="48" inkscape:export-ydpi="48" sodipodi:docname="logo_color_shopping.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:serif="http://www.serif.com/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 558.1 558.1"
style="enable-background:new 0 0 558.1 558.1;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:url(#ellipse2_00000062882008473870802360000001235346600214014370_);}
.st1{clip-path:url(#SVGID_00000040562990235419179500000014537515251997333940_);}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#161616;}
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#FFCB76;}
.st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FF6F00;}
.st5{clip-path:url(#SVGID_00000026884998257896383920000012290328997039565247_);}
.st6{fill-rule:evenodd;clip-rule:evenodd;fill:#FFD100;}
.st7{fill:#161616;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview50" inkscape:current-layer="svg48" inkscape:cx="256" inkscape:cy="256" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1377" inkscape:window-maximized="1" inkscape:window-width="2560" inkscape:window-x="2552" inkscape:window-y="-8" inkscape:zoom="2.0039062" objecttolerance="10" pagecolor="#ffffff" showgrid="false">
</sodipodi:namedview>
<g id="Kreis" transform="matrix(0.92371046,0,0,0.95776263,3.7134303,-54.329713)">
<linearGradient id="ellipse2_00000072976586691886204630000005673338158137684613_" gradientUnits="userSpaceOnUse" x1="-24.1585" y1="348.0664" x2="-23.1585" y2="348.0664" gradientTransform="matrix(2.147900e-06 0 0 -2.227081e-06 4347.1548 66.3621)">
<stop offset="0" style="stop-color:#272727"/>
<stop offset="1" style="stop-color:#6C6C6C"/>
</linearGradient>
<ellipse id="ellipse2" style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#ellipse2_00000072976586691886204630000005673338158137684613_);" cx="298.1" cy="348.1" rx="302.1" ry="291.3"/>
<g>
<defs>
<circle id="SVGID_1_" cx="298.1" cy="348.1" r="279"/>
</defs>
<clipPath id="SVGID_00000080899089203538361760000014164288875061347472_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g id="g18" style="clip-path:url(#SVGID_00000080899089203538361760000014164288875061347472_);">
<g id="Shadow" transform="matrix(1.10322,0,0,1.064,-5.58287,50.5786)">
<path id="path7" class="st2" d="M163.2,477.5l271.2,271.2L759.1,557L416.4,214.2L163.2,477.5z"/>
<g id="g11" transform="translate(-4.22105,0.775864)">
<path id="path9" class="st2" d="M223.4,188.6L545.8,511l121-106.1L326,64.1l-3.2,78.4L223.4,188.6z"/>
</g>
<g id="g15" transform="translate(-85.3876,27.8512)">
<path id="path13" class="st2" d="M328.5,154.7l322.4,322.4l3.1-71.6L313.3,64.7l-3.6,82.2L328.5,154.7z"/>
</g>
</g>
</g>
</g>
</g>
<g id="g23" transform="matrix(0.93750213,0,0,0.93750213,15.953517,15.99888)">
<path id="path21" class="st3" d="M280.6,238.7c35.1,0,65.8-14.8,85.3-30.1c19.7-15.6,48.3-12.5,64.2,6.9
c26.2,31.7,41.8,71.9,41.8,115.6c0,71.6-44.2,159.9-105.6,190.9c-26.4,13.3-53.8,19.6-85.6,19.6l0,0h-0.1h-0.1l0,0
c-31.8,0-59.2-6.3-85.6-19.6C133.5,491.1,89.3,402.7,89.3,331.2c0-43.7,15.7-83.9,41.8-115.6c15.9-19.4,44.5-22.5,64.2-6.9
C214.8,224,245.5,238.7,280.6,238.7L280.6,238.7z"/>
</g>
<g id="Flame-2" transform="matrix(0.61547875,0,0,0.56833279,-138.25728,-438.60298)" serif:id="Flame 2">
<path id="path25" class="st4" d="M636,823.4c-2.8-4-2.8-9.6-0.1-13.7c2.8-4.1,7.7-5.6,12.1-3.9c22.2,8.9,51.2,22.5,73.8,40.9
c46.9,38.3,59.7,63.9,70.2,90.3c12.4,31.2,14.2,63.5,11.6,86c-7.6,64.6-56,117.9-125,117.9c-69,0-123.9-52.8-125-117.9
c-0.7-39.2,12.1-70.5,26.1-92.8c3.5-5.6,10-7.8,15.8-5.6c5.8,2.3,9.5,8.6,8.8,15.3c-2,14.1-3.3,28.8-2.7,40.6
c2.2,39.8,25.9,50,50.2,49.8c25.9-0.2,52.1-22.2,42.7-78.4C686.3,902.9,656.8,853.5,636,823.4L636,823.4z"/>
<g>
<defs>
<path id="SVGID_00000067227953264718972400000007310393595166440114_" d="M636,823.4c-2.8-4-2.8-9.6-0.1-13.7
c2.8-4.1,7.7-5.6,12.1-3.9c22.2,8.9,51.2,22.5,73.8,40.9c46.9,38.3,59.7,63.9,70.2,90.3c12.4,31.2,14.2,63.5,11.6,86
c-7.6,64.6-56,117.9-125,117.9c-69,0-123.9-52.8-125-117.9c-0.7-39.2,12.1-70.5,26.1-92.8c3.5-5.6,10-7.8,15.8-5.6
c5.8,2.3,9.5,8.6,8.8,15.3c-2,14.1-3.3,28.8-2.7,40.6c2.2,39.8,25.9,50,50.2,49.8c25.9-0.2,52.1-22.2,42.7-78.4
C686.3,902.9,656.8,853.5,636,823.4L636,823.4z"/>
</defs>
<clipPath id="SVGID_00000178886517717031376290000002615817110574070691_">
<use xlink:href="#SVGID_00000067227953264718972400000007310393595166440114_" style="overflow:visible;"/>
</clipPath>
<g id="g34" style="clip-path:url(#SVGID_00000178886517717031376290000002615817110574070691_);">
<g id="g32" transform="matrix(1.28784,-0.270602,0.285942,1.59598,247.349,825.209)">
<path id="path30" class="st6" d="M279.8,36.7c28.5,13.5,59.3,44.8,67.8,85.1c14.1,67-25.3,85.6-59.1,84
c-54.2-2.6-72.4-45.5-36.2-97.1C274.8,76.8,253.9,24.5,279.8,36.7z"/>
</g>
</g>
</g>
</g>
<path class="st7" d="M245.7,280.1c6.2,0,11.1,5,11.1,11.1v11.1h44.5v-11.1c0-6.2,5-11.1,11.1-11.1s11.1,5,11.1,11.1v11.1h16.7
c9.2,0,16.7,7.5,16.7,16.7v16.7H201.2V319c0-9.2,7.5-16.7,16.7-16.7h16.7v-11.1C234.5,285,239.5,280.1,245.7,280.1z M201.2,346.8
h155.8v94.6c0,9.2-7.5,16.7-16.7,16.7H217.8c-9.2,0-16.7-7.5-16.7-16.7V346.8z M223.4,374.6v11.1c0,3.1,2.5,5.6,5.6,5.6h11.1
c3.1,0,5.6-2.5,5.6-5.6v-11.1c0-3.1-2.5-5.6-5.6-5.6H229C225.9,369.1,223.4,371.6,223.4,374.6z M267.9,374.6v11.1
c0,3.1,2.5,5.6,5.6,5.6h11.1c3.1,0,5.6-2.5,5.6-5.6v-11.1c0-3.1-2.5-5.6-5.6-5.6h-11.1C270.4,369.1,267.9,371.6,267.9,374.6z
M318,369.1c-3.1,0-5.6,2.5-5.6,5.6v11.1c0,3.1,2.5,5.6,5.6,5.6h11.1c3.1,0,5.6-2.5,5.6-5.6v-11.1c0-3.1-2.5-5.6-5.6-5.6H318z
M223.4,419.1v11.1c0,3.1,2.5,5.6,5.6,5.6h11.1c3.1,0,5.6-2.5,5.6-5.6v-11.1c0-3.1-2.5-5.6-5.6-5.6H229
C225.9,413.6,223.4,416.1,223.4,419.1z M273.5,413.6c-3.1,0-5.6,2.5-5.6,5.6v11.1c0,3.1,2.5,5.6,5.6,5.6h11.1c3.1,0,5.6-2.5,5.6-5.6
v-11.1c0-3.1-2.5-5.6-5.6-5.6H273.5z M312.4,419.1v11.1c0,3.1,2.5,5.6,5.6,5.6h11.1c3.1,0,5.6-2.5,5.6-5.6v-11.1
c0-3.1-2.5-5.6-5.6-5.6H318C314.9,413.6,312.4,416.1,312.4,419.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg48" inkscape:export-xdpi="48" inkscape:export-ydpi="48" sodipodi:docname="logo_color_shopping.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:serif="http://www.serif.com/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 558.1 558.1"
style="enable-background:new 0 0 558.1 558.1;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:url(#ellipse2_00000123406314579419878720000015292012387505797789_);}
.st1{clip-path:url(#SVGID_00000069378195777491616980000011609770225407185072_);}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#161616;}
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#FFCB76;}
.st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FF6F00;}
.st5{clip-path:url(#SVGID_00000129168706913539839680000006972943119257724314_);}
.st6{fill-rule:evenodd;clip-rule:evenodd;fill:#FFD100;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview50" inkscape:current-layer="svg48" inkscape:cx="256" inkscape:cy="256" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1377" inkscape:window-maximized="1" inkscape:window-width="2560" inkscape:window-x="2552" inkscape:window-y="-8" inkscape:zoom="2.0039062" objecttolerance="10" pagecolor="#ffffff" showgrid="false">
</sodipodi:namedview>
<g id="Kreis" transform="matrix(0.92371046,0,0,0.95776263,3.7134303,-54.329713)">
<linearGradient id="ellipse2_00000092432231847401152480000013557045245219773118_" gradientUnits="userSpaceOnUse" x1="-24.1585" y1="348.0664" x2="-23.1585" y2="348.0664" gradientTransform="matrix(2.147900e-06 0 0 -2.227081e-06 4347.1548 66.3621)">
<stop offset="0" style="stop-color:#272727"/>
<stop offset="1" style="stop-color:#6C6C6C"/>
</linearGradient>
<ellipse id="ellipse2" style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#ellipse2_00000092432231847401152480000013557045245219773118_);" cx="298.1" cy="348.1" rx="302.1" ry="291.3"/>
<g>
<defs>
<circle id="SVGID_1_" cx="298.1" cy="348.1" r="279"/>
</defs>
<clipPath id="SVGID_00000078750348294658121660000005213362532985289101_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g id="g18" style="clip-path:url(#SVGID_00000078750348294658121660000005213362532985289101_);">
<g id="Shadow" transform="matrix(1.10322,0,0,1.064,-5.58287,50.5786)">
<path id="path7" class="st2" d="M163.2,477.5l271.2,271.2L759.1,557L416.4,214.2L163.2,477.5z"/>
<g id="g11" transform="translate(-4.22105,0.775864)">
<path id="path9" class="st2" d="M223.4,188.6L545.8,511l121-106.1L326,64.1l-3.2,78.4L223.4,188.6z"/>
</g>
<g id="g15" transform="translate(-85.3876,27.8512)">
<path id="path13" class="st2" d="M328.5,154.7l322.4,322.4l3.1-71.6L313.3,64.7l-3.6,82.2L328.5,154.7z"/>
</g>
</g>
</g>
</g>
</g>
<g id="g23" transform="matrix(0.93750213,0,0,0.93750213,15.953517,15.99888)">
<path id="path21" class="st3" d="M280.6,238.7c35.1,0,65.8-14.8,85.3-30.1c19.7-15.6,48.3-12.5,64.2,6.9
c26.2,31.7,41.8,71.9,41.8,115.6c0,71.6-44.2,159.9-105.6,190.9c-26.4,13.3-53.8,19.6-85.6,19.6l0,0h-0.1h-0.1l0,0
c-31.8,0-59.2-6.3-85.6-19.6C133.5,491.1,89.3,402.7,89.3,331.2c0-43.7,15.7-83.9,41.8-115.6c15.9-19.4,44.5-22.5,64.2-6.9
C214.8,224,245.5,238.7,280.6,238.7L280.6,238.7z"/>
</g>
<g id="Flame-2" transform="matrix(0.61547875,0,0,0.56833279,-138.25728,-438.60298)" serif:id="Flame 2">
<path id="path25" class="st4" d="M636,823.4c-2.8-4-2.8-9.6-0.1-13.7c2.8-4.1,7.7-5.6,12.1-3.9c22.2,8.9,51.2,22.5,73.8,40.9
c46.9,38.3,59.7,63.9,70.2,90.3c12.4,31.2,14.2,63.5,11.6,86c-7.6,64.6-56,117.9-125,117.9c-69,0-123.9-52.8-125-117.9
c-0.7-39.2,12.1-70.5,26.1-92.8c3.5-5.6,10-7.8,15.8-5.6c5.8,2.3,9.5,8.6,8.8,15.3c-2,14.1-3.3,28.8-2.7,40.6
c2.2,39.8,25.9,50,50.2,49.8c25.9-0.2,52.1-22.2,42.7-78.4C686.3,902.9,656.8,853.5,636,823.4L636,823.4z"/>
<g>
<defs>
<path id="SVGID_00000170974775404888027640000005842834300324528060_" d="M636,823.4c-2.8-4-2.8-9.6-0.1-13.7
c2.8-4.1,7.7-5.6,12.1-3.9c22.2,8.9,51.2,22.5,73.8,40.9c46.9,38.3,59.7,63.9,70.2,90.3c12.4,31.2,14.2,63.5,11.6,86
c-7.6,64.6-56,117.9-125,117.9c-69,0-123.9-52.8-125-117.9c-0.7-39.2,12.1-70.5,26.1-92.8c3.5-5.6,10-7.8,15.8-5.6
c5.8,2.3,9.5,8.6,8.8,15.3c-2,14.1-3.3,28.8-2.7,40.6c2.2,39.8,25.9,50,50.2,49.8c25.9-0.2,52.1-22.2,42.7-78.4
C686.3,902.9,656.8,853.5,636,823.4L636,823.4z"/>
</defs>
<clipPath id="SVGID_00000137101924108689735560000002063526552501109413_">
<use xlink:href="#SVGID_00000170974775404888027640000005842834300324528060_" style="overflow:visible;"/>
</clipPath>
<g id="g34" style="clip-path:url(#SVGID_00000137101924108689735560000002063526552501109413_);">
<g id="g32" transform="matrix(1.28784,-0.270602,0.285942,1.59598,247.349,825.209)">
<path id="path30" class="st6" d="M279.8,36.7c28.5,13.5,59.3,44.8,67.8,85.1c14.1,67-25.3,85.6-59.1,84
c-54.2-2.6-72.4-45.5-36.2-97.1C274.8,76.8,253.9,24.5,279.8,36.7z"/>
</g>
</g>
</g>
</g>
<path id="path958" class="st2" d="M182.4,290.6c0-4.5,3.6-8.1,8.1-8.1h15.4c7.4,0,14,4.3,17.1,10.8h139.1c8.9,0,15.4,8.5,13.1,17.1
l-13.9,51.5c-2.9,10.6-12.5,18-23.5,18h-97.6l1.8,9.6c0.7,3.8,4.1,6.6,8,6.6h97.6c4.5,0,8.1,3.6,8.1,8.1s-3.6,8.1-8.1,8.1H250
c-11.7,0-21.8-8.3-23.9-19.8L208.6,301c-0.2-1.3-1.4-2.2-2.7-2.2h-15.4C186,298.8,182.4,295.1,182.4,290.6L182.4,290.6z
M225.7,439.5c0-9,7.3-16.3,16.2-16.3c9,0,16.3,7.3,16.3,16.2c0,0,0,0,0,0c0,9-7.3,16.3-16.2,16.3
C233,455.8,225.7,448.5,225.7,439.5C225.7,439.6,225.7,439.5,225.7,439.5z M339.4,423.3c9,0,16.2,7.3,16.3,16.2
c0,9-7.3,16.2-16.2,16.3c0,0,0,0,0,0c-9,0-16.2-7.3-16.3-16.2C323.2,430.6,330.4,423.3,339.4,423.3
C339.4,423.3,339.4,423.3,339.4,423.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

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