Compare commits

...

418 Commits

Author SHA1 Message Date
vabene1111
f52dd9ba51 recompiled messages 2020-07-15 19:33:09 +02:00
vabene1111
4ffc4dc248 updates german translation 2020-07-15 19:26:41 +02:00
vabene1111
696791d059 url import improvements 2020-07-15 18:28:43 +02:00
vabene1111
c9f1576b24 several recipe view improvements 2020-07-15 18:05:12 +02:00
vabene1111
1cea0350c9 added todo info 2020-07-14 00:23:35 +02:00
vabene1111
e0dac67b84 fixed recipe api permissions when using shared recipes 2020-07-14 00:22:39 +02:00
vabene1111
c45472689e small fixes 2020-07-13 23:07:35 +02:00
vabene1111
5b7ec37637 hotkeys, styling, step improvements 2020-07-13 22:55:09 +02:00
vabene1111
4d08477b23 fixed decimals 2020-07-13 21:38:42 +02:00
vabene1111
9b9fbd5cab added food api view tests 2020-07-09 22:19:24 +02:00
vabene1111
8c118dbd3e use appropriate serializer for unit 2020-07-09 22:16:52 +02:00
vabene1111
bfe9e794c5 added tests for unit api view 2020-07-09 22:13:33 +02:00
vabene1111
3e16361322 added keyword api tests 2020-07-09 22:00:14 +02:00
vabene1111
35e8dc4ce9 removed keyword created by as it was never populated 2020-07-09 21:55:05 +02:00
vabene1111
8e8c7fe563 fixed sync test 2020-07-09 00:43:09 +02:00
vabene1111
69d5e628c5 sync admin view + tests 2020-07-08 23:40:53 +02:00
vabene1111
e65c8436ef sync api view + tests 2020-07-08 23:28:28 +02:00
vabene1111
e83565b1f2 added api endpoints and tests 2020-07-08 23:19:39 +02:00
vabene1111
692811f84f fixed settings dropdown being below recipe edit buttons 2020-07-08 22:26:06 +02:00
vabene1111
0aa37d176f api function queryset cleanup 2020-07-08 22:24:52 +02:00
vabene1111
92341380bc actually fixed error 2020-07-08 22:14:22 +02:00
vabene1111
19df215bd2 external recipe view vue error fix 2020-07-08 22:03:41 +02:00
vabene1111
372dd0afa1 recipe step sidebar no name 2020-07-08 21:55:17 +02:00
vabene1111
b89c38a696 error messages 2020-07-08 21:53:55 +02:00
vabene1111
f177fed8e2 Merge branch 'feature/recipe-steps' into develop 2020-07-08 21:23:58 +02:00
vabene1111
1d2976b687 create default step + view time improvement 2020-07-08 21:23:41 +02:00
vabene1111
b490673866 javascript toasts 2020-07-08 21:01:01 +02:00
vabene1111
5f2320a3e7 added save confirm 2020-07-05 22:36:39 +02:00
vabene1111
5b916e955c WIP page leave confirm 2020-07-05 22:15:00 +02:00
vabene1111
5bad507845 fixed and improved URL import 2020-07-05 21:11:25 +02:00
vabene1111
64603fca51 added time to text steps 2020-07-02 13:23:16 +02:00
vabene1111
6018101012 fixed step time calculation 2020-07-02 13:19:27 +02:00
vabene1111
ddaa9e6356 added step time calculation 2020-07-02 13:16:15 +02:00
vabene1111
98f9555ef6 check box linking 2020-07-02 12:15:30 +02:00
vabene1111
1619d4894c step header rendering 2020-07-02 12:12:17 +02:00
vabene1111
1e2d7f77af basic vue in recipe view 2020-07-02 11:20:11 +02:00
vabene1111
80423da16d removed duplicate div 2020-07-01 23:44:58 +02:00
vabene1111
67a4a2627b fixed meal plan form not own type display 2020-07-01 23:29:32 +02:00
vabene1111
0005ca243c testing around 2020-07-01 23:22:29 +02:00
vabene1111
adbc7e4a39 icons and stuff 2020-07-01 22:27:32 +02:00
vabene1111
9caec2dff2 step sidebar sorting 2020-07-01 21:53:41 +02:00
vabene1111
41eef9a913 step sorting buttons 2020-07-01 21:42:46 +02:00
vabene1111
31357c552c ui improvements 2020-07-01 21:04:28 +02:00
vabene1111
5d70dca039 time step + step delete + sorting improvement 2020-07-01 20:38:35 +02:00
vabene1111
2c5fbc558e fixed recipe image preview scaling in large search 2020-07-01 18:56:00 +02:00
vabene1111
98bdad5955 second fix 2020-06-29 22:58:31 +02:00
vabene1111
6eb41fe33f fixed keywords and food/unit loading 2020-06-29 22:41:54 +02:00
vabene1111
e94bd32a70 license update 2020-06-29 22:22:35 +02:00
vabene1111
29e2f9ee15 fixed filter for foods 2020-06-29 22:20:55 +02:00
vabene1111
357771e81b updated translation files 2020-06-29 22:20:42 +02:00
vabene1111
941264731e fixed ingredient adding attributes 2020-06-29 22:03:43 +02:00
vabene1111
2c59302fe9 conditional header/no amount render 2020-06-29 22:01:42 +02:00
vabene1111
a96bf9a4d7 license update 2020-06-29 21:07:55 +02:00
vabene1111
e2508cbc6f step ordering 2020-06-29 20:45:30 +02:00
vabene1111
577af85d38 fixed import export 2020-06-29 19:09:15 +02:00
vabene1111
38e5882376 added recipe large search context menu 2020-06-29 18:17:13 +02:00
vabene1111
08f06fe1d8 fixed non fa-fw recipe view edit icon 2020-06-29 18:16:10 +02:00
vabene1111
4ddc39f1c2 basic shopping list fix 2020-06-29 18:11:25 +02:00
vabene1111
78275145f8 one more non atomic transaction 2020-06-29 18:07:56 +02:00
vabene1111
f7b518d890 run rename migration as non atomic to support old sqlite versions 2020-06-29 18:04:56 +02:00
vabene1111
8cff44df7d recipe steps tagging support 2020-06-29 17:56:50 +02:00
vabene1111
cdee31e9af tests working again 2020-06-29 16:43:53 +02:00
vabene1111
06db7114ed basic view step design 2020-06-29 16:24:41 +02:00
vabene1111
8f28e4766d side nav in recipe edit 2020-06-29 16:12:24 +02:00
vabene1111
dbdb35e2f3 reworked base container to allow side cols 2020-06-29 15:59:19 +02:00
vabene1111
27963fcaa6 image upload working 2020-06-29 15:39:58 +02:00
vabene1111
d613da7faf mobile and large layout 2020-06-29 14:10:21 +02:00
vabene1111
d481767154 wip flex ui 2020-06-29 13:29:59 +02:00
vabene1111
8b9c7daaae fixed commas + desktop flex design 2020-06-29 12:27:54 +02:00
vabene1111
3a2ba773cf layout before flex test 2020-06-29 11:30:24 +02:00
vabene1111
642224fec9 sorting and ingredient adding 2020-06-26 15:00:41 +02:00
vabene1111
3d4aebcd9d ordering of ingredients and steps 2020-06-26 14:50:33 +02:00
vabene1111
c178aea363 nested serializers and basic recipe editing working 2020-06-26 13:41:41 +02:00
vabene1111
5e20833b7e testing with serializers 2020-06-26 13:10:23 +02:00
vabene1111
db7f0d4988 fixed recipe edit button text 2020-06-26 08:53:46 +02:00
vabene1111
381bc5ffb4 partially working steps edit view 2020-06-25 23:48:27 +02:00
vabene1111
4a6b32d9af super basic edit view 2020-06-25 23:24:09 +02:00
vabene1111
8b814669af fixed API permission denied error
forgot a return in the custom permissions wich prevent authorized useres to access certain endpoints
2020-06-25 23:23:12 +02:00
vabene1111
ae0928f85f added recipe steps 2020-06-25 22:38:17 +02:00
vabene1111
bf516d37a9 Merge branch 'develop' into feature/recipe-steps 2020-06-25 22:09:00 +02:00
vabene1111
6979bf34d9 fixed insecure uuid defaults
due to some unexpected behavior the share link uuid values were not random. All already created share links were disabled.
2020-06-25 22:01:54 +02:00
vabene1111
bda38f0647 changed recipe ingredient relation 2020-06-25 21:45:16 +02:00
vabene1111
df912b8865 renamed recipeingredient to ingredient 2020-06-25 21:37:18 +02:00
vabene1111
f685253645 renamed ingredient to food 2020-06-25 21:24:03 +02:00
vabene1111
2c5e44d73c improved recipe parser, added tests, cleaned up resources 2020-06-24 21:22:23 +02:00
vabene1111
b6d98397b5 improved recipe import 2020-06-24 20:21:48 +02:00
vabene1111
525ad6dd98 removed debug code and cdn dependencies 2020-06-23 20:36:04 +02:00
vabene1111
166833fe83 moved recipe edit button to dropdown menu 2020-06-23 19:01:35 +02:00
vabene1111
752854028a more recipe import fixes 2020-06-23 18:48:22 +02:00
vabene1111
6197cab1ed improved url import [WIP] 2020-06-23 18:13:16 +02:00
vabene1111
73f2240763 fixed import error 2020-06-23 14:00:07 +02:00
vabene1111
3ef82aee9c updated readme 2020-06-23 12:57:05 +02:00
vabene1111
e49e53e2b2 async loading of multiselect fields 2020-06-23 12:54:27 +02:00
vabene1111
0266476aef import page style update 2020-06-23 12:46:13 +02:00
vabene1111
5ed369ba69 recipe import unit/ingredeitn selection 2020-06-23 12:34:00 +02:00
vabene1111
98c278fe60 importing improvements 2020-06-23 11:04:49 +02:00
vabene1111
8594346488 basic importing working 2020-06-23 10:34:04 +02:00
vabene1111
dc91e1e8ed fixed url router and added recipe import test ressources 2020-06-22 23:35:18 +02:00
vabene1111
976dd13a31 basic importing working 2020-06-22 23:23:06 +02:00
vabene1111
8cca272bb9 import UI cleanup 2020-06-22 22:56:56 +02:00
vabene1111
f066b7097c microdata import 2020-06-22 22:47:33 +02:00
vabene1111
71b41a9ca2 refactor json ld code into helper 2020-06-22 21:35:56 +02:00
vabene1111
9e748552b2 recipe url import ld json 2020-06-22 21:16:31 +02:00
vabene1111
743d7bf608 cleanup vue debug settings 2020-06-22 12:23:49 +02:00
vabene1111
315b4521b6 test page 2020-06-22 12:14:24 +02:00
vabene1111
b558ba55b4 fixed mis-aligned menu in recipe view 2020-06-22 11:12:26 +02:00
vabene1111
0368630c92 added more view tests 2020-06-17 15:25:16 +02:00
vabene1111
02c1ba0c71 structured and extended test 2020-06-17 15:06:29 +02:00
vabene1111
83cc8832cb improved duplicate testing code 2020-06-17 14:53:00 +02:00
vabene1111
14a5d43dc8 added complete test for user preference api 2020-06-17 14:33:55 +02:00
vabene1111
bea079dd05 further permission cleanup 2020-06-17 13:23:04 +02:00
vabene1111
df8170fa55 improved permission handlin 2020-06-17 13:18:28 +02:00
vabene1111
2904d5938d fixed sync create permission 2020-06-17 13:00:13 +02:00
vabene1111
18bfecb026 fixed DRF object permission 2020-06-17 12:12:42 +02:00
vabene1111
4ee5a4fd9f testing with user permission 2020-06-17 11:06:08 +02:00
vabene1111
bbaedfad33 documentation update api 2020-06-17 10:29:01 +02:00
vabene1111
de413f1473 custom browsable api header 2020-06-17 10:12:27 +02:00
vabene1111
d012385088 WIP api stuff 2020-06-16 20:32:41 +02:00
vabene1111
d18a330135 reset log level 2020-06-16 19:56:42 +02:00
vabene1111
a8f7ef8ef7 added missing dependency 2020-06-16 19:55:48 +02:00
vabene1111
d6972cacfb pyyaml dependency 2020-06-16 19:43:44 +02:00
vabene1111
3b21e44422 log level debug 2020-06-16 18:57:08 +02:00
vabene1111
1a78ca68bb added access logging 2020-06-16 18:50:24 +02:00
vabene1111
fac7b8cd5b added gunicorn error logging 2020-06-16 18:43:16 +02:00
vabene1111
8f780545a4 api browser link 2020-06-16 18:14:41 +02:00
vabene1111
218f7d92d7 fixed password settings autofocus 2020-06-16 18:04:42 +02:00
vabene1111
621bacff1c api token settings 2020-06-16 18:01:16 +02:00
vabene1111
9a849a979c ui secret key check 2020-06-16 17:42:53 +02:00
vabene1111
e8366e5280 added secret key system check 2020-06-16 17:42:43 +02:00
vabene1111
0a8270e7cf api documentation basics 2020-06-16 17:21:50 +02:00
vabene1111
aad8b220d1 added custom 404 page 2020-06-16 16:21:15 +02:00
vabene1111
d5e0a0a623 added comment setting to user pref admin table 2020-06-16 12:45:46 +02:00
vabene1111
8cd94d49e8 added comment system preference 2020-06-16 12:44:45 +02:00
vabene1111
08b805a547 fixed meal plan delete after create 2020-06-16 12:12:37 +02:00
vabene1111
ecac30136b no sharing external recipes + hide buttons for unauth 2020-06-16 12:07:18 +02:00
vabene1111
d694408af6 share link admin 2020-06-16 12:05:36 +02:00
vabene1111
6e284f6ae8 moved all recipe button 2020-06-16 12:03:42 +02:00
vabene1111
62c049a6de fixed unauthenticated recipe viewing 2020-06-16 11:49:02 +02:00
vabene1111
dee7249347 added sharing links and appropriate tests 2020-06-16 11:23:58 +02:00
vabene1111
17946c8dac markdown hint text 2020-06-16 10:37:11 +02:00
vabene1111
fa2326949e added back markdown support to meal plannin notes 2020-06-16 10:34:20 +02:00
vabene1111
8177d9ba0f added synology install instruction 2020-06-16 10:19:24 +02:00
vabene1111
8781a6572d set some norefer labels
probably not really needed for this case but satisfies the code scanning and does not hurt to have
2020-06-16 10:13:54 +02:00
vabene1111
c7d518071c Create codeql-analysis.yml 2020-06-16 09:56:57 +02:00
vabene1111
ea96c63289 stupid apple fix 2020-06-13 14:03:50 +02:00
vabene1111
8485a64726 viewlog/cooklog admin 2020-06-12 00:07:47 +02:00
vabene1111
e89bd44412 added to string to meal plan 2020-06-11 23:43:48 +02:00
vabene1111
2e0e48bb38 removed celery 2 2020-06-11 23:29:24 +02:00
vabene1111
040fa7c192 removed celery 2020-06-11 23:29:10 +02:00
vabene1111
7000097602 fixed share reset in meal plan move 2020-06-11 22:53:04 +02:00
vabene1111
3cbc6b5609 meal type admin 2020-06-11 22:48:26 +02:00
vabene1111
8ff52f542e ingredient rounding upgrades 2020-06-11 22:32:45 +02:00
vabene1111
8cc0fcaed2 fixed properly deep copy meal_type array 2020-06-11 20:22:04 +02:00
vabene1111
d4197773bf clean up logging 2020-06-11 19:04:15 +02:00
vabene1111
f530b3dc7a dependency cleanup 2020-06-11 19:00:17 +02:00
vabene1111
d1bf4d4bbb added ical export support 2020-06-11 18:38:56 +02:00
vabene1111
d584a3db25 added more help 2020-06-11 17:57:30 +02:00
vabene1111
aaa3737ae0 help and name based type indexing 2020-06-11 17:55:24 +02:00
vabene1111
5072859e57 re enabled basic sharing 2020-06-11 17:27:27 +02:00
vabene1111
ead3c6ef76 fixed sharing 2020-06-11 17:13:31 +02:00
vabene1111
d734cb813e plan type editing 2020-06-11 16:58:57 +02:00
vabene1111
8aa24d4771 per user meal types 2020-06-11 15:19:15 +02:00
vabene1111
c714ff4dbe name function cleanup 2020-06-11 11:42:38 +02:00
vabene1111
a32545c1dc proper mobile handle 2020-06-11 11:40:32 +02:00
vabene1111
dfe8e1fd42 layout improvements 2020-06-11 10:59:26 +02:00
vabene1111
729d573460 imrpoved detail view 2020-06-11 10:26:40 +02:00
vabene1111
8472b541aa correctly update plan 2020-06-11 01:14:09 +02:00
vabene1111
7e95e985ec proper async loading 2020-06-11 01:05:02 +02:00
vabene1111
f7e2aa9b83 week switching 2020-06-11 00:49:19 +02:00
vabene1111
99cf428470 plan entry detail modal 2020-06-10 23:33:18 +02:00
vabene1111
60a533f9c8 Merge branch 'develop' into feature/meal-planning 2020-06-10 16:01:21 +02:00
vabene1111
723416575f added alt text 2020-06-10 15:52:18 +02:00
vabene1111
e34889953a cleanup and allow loading of images 2020-06-10 15:51:47 +02:00
vabene1111
ec8a879efa improved pdf viewer 2020-06-10 15:40:24 +02:00
vabene1111
f44ebe0d05 Update issue templates 2020-06-10 12:09:37 +02:00
vabene1111
13c82cdbf9 made remote auth a config setting 2020-06-10 12:02:27 +02:00
vabene1111
e9a60ece81 Merge pull request #88 from cazier/develop
Adding Reverse Proxy Authentication
2020-06-10 11:56:16 +02:00
vabene1111
9575a86480 fixed ingredient deletion error 2020-06-10 11:52:24 +02:00
vabene1111
12036b9972 fixes protect error on storage monitor delete #102 2020-06-10 11:45:52 +02:00
vabene1111
bffb260dfa fixed delete source permission 2020-06-10 11:42:44 +02:00
vabene1111
9d460a3623 Update docker-publish-release.yml 2020-06-10 11:38:45 +02:00
vabene1111
56c5f28348 Update docker-publish-latest.yml 2020-06-10 11:37:49 +02:00
vabene1111
29d4dcb73d fixed yaml formatting 2020-06-10 11:36:45 +02:00
vabene1111
e60441ec99 add version script to tagged releases 2020-06-10 11:34:05 +02:00
vabene1111
608e024caa formatting + further testing 2020-06-10 11:19:28 +02:00
vabene1111
f596b12f12 testing build ref version writing 2020-06-10 11:08:59 +02:00
vabene1111
bdd41a5ba2 removed redundant quotes 2020-06-10 10:47:47 +02:00
vabene1111
80e566917e version writer script fixes 2020-06-10 10:33:19 +02:00
vabene1111
4294c132c6 version script path fix 2020-06-10 10:24:54 +02:00
vabene1111
da12daaf03 add version to action 2020-06-10 10:15:28 +02:00
vabene1111
150cf5ebac testing auto version 2020-06-10 10:13:11 +02:00
vabene1111
01b9022451 version script test 2020-06-10 10:07:33 +02:00
vabene1111
2bda5bbbf7 note text saving 2020-06-09 20:27:31 +02:00
vabene1111
a743a4e202 basics completly working 2020-06-09 20:15:06 +02:00
vabene1111
ffa7513f9e recipes and notes basically working 2020-06-09 17:26:50 +02:00
vabene1111
8cb6ed2f60 adding items to plan 2020-06-09 13:11:01 +02:00
vabene1111
1d6375bf84 fixed pdf js files missing 2020-06-09 12:28:48 +02:00
vabene1111
cfab867e0d removed no longer valid warning 2020-06-09 00:18:28 +02:00
vabene1111
29dd7c9ee7 add port mapping to plain example 2020-06-09 00:17:17 +02:00
vabene1111
72cb046e37 updated plain example 2020-06-09 00:16:04 +02:00
vabene1111
a555906a32 fixed user setup staff permission 2020-06-06 11:31:19 +02:00
vabene1111
2e255aba0d more plan basics working 2020-06-04 19:46:35 +02:00
vabene1111
a136a18a8e basic updating and loading working 2020-06-04 19:34:47 +02:00
Brendan Cazier
cdf4c0d1bb Re-adding import statement 2020-06-04 07:51:43 -05:00
vabene1111
3aedbfbdc3 Merge branch 'develop' into feature/meal-planning
# Conflicts:
#	requirements.txt
2020-06-04 13:12:52 +02:00
vabene1111
dc7c688ed5 show english first in language chooser 2020-06-04 09:58:18 +02:00
vabene1111
8aa8f15ad7 fixed path + added language option dutch (nl) 2020-06-04 09:57:28 +02:00
vabene1111
6b8a231eee Merge pull request #95 from D0T1X/develop
added base translation file for Dutch
2020-06-04 09:33:17 +02:00
D0T1X
b0e338f08a added base translation files 2020-06-04 07:25:31 +02:00
D0T1X
1a6e0c8706 Uploaded Django.po 2020-06-04 07:24:45 +02:00
D0T1X
c7ceae4350 Create temp 2020-06-04 07:24:24 +02:00
D0T1X
caefa6099b Delete temp 2020-06-04 07:21:33 +02:00
D0T1X
2e4645bb0c Creatend nl/MC_MESSAGES folde 2020-06-04 07:20:23 +02:00
vabene1111
6c966f8ef6 added versions to requirements.txt 2020-06-03 23:43:25 +02:00
vabene1111
7140cb0f93 basic system information page 2020-06-03 23:07:19 +02:00
vabene1111
7f08815482 added .map files 2020-06-03 22:40:48 +02:00
vabene1111
6c0a81a4c5 fixed bnroken tooltips and buttons 2020-06-03 22:36:17 +02:00
vabene1111
9179bde8f9 name images with uuid instead of recipe ids
in order to stil be able to manually map images to recipes the name consists of uuid_recipeId.png
2020-06-03 22:09:07 +02:00
vabene1111
0684c47e2a addding missing cli flag to chmod 2020-06-03 21:52:42 +02:00
vabene1111
be52238413 boot script fix 2020-06-03 21:38:59 +02:00
vabene1111
a0332c432d moved permission fix to boot.sh 2020-06-03 21:31:56 +02:00
vabene1111
35b038a33c media folder permission fix 2020-06-03 21:28:42 +02:00
vabene1111
9ac3d79b95 updated examples 2020-06-03 20:54:38 +02:00
vabene1111
6f24b7d34e fixed media file serving 2020-06-03 20:38:48 +02:00
vabene1111
e84d15a7ed debugging media serving problems 2020-06-03 20:18:42 +02:00
vabene1111
8f268b3b75 corrected .env template 2020-06-03 20:00:44 +02:00
vabene1111
f9cb44c66b gunicorn media file serving 2020-06-03 19:58:01 +02:00
vabene1111
b95c3f6685 working lists 2020-06-03 18:10:34 +02:00
Brendan Cazier
81cd551975 Added some basic documentation 2020-06-03 10:40:12 -05:00
Brendan Cazier
55777fd948 Added REMOTE_USER auth 2020-06-03 10:16:17 -05:00
vabene1111
3b5b505116 basic plan table drag and drop working 2020-06-03 12:05:43 +02:00
vabene1111
aea3f62f9b event parameter 2020-06-03 11:18:37 +02:00
vabene1111
d09c6dbfed added automatic redirect for setup 2020-06-03 10:51:39 +02:00
vabene1111
201c493658 some working drag and drop stuff 2020-06-03 00:03:54 +02:00
vabene1111
8ffc6a0236 some basic drag and drop workin 2020-06-02 22:52:28 +02:00
vabene1111
84ad88b30b Merge branch 'develop' into feature/meal-planning 2020-06-02 22:32:30 +02:00
vabene1111
67d7cd1d23 Update README.md 2020-06-02 22:30:33 +02:00
vabene1111
bf3337b5e1 Update README.md 2020-06-02 22:25:28 +02:00
vabene1111
0b0d214085 update translations 2020-06-02 22:10:50 +02:00
transifex-integration[bot]
60dc008b05 Apply translations in de
translation completed for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'de' language.
2020-06-02 20:09:55 +00:00
transifex-integration[bot]
475b6e3728 Apply translations in de
at least 1% translated for the source file '/cookbook/locale/en/LC_MESSAGES/django.po'
on the 'de' language.

 Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format
2020-06-02 19:51:54 +00:00
vabene1111
0e70cd83e2 added base translation files 2020-06-02 21:21:03 +02:00
vabene1111
233f2a911f nothing working yet 2020-06-02 14:33:27 +02:00
vabene1111
989d8765d7 basic vue stuff working 2020-06-02 12:47:36 +02:00
vabene1111
2fcd207dc7 basic api 2020-06-02 12:04:14 +02:00
vabene1111
a3dc5f283a dynamic meal types 2020-06-02 11:46:16 +02:00
vabene1111
27297d170a removed obsolte all_tags function 2020-06-02 11:07:11 +02:00
vabene1111
bc8f8b8138 improved keyword rendering in search + plan 2020-06-02 11:06:16 +02:00
vabene1111
87ba53fde9 remove spaces and change line break on shopping 2020-06-02 11:03:31 +02:00
vabene1111
098dda28a4 updated readme 2020-06-02 10:58:05 +02:00
vabene1111
8ffe6abb5c markdown docs lin 2020-06-02 10:56:18 +02:00
vabene1111
5706776002 added setting to disable recent viewed recipes 2020-06-02 10:53:32 +02:00
vabene1111
fbe528e935 added github link 2020-06-02 10:49:54 +02:00
vabene1111
0df86b940f setup page 2020-06-02 10:40:21 +02:00
vabene1111
81c3707090 some cleanup + env comments 2020-06-02 10:11:18 +02:00
vabene1111
9fd691b9f0 fixed ci/static path 2020-06-02 09:01:09 +02:00
vabene1111
821136787d fixed shopping list headers #78 2020-06-01 23:23:46 +02:00
vabene1111
6aedba09f3 removed some decentral cdn deps 2020-06-01 23:17:16 +02:00
vabene1111
7b17a1acfa removed all cdn dependencies 2020-06-01 23:13:38 +02:00
vabene1111
9dd538519f fixed recipe table image 2020-06-01 22:51:33 +02:00
vabene1111
b8821f1f72 Merge pull request #80 from cazier/develop
Made recipe images hyperlinked
2020-06-01 22:43:21 +02:00
Brendan Cazier
3420dcd07d Made recipe images hyperlinked 2020-06-01 12:36:47 -05:00
vabene1111
445c01bddc added basic setup template 2020-06-01 13:53:17 +02:00
vabene1111
dd5996084d make jquery local 2020-06-01 13:51:55 +02:00
vabene1111
dfb1d80ca0 fixed duplicates in recent view 2020-05-27 09:38:57 +02:00
vabene1111
744fbc7a46 revert psql distinct change 2020-05-15 13:12:52 +02:00
vabene1111
cd11cc58cf possible duplicate fix 2020-05-15 12:49:31 +02:00
vabene1111
569e385915 Revert "Create FUNDING.yml"
This reverts commit abf552cd18.
2020-05-13 13:27:46 +02:00
vabene1111
abf552cd18 Create FUNDING.yml 2020-05-13 13:22:26 +02:00
vabene1111
c6959488dc fixed typo on search page 2020-05-11 13:08:49 +02:00
vabene1111
85e3155b50 added group required filter to history view 2020-05-11 13:08:00 +02:00
vabene1111
f6aa50bbfc added history page 2020-05-11 12:59:54 +02:00
vabene1111
5ad27c015e markdown info central blockquote css 2020-05-11 12:44:31 +02:00
vabene1111
4a68a99907 show last viewd recipes on search page 2020-05-11 12:42:55 +02:00
vabene1111
123dc1a74d meal plan entry view 2020-05-08 00:10:23 +02:00
vabene1111
2e23fcfd5d added sharing to meal plan + fixed meal plan visibility 2020-05-07 23:16:24 +02:00
vabene1111
edbc21df19 Update README.md 2020-05-06 08:21:53 +02:00
vabene1111
f0e1c901c6 fixed print button tooltip messing up print 2020-05-04 20:48:17 +02:00
vabene1111
22e403e0ff added basic markdown doc 2020-05-03 00:43:13 +02:00
vabene1111
6a7b02b700 add special type of ingredients to allow headers 2020-05-02 23:46:57 +02:00
vabene1111
4aa2983681 order recipe ingredients 2020-05-02 23:05:36 +02:00
vabene1111
18888bc3ae search image heigth fixes 2020-05-02 22:10:02 +02:00
vabene1111
07a0a3f598 recipe book improvements 2020-05-02 21:59:32 +02:00
vabene1111
76e1274ba5 rating/last cooked display 2020-05-02 21:54:38 +02:00
vabene1111
598387efc8 fixed duplicate recipe books when sharing 2020-05-02 21:46:03 +02:00
vabene1111
f00ee7d9fa display log info 2020-05-02 21:44:12 +02:00
vabene1111
6abe6f2ee4 re added mistakingly deleted file 2020-05-02 21:43:53 +02:00
vabene1111
bd69f2d103 log button in view 2020-05-02 17:55:14 +02:00
vabene1111
6a963c26b2 recipe rating 2020-05-02 17:31:35 +02:00
vabene1111
4c08ade3ee fixed markdown bleach renderer again 2020-05-02 15:10:15 +02:00
vabene1111
37f7326f4c minor mealplan cleanups 2020-05-02 14:58:23 +02:00
vabene1111
c398fda15c Merge branch 'feature/plan-title' into develop 2020-05-02 14:53:26 +02:00
vabene1111
e9da17151a added title field and custom validation 2020-05-02 14:53:09 +02:00
vabene1111
fd4354f16d Merge pull request #57 from tourn/mealplan-recipes-optional
Allow mealplan items to have no recipes
2020-05-02 14:44:46 +02:00
vabene1111
0d0c6c9066 Merge branch 'feature/plan-title' into mealplan-recipes-optional 2020-05-02 14:44:15 +02:00
vabene1111
4620c78f5a user preference fixes and improvements 2020-05-02 14:41:54 +02:00
vabene1111
349b9629f8 added sharing to recipe books 2020-05-02 14:15:56 +02:00
vabene1111
64ee18c4d8 improved recipe book design 2020-05-02 13:58:42 +02:00
vabene1111
3a9e5a80ba final style touches + settings 2020-05-02 12:48:22 +02:00
vabene1111
de85a6b334 further search style improvements 2020-05-02 12:07:03 +02:00
vabene1111
25318b691d new search design improvements 2020-05-02 01:04:45 +02:00
vabene1111
77e778caac new search design basics + Boostrap fixes 2020-05-02 00:49:29 +02:00
vabene1111
b53f83a76c improved stats page 2020-04-29 17:18:12 +02:00
vabene1111
2304c43a60 fixed ingredient calculator rounding error 2020-04-29 16:21:45 +02:00
vabene1111
16963c17dc add default roles to existing users 2020-04-27 18:22:29 +02:00
vabene1111
1d9dc0f952 api permission tests 2020-04-27 17:57:43 +02:00
vabene1111
a9fe821067 added test for comments 2020-04-27 17:48:11 +02:00
vabene1111
c7b1b08516 updated tests 2020-04-27 17:13:43 +02:00
vabene1111
1617fa7a3f fixed permissions comments, books 2020-04-27 16:50:05 +02:00
vabene1111
ad467fae28 added basic group permission system 2020-04-26 17:21:44 +02:00
vabene1111
c7046bc705 fixed markdown urlize 2020-04-26 15:52:07 +02:00
vabene1111
52946a8e4c fixed broken emoji 2020-04-26 00:29:56 +02:00
vabene1111
dd6b77e029 added screenshots + refactor preview + moved docu 2020-04-26 00:28:14 +02:00
vabene1111
396c1f3d5f added tooltips to recipe view 2020-04-25 23:35:01 +02:00
vabene1111
379d5a5177 import export cleanup + features 2020-04-25 23:32:15 +02:00
vabene1111
85a4d5d432 basic import export working 2020-04-25 22:26:59 +02:00
vabene1111
43eb10e488 added basic exporting capability 2020-04-25 22:05:55 +02:00
vabene1111
d702c08a12 fixed urlize breaking markdown links 2020-04-25 10:46:27 +02:00
vabene1111
e78323d214 Update docker-publish-latest.yml 2020-04-15 15:05:36 +02:00
vabene1111
d2e866dd74 cleanup/refactor workflows 2020-04-15 14:48:18 +02:00
vabene1111
76687ad5df testing multi platform deployment 2020-04-15 12:34:58 +02:00
vabene1111
dab77e8e4f imrpoved index redirect + fixed tests 2020-04-13 23:11:33 +02:00
vabene1111
0b250c71aa actually fixed action yml 2020-04-13 22:55:50 +02:00
vabene1111
571f670db0 fixed action intendations 2020-04-13 22:54:17 +02:00
vabene1111
4e9e628162 added ability to change default page 2020-04-13 22:52:02 +02:00
vabene1111
4f49b06704 user setting default ingredient unit 2020-04-13 22:37:50 +02:00
vabene1111
8eb0c36665 fixed action invalid yaml 2020-04-13 21:53:36 +02:00
vabene1111
6f69c09aca Merge pull request #55 from hakoerber/kubernetes-manifests
Kubernetes manifests
2020-04-13 21:51:58 +02:00
vabene1111
8e6f153882 Merge pull request #56 from tourn/clickable-links
Make links in recipe clickable
2020-04-13 21:50:25 +02:00
vabene1111
07183fd40f actions testing 2020-04-13 21:39:20 +02:00
tourn
08cccfa133 Allow mealplan items to have no recipes
And display first line of notes in plan
2020-04-13 21:29:22 +02:00
tourn
04b7f0a398 Make links in recipe clickable 2020-04-13 21:27:22 +02:00
Hannes Körber
1735fda48f Add basic kubernetes manifest
Closes #50
2020-04-13 19:39:41 +02:00
Hannes Körber
1c9ea0eda7 Use relative links in README
See https://github.blog/2013-01-31-relative-links-in-markup-files/ for
more details.
2020-04-13 19:39:39 +02:00
vabene1111
83b5b6695c Update docker-release-publish.yml 2020-04-13 18:25:49 +02:00
vabene1111
342fb3c96d Update docker-release-publish.yml 2020-04-13 18:23:19 +02:00
vabene1111
b7a18466b5 testing multi plattform builds 2020-04-13 18:17:53 +02:00
vabene1111
0cdc4d51df Merge branch 'feature/webdav-root-option' into develop 2020-04-13 17:41:02 +02:00
vabene1111
e177669514 Merge pull request #51 from pataya23/develop
added 'webdav_root' option
2020-04-13 17:38:19 +02:00
pataya23
fd294dfcdd added 'webdav_root' option
otherwise I get an error (webdav3.exceptions.RemoteResourceNotFound: Remote resource: </path> not found)
2020-04-13 13:19:03 +02:00
vabene1111
bdd092e5d3 fixed ingredient amount numeric 2020-04-07 20:13:20 +02:00
vabene1111
f1c5a0ef5f character limit increased 2020-04-07 18:46:20 +02:00
vabene1111
1e8ff763d5 increased character limit keyword icon
thanks cazier for pointing out this issue and providing a PR https://github.com/vabene1111/recipes/pull/46
2020-04-07 18:46:09 +02:00
vabene1111
4cf6a3b219 Merge pull request #45 from tourn/improved-meal-plan
Add buttons to add a meal plan to a specific point in time
2020-04-05 14:47:55 +02:00
tourn
de145b6b18 Add some styling 2020-04-05 11:10:39 +02:00
vabene1111
84a8308bf3 updated tabulator js to 4.6 2020-03-31 01:20:49 +02:00
vabene1111
8d191fa1a1 set recipe name as page title 2020-03-31 01:11:14 +02:00
vabene1111
b47a0197e2 imroved recipe printing and view 2020-03-31 01:10:30 +02:00
tourn
4e7c5f9495 Add buttons to add a meal plan to a specific point in time 2020-03-30 22:06:17 +02:00
vabene1111
d704ddacdd fixed shopping list format 2020-03-27 21:47:00 +01:00
vabene1111
2c3140248c fixed broken links 2020-03-27 21:41:55 +01:00
vabene1111
6d5ea31f8e re added mistakingly removed command 2020-03-26 18:55:35 +01:00
vabene1111
b538761746 run container as root for now
since i want to realease this we will for now continue to run this as root inside the containerr. this can be fixed later, PR's welcome
2020-03-26 18:20:44 +01:00
vabene1111
913d858473 updated boot script to fix permission 2020-03-25 22:30:44 +01:00
vabene1111
574d088cdd Create docker-publish.yml 2020-03-24 18:04:52 +01:00
vabene1111
ed360ca1c7 updated readme 2020-03-24 17:37:16 +01:00
vabene1111
08848da4a3 Merge branch 'feature/docker-rewrite' into develop 2020-03-24 17:23:04 +01:00
vabene1111
3f1f63d7e0 remove image tag as it is default 2020-03-24 17:22:51 +01:00
vabene1111
3bd6557e59 use stable image 2020-03-24 17:18:27 +01:00
vabene1111
23cb98f631 Create docker-release-publish.yml 2020-03-24 17:16:24 +01:00
vabene1111
6e91c30245 fixed tests 2020-03-24 17:03:52 +01:00
vabene1111
d7e0fa821b updated examples 2020-03-24 16:44:19 +01:00
vabene1111
c67342df26 wip changes 2020-03-24 12:57:45 +01:00
vabene1111
e3b71d47f4 Merge pull request #39 from h4llow3En/develop
Run as alpine docker image and server static files with gunicorn
2020-03-23 20:03:09 +01:00
h4llow3En
1e3e03e4af Simplify first user creation 2020-03-20 10:50:34 +01:00
h4llow3En
391ab5ddac Don't use "latest" images 2020-03-20 10:02:59 +01:00
h4llow3En
e0c560c2d7 Remove debug output from Dockerfile 2020-03-20 09:28:33 +01:00
h4llow3En
be942bcb79 Fix "Update" description in readme 2020-03-19 18:02:28 +01:00
h4llow3En
6b27f0c8ab Cleanup and simplify deployment 2020-03-19 15:08:53 +01:00
h4llow3En
cc931189e8 Run as alpine docker image and server static files with gunicorn 2020-03-19 10:13:49 +01:00
vabene1111
1b45121385 case insenstitive import 2020-03-18 16:58:27 +01:00
vabene1111
97e2593f72 fixed single import 2020-03-18 16:50:28 +01:00
vabene1111
00539b9d1b fixed theme switching 2020-03-18 13:08:31 +01:00
vabene1111
1cadb1e85e added password change form 2020-03-18 13:06:39 +01:00
vabene1111
9e524a8f22 added user name change 2020-03-18 12:51:13 +01:00
vabene1111
a8a7d4e0f4 complete bottom border "hack" 2020-03-18 12:30:35 +01:00
vabene1111
d0cf396f68 ingredient mobile friendly 2020-03-18 12:29:40 +01:00
vabene1111
e45f3f3343 updated translations 2020-03-18 12:20:45 +01:00
vabene1111
13ea2ecd7d display ingredient note 2020-03-18 12:12:03 +01:00
vabene1111
25ba62e87c improved ingredient editing 2020-03-18 12:11:15 +01:00
vabene1111
48107b918d added ingredient list page 2020-03-18 11:36:39 +01:00
vabene1111
0b56e22af9 nav improvements 2020-03-18 11:36:24 +01:00
vabene1111
47128fbb79 properly align nav icon vertically 2020-03-18 11:19:17 +01:00
vabene1111
12f6aa6df7 changed docker base image for python 3.8 2020-03-17 23:55:16 +01:00
vabene1111
c2dc038ac9 note optional 2020-03-17 22:49:53 +01:00
vabene1111
0c2b3d2d03 added ingredient notes + removed null constraints 2020-03-17 22:47:17 +01:00
vabene1111
1d562452df changed behavior of delete original 2020-03-17 18:54:44 +01:00
vabene1111
4c90664aa2 fixed translation mistaek 2020-03-17 18:47:24 +01:00
vabene1111
90dbc36402 added ability to link recipes to ingredients 2020-03-17 18:44:11 +01:00
vabene1111
a60b09e491 fixed confirm message on unit/ingredeitn merge 2020-03-17 18:28:53 +01:00
vabene1111
deeda425a8 added tests for storage edit 2020-03-17 18:22:13 +01:00
vabene1111
6fcbc9f0cd some basic testing for external recipe edits 2020-03-17 17:48:23 +01:00
vabene1111
7518d8c6b1 fixed several rewrite issues 2020-03-17 17:36:05 +01:00
vabene1111
eb25a9163f fixed import log badge 2020-03-17 17:31:26 +01:00
vabene1111
e2f6e07e42 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2020-03-17 17:23:45 +01:00
vabene1111
17b9519fa9 refactor generic url creation 2020-03-17 17:23:39 +01:00
vabene1111
adcef1d887 Update main.yml 2020-03-17 16:41:21 +01:00
vabene1111
7398304d16 Update main.yml 2020-03-17 16:40:59 +01:00
vabene1111
09ff7e82f1 django admin cleanup 2020-03-17 16:16:04 +01:00
vabene1111
47072763ee cleand up search ui 2020-03-17 15:34:17 +01:00
vabene1111
86f2c9d89c cleand up and fixed ingredient edit table 2020-03-16 23:08:52 +01:00
vabene1111
c8eaa2a349 removed js test code 2020-03-16 22:50:52 +01:00
vabene1111
9853cecabb fixed keywords without icons 2020-03-16 14:48:04 +01:00
vabene1111
7b65252d47 documentation 2020-03-12 21:48:09 +01:00
vabene1111
f62ec51c91 ignore compose 2020-03-12 21:03:34 +01:00
vabene1111
9f8b93732f removed root docker compose 2020-03-12 20:57:27 +01:00
vabene1111
bf07fc7437 restructure 2020-03-12 20:48:52 +01:00
vabene1111
08837032ce ignore docker compose 2020-03-12 20:38:48 +01:00
vabene1111
abd655ce62 moved delete to seperate file 2020-03-01 12:34:59 +01:00
vabene1111
e755068a31 added test for png's 2020-03-01 12:13:36 +01:00
vabene1111
b5e35115fa fixed png's not working in recipes 2020-03-01 12:13:06 +01:00
vabene1111
c8cc140a78 improved recipe edit tests and fixed bugs 2020-02-29 00:00:13 +01:00
vabene1111
8c7a171d56 added more tests and test structure 2020-02-28 23:17:04 +01:00
vabene1111
df62717806 removed non internal recipes from shopping 2020-02-28 22:21:03 +01:00
vabene1111
a1e6bd5441 fixed text outside of colum 2020-02-28 22:12:18 +01:00
vabene1111
034e2c612b added internal recipe filter 2020-02-28 22:12:02 +01:00
vabene1111
45c85b9de8 fixed markdown blockquotes not really rendering 2020-02-28 22:09:16 +01:00
vabene1111
a9952b8f57 improved markdown rendering of tables and images 2020-02-28 21:53:27 +01:00
439 changed files with 133627 additions and 1729 deletions

View File

@@ -7,4 +7,12 @@ docker-compose*
.gitignore
README.md
LICENSE
.vscode
.vscode
.env
.env.template
.github
.idea
LICENSE.md
docs
nginx
update.sh

View File

@@ -1,14 +1,35 @@
VIRTUAL_HOST=
LETSENCRYPT_HOST=
LETSENCRYPT_EMAIL=
# only set this to true when testing/debugging
# when unset: 1 (true) - dont unset this, just for development
DEBUG=0
DEBUG=1
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
ALLOWED_HOSTS=*
# random secret key, use for example base64 /dev/urandom | head -c50 to generate one
SECRET_KEY=
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
DB_ENGINE=django.db.backends.postgresql_psycopg2
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangodb
POSTGRES_PASSWORD=
POSTGRES_DB=djangodb
POSTGRES_DB=djangodb
# Serve mediafiles directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
# provided that include an additional nxginx container to handle media file serving.
# If you know what you are doing turn this back on (1) to serve media files using djangos serve() method.
# when unset: 1 (true) - this is temporary until an appropriate amount of time has passed for everyone to migrate
GUNICORN_MEDIA=0
# allow authentication via reverse proxy (e.g. authelia), leave of if you dont know what you are doing
# docs: https://github.com/vabene1111/recipes/tree/develop/docs/docker/nginx-proxy%20with%20proxy%20authentication
# when unset: 0 (false)
REVERSE_PROXY_AUTH=0
# the default value for the user preference 'comments' (enable/disable commenting system)
# when unset: 1 (true)
COMMENT_PREF_DEFAULT=1

15
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,15 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Bug description
A clear and concise description of what the bug is.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

27
.github/ISSUE_TEMPLATE/help-request.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Help request
about: If there is anything wrong with your setup
title: ''
labels: setup issue
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Issue
Please describe your problem here
### `.env`
Please include your `.env` config file (**make sure to remove/replace all secrets**)
```
env content
```
### `docker-compose.yml`
When running with docker compose please provide your `docker-compose.yml`
```
docker-compose.yml content
```

View File

@@ -9,18 +9,19 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.7]
python-version: [3.8]
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
python3 manage.py collectstatic --noinput
- name: Django Testing project
run: |
python3 manage.py test

51
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 13 * * 2'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -0,0 +1,26 @@
name: publish dev image docker
on:
push:
branches:
- '*'
- '*/*'
- '!master'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = 'develop'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@2.13
with:
name: vabene1111/recipes
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -0,0 +1,30 @@
name: publish latest image docker
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: latest
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -0,0 +1,33 @@
name: publish tagged release docker
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master#
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

22
.gitignore vendored
View File

@@ -69,29 +69,11 @@ mediafiles/
\.idea/misc\.xml
\.idea/recipes\.iml
# Deployment
\.env
staticfiles/
postgresql/
postgresql/
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
/docker-compose.override.yml

View File

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

10
.idea/dictionaries/vabene1111_PC.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<component name="ProjectDictionaryState">
<dictionary name="vabene1111-PC">
<words>
<w>csrftoken</w>
<w>gunicorn</w>
<w>ical</w>
<w>traefik</w>
</words>
</dictionary>
</component>

View File

@@ -1,14 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="psycopg2" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -1,6 +1,5 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{jquery-3.4.1, pdf, pdf_viewer, pretty-checkbox}" />
</component>
</project>

36
.idea/recipes.iml generated Normal file
View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="recipes/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/cookbook/tests/resources" />
<excludeFolder url="file://$MODULE_DIR$/staticfiles" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (recipes)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jquery-3.4.1" level="application" />
<orderEntry type="library" name="pretty-checkbox" level="application" />
<orderEntry type="library" name="pdf" level="application" />
<orderEntry type="library" name="pdf_viewer" level="application" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/cookbook/templates" />
</list>
</option>
</component>
</module>

View File

@@ -1,24 +1,18 @@
FROM ubuntu:18.04
RUN mkdir /Recipes
WORKDIR /Recipes
ADD . /Recipes/
RUN apt-get update
RUN apt-get -y upgrade
RUN apt-get install -y \
python3 \
python3-pip \
postgresql-client \
gettext
RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt
RUN apt-get autoremove -y
FROM python:3.8-alpine
RUN apk add --no-cache postgresql-libs gettext zlib libjpeg libxml2-dev libxslt-dev
ENV PYTHONUNBUFFERED 1
EXPOSE 8080
EXPOSE 8080
RUN mkdir /opt/recipes
WORKDIR /opt/recipes
COPY . ./
RUN chmod +x boot.sh setup.sh
RUN ln -s /opt/recipes/setup.sh /usr/local/bin/createsuperuser
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev && \
python -m venv venv && \
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@@ -1,22 +1,690 @@
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
Software: https://github.com/vabene1111/recipes
License: GNU AFFERO GENERAL PUBLIC LICENSE v3
Licensor: https://github.com/vabene1111
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
---
### Prior 0.10.0
> All versions released before version 0.10. or 29.06.2020 were licensed under the following license
The MIT License (MIT)
Copyright (c) 2018
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,7 +1,9 @@
# Recipes ![Continous Integration](https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop)
# Recipes ![CI](https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop)
Recipes is a Django application to manage, tag and search recipes using either built in models or external storage providers hosting PDF's, Images or other files.
![Preview](preview.png)
![Preview](docs/preview.png)
[More Screenshots](https://imgur.com/a/V01151p)
### Features
@@ -9,66 +11,71 @@ Recipes is a Django application to manage, tag and search recipes using either b
- :mag: Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- :label: Create and search for **tags**, assign them in batch to all files matching certain filters
- :page_facing_up: **Create recipes** locally within a nice, standardized web interface
- :arrow_down: **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
- :iphone: Optimized for use on **mobile** devices like phones and tablets
- :shopping_cart: Generate **shopping** lists from recipes
- :calendar: Create a **Plan** on what to eat when
- :person_with_blond_hair: **Share** recipes with friends and comment on them to suggest or remember changes you made
- :family: **Share** recipes with friends and comment on them to suggest or remember changes you made
- :whale: Easy setup with **Docker**
- :art: Customize your interface with **themes**
- :envelope: Export and import recipes from other users
- :heavy_plus_sign: Many more like recipe scaling, image compression, cookbooks, printing views, ...
This application is meant for people with a collection of recipes they want to share with family and friends or simply store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as a public page.
This application is meant for people with a collection of recipes they want to share with family and friends or simply
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as a public page.
Some Documentation can be found [here](https://github.com/vabene1111/recipes/wiki)
# Installation
# Documentation
Most things should be straight forward but there are some more complicated things.
##### Storage Backends
A `Storage Backend` is a remote storage location where files are stored. To add a new backend click on `Storage Data` and then on `Storage Backends`. There click the plus button.
Enter a name (just a display name for you to identify it) and an API access Token for the account you want to use.
Dropboxes API tokens can be found on the [Dropboxes API explorer](https://dropbox.github.io/dropbox-api-v2-explorer/#auth_token/from_oauth1)
with the button on the top right. For Nextcloud you can use a App apssword created in the settings.
##### Adding Synced Paths
To add a new path from your Storage backend to the sync list, go to `Storage Data >> Configure Sync` and select the storage backend you want to use.
Then enter the path you want to monitor starting at the storage root (e.g. `/Folder/RecipesFolder`) and save it.
##### Syncing Data
To sync the recipes app with the storage backends press `Sync now` under `Storage Data >> Configure Sync`.
##### Import Recipes
All files found by the sync can be found under `Manage Data >> Import recipes`. There you can either import all at once without modifying them or import one by one, adding tags while importing.
##### Batch Edit
If you have many untagged recipes, you may want to edit them all at once. To do so, go to
`Storage Data >> Batch Edit`. Enter a word which should be contained in the recipe name and select the tags you want to apply.
When clicking submit, every recipe containing the word will be updated (tags are added).
> Currently the only option is word contains, maybe some more SQL like operators will be added later.
## Installation
The docker image (`vabene1111/recipes`) simply exposes the application on port `8080`. You may choose any preferred installation method, the following are just examples to make it easier.
### Docker-Compose
When cloning this repository, a simple docker-compose file is included. It is made for setups already running an nginx-reverse proxy network with lets encrypt companion but can be changed easily. Copy `.env.template` to `.env` and fill in the missing values accordingly.
Now simply start the containers and run the `update.sh` script that will apply all migrations and collect static files.
Create a default user by executing into the container with `docker-compose exec web_recipes sh` and run `python3 manage.py createsuperuser`.
2. Choose one of the included configurations [here](docs/docker).
2. Download the environment (config) file template and fill it out `wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env `
3. Start the container `docker-compose up -d`
4. Open the page to create the first user. Alternatively use `docker-compose exec web_recipes createsuperuser`
### Manual
**Python >= 3.8** is required to run this!
Copy `.env.template` to `.env` and fill in the missing values accordingly.
You can leave out the docker specific variables (VIRTUAL_HOST, LETSENCRYPT_HOST, LETSENCRYPT_EMAIL).
Make sure all variables are available to whatever serves your application.
Otherwise simply follow the instructions for any django based deployment
(for example this one http://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html).
(for example [this one](http://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html)).
To start developing:
1. Clone the repository using your preferred method
2. Install requirements from `requirements.txt` either globally or in a virtual environment
3. Run migrations with `manage.py migrate`
4. Create a first user with `manage.py createsuperuser`
5. Start development server with `manage.py runserver`
## Updating
While intermediate updates can be skipped when updating please make sure to **read the release notes** in case some special action is required to update.
0. Before updating it is recommended to **create a backup!**
1. Stop the container using `docker-compose down`
2. Pull the latest image using `docker-compose pull`
3. Start the container again using `docker-compose up -d`
## Kubernetes
You can find a basic kubernetes setup [here](docs/k8s/). Please see the README in the folder for more detail.
## Contributing
Pull Requests and ideas are welcome, feel free to contribute in any way.
For any questions on how to work with django please refer to their excellent [documentation](https://www.djangoproject.com/start/).
### Translating
There is a [transifex project](https://www.transifex.com/django-recipes/django-cookbook/) project to enable community driven translations. If you want to contribute a new language or help maintain an already existing one feel free to create a transifex account (using the link above) and request to join the project.
It is also possible to provide the translations directly by creating a new language using `manage.py makemessages -l <language_code> -i venv`. Once finished simply open a PR with the changed files.
## License
This project is licensed under the MIT license. Even though it is not required to publish derivatives, I highly encourage pushing changes upstream and letting people profit from any work done on this project.
Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with an
[common clause](https://commonsclause.com/) selling exception. See [LICENSE.md](https://github.com/vabene1111/recipes/blob/develop/LICENSE.md) for details.
**Reasoning**
#### This software and **all** its features are and will always be free for everyone to use and enjoy.
The reason for the selling exception is that a significant amount of time was spend over multiple years to develop this software.
A payed hosted version which will be identical in features and code base to the software offered in this repository will
likely be released in the future (including all features needed to sell a hosted version as they might also be useful for personal use).
This will not only benefit me personally but also everyone who self-hosts this software as any profits made trough selling the hosted option
allow me to spend more time developing and improving the software for everyone. Selling exceptions are [approved by Richard Stallman](http://www.gnu.org/philosophy/selling-exceptions.en.html) and the
common clause license is very permissive (see the [FAQ](https://commonsclause.com/)).

11
boot.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
source venv/bin/activate
echo "Updating database"
python manage.py migrate
python manage.py collectstatic --noinput
echo "Done"
chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :8080 --access-logfile - --error-logfile - --log-level INFO recipes.wsgi

View File

@@ -2,10 +2,138 @@ from django.contrib import admin
from .models import *
admin.site.register(Recipe)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style', 'comments')
@staticmethod
def name(obj):
return obj.user.get_user_name()
admin.site.register(UserPreference, UserPreferenceAdmin)
class StorageAdmin(admin.ModelAdmin):
list_display = ('name', 'method')
admin.site.register(Storage, StorageAdmin)
class SyncAdmin(admin.ModelAdmin):
list_display = ('storage', 'path', 'active', 'last_checked')
admin.site.register(Sync, SyncAdmin)
class SyncLogAdmin(admin.ModelAdmin):
list_display = ('sync', 'status', 'msg', 'created_at')
admin.site.register(SyncLog, SyncLogAdmin)
admin.site.register(Keyword)
admin.site.register(Sync)
admin.site.register(SyncLog)
admin.site.register(RecipeImport)
admin.site.register(Storage)
class StepAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'order')
admin.site.register(Step, StepAdmin)
class RecipeAdmin(admin.ModelAdmin):
list_display = ('name', 'internal', 'created_by', 'storage')
@staticmethod
def created_by(obj):
return obj.created_by.get_user_name()
admin.site.register(Recipe, RecipeAdmin)
admin.site.register(Unit)
admin.site.register(Food)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('food', 'amount', 'unit')
admin.site.register(Ingredient, IngredientAdmin)
class CommentAdmin(admin.ModelAdmin):
list_display = ('recipe', 'name', 'created_at')
@staticmethod
def name(obj):
return obj.created_by.get_user_name()
admin.site.register(Comment, CommentAdmin)
class RecipeImportAdmin(admin.ModelAdmin):
list_display = ('name', 'storage', 'file_path')
admin.site.register(RecipeImport, RecipeImportAdmin)
class RecipeBookAdmin(admin.ModelAdmin):
list_display = ('name', 'user_name')
@staticmethod
def user_name(obj):
return obj.created_by.get_user_name()
admin.site.register(RecipeBook, RecipeBookAdmin)
class RecipeBookEntryAdmin(admin.ModelAdmin):
list_display = ('book', 'recipe')
admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin)
class MealPlanAdmin(admin.ModelAdmin):
list_display = ('user', 'recipe', 'meal_type', 'date')
@staticmethod
def user(obj):
return obj.created_by.get_user_name()
admin.site.register(MealPlan, MealPlanAdmin)
class MealTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'order')
admin.site.register(MealType, MealTypeAdmin)
class ViewLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at')
admin.site.register(ViewLog, ViewLogAdmin)
class CookLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
admin.site.register(CookLog, CookLogAdmin)
class ShareLinkAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'uuid', 'created_at',)
admin.site.register(ShareLink, ShareLinkAdmin)

View File

@@ -2,7 +2,7 @@ import django_filters
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from cookbook.forms import MultiSelectWidget
from cookbook.models import Recipe, Keyword, Ingredient
from cookbook.models import Recipe, Keyword, Food
from django.conf import settings
from django.utils.translation import gettext as _
@@ -11,8 +11,8 @@ class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
method='filter_keywords')
ingredients = django_filters.ModelMultipleChoiceFilter(queryset=Ingredient.objects.all(), widget=MultiSelectWidget,
method='filter_ingredients', label=_('Ingredients'))
foods = django_filters.ModelMultipleChoiceFilter(queryset=Food.objects.all(), widget=MultiSelectWidget,
method='filter_foods', label=_('Ingredients'))
@staticmethod
def filter_keywords(queryset, name, value):
@@ -23,11 +23,11 @@ class RecipeFilter(django_filters.FilterSet):
return queryset
@staticmethod
def filter_ingredients(queryset, name, value):
if not name == 'ingredients':
def filter_foods(queryset, name, value):
if not name == 'foods':
return queryset
for x in value:
queryset = queryset.filter(recipeingredient__ingredient=x).distinct()
queryset = queryset.filter(steps__ingredients__food__name=x).distinct()
return queryset
@staticmethod
@@ -43,4 +43,12 @@ class RecipeFilter(django_filters.FilterSet):
class Meta:
model = Recipe
fields = ['name', 'keywords', 'ingredients']
fields = ['name', 'keywords', 'foods', 'internal']
class IngredientFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Food
fields = ['name']

View File

@@ -1,6 +1,6 @@
from dal import autocomplete
from django import forms
from django.forms import widgets
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from emoji_picker.widgets import EmojiPickerTextInput
@@ -27,12 +27,35 @@ class DateWidget(forms.DateInput):
class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'
class Meta:
model = UserPreference
fields = ('theme', 'nav_color')
fields = ('default_unit', 'theme', 'nav_color', 'default_page', 'show_recent', 'search_style', 'plan_share', 'ingredient_decimals', 'comments')
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!')
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'plan_share': _('Default user to share newly created meal plan entries with.'),
'show_recent': _('Show recently viewed recipes on search page.'),
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.')
}
widgets = {
'plan_share': MultiSelectWidget
}
class UserNameForm(forms.ModelForm):
prefix = 'name'
class Meta:
model = User
fields = ('first_name', 'last_name')
help_texts = {
'first_name': _('Both fields are optional. If none are given the username will be displayed instead')
}
@@ -61,12 +84,11 @@ class InternalRecipeForm(forms.ModelForm):
class Meta:
model = Recipe
fields = ('name', 'instructions', 'image', 'working_time', 'waiting_time', 'keywords')
fields = ('name', 'image', 'working_time', 'waiting_time', 'keywords')
labels = {
'name': _('Name'),
'keywords': _('Keywords'),
'instructions': _('Instructions'),
'working_time': _('Preparation time in minutes'),
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
}
@@ -75,7 +97,7 @@ class InternalRecipeForm(forms.ModelForm):
class ShoppingForm(forms.Form):
recipe = forms.ModelMultipleChoiceField(
queryset=Recipe.objects.all(),
queryset=Recipe.objects.filter(internal=True).all(),
widget=MultiSelectWidget
)
markdown_format = forms.BooleanField(
@@ -85,6 +107,25 @@ class ShoppingForm(forms.Form):
)
class ExportForm(forms.Form):
recipe = forms.ModelChoiceField(
queryset=Recipe.objects.filter(internal=True).all(),
widget=SelectWidget
)
image = forms.BooleanField(
help_text=_('Export Base64 encoded image?'),
required=False
)
download = forms.BooleanField(
help_text=_('Download export directly or show on page?'),
required=False
)
class ImportForm(forms.Form):
recipe = forms.CharField(widget=forms.Textarea, help_text=_('Simply paste a JSON export into this textarea and click import.'))
class UnitMergeForm(forms.Form):
prefix = 'unit'
@@ -102,20 +143,20 @@ class UnitMergeForm(forms.Form):
)
class IngredientMergeForm(forms.Form):
prefix = 'ingredient'
class FoodMergeForm(forms.Form):
prefix = 'food'
new_ingredient = forms.ModelChoiceField(
queryset=Ingredient.objects.all(),
new_food = forms.ModelChoiceField(
queryset=Food.objects.all(),
widget=SelectWidget,
label=_('New Ingredient'),
help_text=_('New ingredient that other gets replaced by.'),
label=_('New Food'),
help_text=_('New food that other gets replaced by.'),
)
old_ingredient = forms.ModelChoiceField(
queryset=Ingredient.objects.all(),
old_food = forms.ModelChoiceField(
queryset=Food.objects.all(),
widget=SelectWidget,
label=_('Old Ingredient'),
help_text=_('Ingredient that should be replaced.'),
label=_('Old Food'),
help_text=_('Food that should be replaced.'),
)
@@ -141,6 +182,13 @@ class KeywordForm(forms.ModelForm):
widgets = {'icon': EmojiPickerTextInput}
class FoodForm(forms.ModelForm):
class Meta:
model = Food
fields = ('name', 'recipe')
widgets = {'recipe': SelectWidget}
class StorageForm(forms.ModelForm):
username = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password'}), required=False)
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
@@ -197,12 +245,33 @@ class ImportRecipeForm(forms.ModelForm):
class RecipeBookForm(forms.ModelForm):
class Meta:
model = RecipeBook
fields = ('name',)
fields = ('name', 'icon', 'description', 'shared')
widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget}
class MealPlanForm(forms.ModelForm):
def clean(self):
cleaned_data = super(MealPlanForm, self).clean()
if cleaned_data['title'] == '' and cleaned_data['recipe'] is None:
raise forms.ValidationError(_('You must provide at least a recipe or a title.'))
return cleaned_data
class Meta:
model = MealPlan
fields = ('recipe', 'meal', 'note', 'date')
fields = ('recipe', 'title', 'meal_type', 'note', 'date', 'shared')
widgets = {'recipe': SelectWidget, 'date': DateWidget}
help_texts = {
'shared': _('You can list default users to share recipes with in the settings.'),
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
}
widgets = {'recipe': SelectWidget, 'date': DateWidget, 'shared': MultiSelectWidget}
class SuperUserForm(forms.Form):
name = forms.CharField()
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))
password_confirm = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))

View File

@@ -1,6 +1,6 @@
from dal import autocomplete
from cookbook.models import Keyword, RecipeIngredient, Recipe, Unit, Ingredient
from cookbook.models import Keyword, Recipe, Unit, Food
class KeywordAutocomplete(autocomplete.Select2QuerySetView):
@@ -19,9 +19,9 @@ class KeywordAutocomplete(autocomplete.Select2QuerySetView):
class IngredientsAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return Ingredient.objects.none()
return Food.objects.none()
qs = Ingredient.objects.all()
qs = Food.objects.all()
if self.q:
qs = qs.filter(name__icontains=self.q)

View File

@@ -0,0 +1,24 @@
import markdown
from markdown.treeprocessors import Treeprocessor
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 == "img":
child.set("class", "img-fluid")
self.run_processor(child)
return node
def run(self, root):
self.run_processor(root)
return root
class MarkdownFormatExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
md.treeprocessors.register(StyleTreeprocessor(), 'StyleTreeprocessor', 10)

View File

@@ -0,0 +1,81 @@
"""A more liberal autolinker
Inspired by Django's urlize function.
Positive examples:
>>> import markdown
>>> md = markdown.Markdown(extensions=['urlize'])
>>> md.convert('http://example.com/')
u'<p><a href="http://example.com/">http://example.com/</a></p>'
>>> md.convert('go to http://example.com')
u'<p>go to <a href="http://example.com">http://example.com</a></p>'
>>> md.convert('example.com')
u'<p><a href="http://example.com">example.com</a></p>'
>>> md.convert('example.net')
u'<p><a href="http://example.net">example.net</a></p>'
>>> md.convert('www.example.us')
u'<p><a href="http://www.example.us">www.example.us</a></p>'
>>> md.convert('(www.example.us/path/?name=val)')
u'<p>(<a href="http://www.example.us/path/?name=val">www.example.us/path/?name=val</a>)</p>'
>>> md.convert('go to <http://example.com> now!')
u'<p>go to <a href="http://example.com">http://example.com</a> now!</p>'
Negative examples:
>>> md.convert('del.icio.us')
u'<p>del.icio.us</p>'
"""
import markdown
# Global Vars
URLIZE_RE = '(%s)' % '|'.join([
r'<(?:f|ht)tps?://[^>]*>',
r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
r'\bwww\.[^)<>\s]+[^.,)<>\s]',
r'[^(<\s]+\.(?:com|net|org)\b',
])
class UrlizePattern(markdown.inlinepatterns.Pattern):
""" Return a link Element given an autolink (`http://example/com`). """
def handleMatch(self, m):
url = m.group(2)
if url.startswith('<'):
url = url[1:-1]
text = url
if not url.split('://')[0] in ('http','https','ftp'):
if '@' in url and not '/' in url:
url = 'mailto:' + url
else:
url = 'http://' + url
el = markdown.util.etree.Element("a")
el.set('href', url)
el.text = markdown.util.AtomicString(text)
return el
class UrlizeExtension(markdown.Extension):
""" Urlize Extension for Python-Markdown. """
def extendMarkdown(self, md, md_globals):
""" Replace autolink with UrlizePattern """
md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
def makeExtension(*args, **kwargs):
return UrlizeExtension(*args, **kwargs)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@@ -0,0 +1,10 @@
# Permission Config
from cookbook.helper.permission_helper import CustomIsUser, CustomIsOwner, CustomIsAdmin, CustomIsGuest
class PermissionConfig:
BOOKS = {
'owner': True,
'groups': ['user'],
'drf': [CustomIsUser],
}

View File

@@ -0,0 +1,198 @@
"""
Source: https://djangosnippets.org/snippets/1703/
"""
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
from cookbook.models import ShareLink
# Helper Functions
def get_allowed_groups(groups_required):
"""
Builds a list of all groups equal or higher to the provided groups
This means checking for guest will also allow admins to access
:param groups_required: list or tuple of groups
:return: tuple of groups
"""
groups_allowed = tuple(groups_required)
if 'guest' in groups_required:
groups_allowed = groups_allowed + ('user', 'admin')
if 'user' in groups_required:
groups_allowed = groups_allowed + ('admin',)
return groups_allowed
def has_group_permission(user, groups):
"""
Tests if a given user is member of a certain group (or any higher group)
Superusers always bypass permission checks. Unauthenticated users cant be member of any
group thus always return false.
:param user: django auth user object
:param groups: list or tuple of groups the user should be checked for
:return: True if user is in allowed groups, false otherwise
"""
if not user.is_authenticated:
return False
groups_allowed = get_allowed_groups(groups)
if user.is_authenticated:
if user.is_superuser | bool(user.groups.filter(name__in=groups_allowed)):
return True
return False
def is_object_owner(user, obj):
"""
Tests if a given user is the owner of a given object
test performed by checking user against the objects user and create_by field (if exists)
superusers bypass all checks, unauthenticated users cannot own anything
:param user django auth user object
:param obj any object that should be tested
:return: true if user is owner of object, false otherwise
"""
# TODO this could be improved/cleaned up by adding get_owner methods to all models that allow owner checks
if not user.is_authenticated:
return False
if user.is_superuser:
return True
if owner := getattr(obj, 'created_by', None):
return owner == user
if owner := getattr(obj, 'user', None):
return owner == user
return False
def share_link_valid(recipe, share):
"""
Verifies the validity of a share uuid
:param recipe: recipe object
:param share: share uuid
:return: true if a share link with the given recipe and uuid exists, false otherwise
"""
try:
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
except ValidationError:
return False
# Django Views
def group_required(*groups_required):
"""
Decorator that tests the requesting user to be member of at least one of the provided groups
or higher level groups
:param groups_required: list of required groups
:return: true if member of group, false otherwise
"""
def in_groups(u):
return has_group_permission(u, groups_required)
return user_passes_test(in_groups, login_url='index')
class GroupRequiredMixin(object):
"""
groups_required - list of strings, required param
"""
groups_required = None
def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required):
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
class OwnerRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('login'))
else:
if not is_object_owner(request.user, self.get_object()):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
# Django Rest Framework Permission classes
class CustomIsOwner(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies user has ownership over object
(either user or created_by or user is request user)
"""
message = _('You cannot interact with this object as its not owned by you!')
def has_permission(self, request, view):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return is_object_owner(request.user, obj)
class CustomIsGuest(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: guest
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
return has_group_permission(request.user, ['guest'])
def has_object_permission(self, request, view, obj):
return has_group_permission(request.user, ['guest'])
class CustomIsUser(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: user
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
return has_group_permission(request.user, ['user'])
class CustomIsAdmin(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: admin
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
return has_group_permission(request.user, ['admin'])
class CustomIsShare(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the requesting user provided a valid share link
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
return request.method in SAFE_METHODS and 'pk' in view.kwargs
def has_object_permission(self, request, view, obj):
share = request.query_params.get('share', None)
if share:
return share_link_valid(obj, share)
return False

View File

@@ -0,0 +1,190 @@
import json
import random
import re
from json import JSONDecodeError
import microdata
from bs4 import BeautifulSoup
from django.http import JsonResponse
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from cookbook.models import Keyword
def get_from_html(html_text, url):
soup = BeautifulSoup(html_text, "html.parser")
# first try finding ld+json as its most common
for ld in soup.find_all('script', type='application/ld+json'):
try:
ld_json = json.loads(ld.string)
if type(ld_json) != list:
ld_json = [ld_json]
for ld_json_item in ld_json:
# recipes type might be wrapped in @graph type
if '@graph' in ld_json_item:
for x in ld_json_item['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
ld_json_item = x
if '@type' in ld_json_item and ld_json_item['@type'] == 'Recipe':
return find_recipe_json(ld_json_item, url)
except JSONDecodeError:
JsonResponse({'error': True, 'msg': _('The requested site provided malformed data and cannot be read.')}, status=400)
# now try to find microdata
items = microdata.get_items(html_text)
for i in items:
md_json = json.loads(i.json())
if 'schema.org/Recipe' in str(md_json['type']):
return find_recipe_json(md_json['properties'], url)
return JsonResponse({'error': True, 'msg': _('The requested site does not provide any recognized data format to import the recipe from.')}, status=400)
def find_recipe_json(ld_json, url):
if type(ld_json['name']) == list:
try:
ld_json['name'] = ld_json['name'][0]
except:
ld_json['name'] = 'ERROR'
# some sites use ingredients instead of recipeIngredients
if 'recipeIngredient' not in ld_json and 'ingredients' in ld_json:
ld_json['recipeIngredient'] = ld_json['ingredients']
if 'recipeIngredient' in ld_json:
# some pages have comma separated ingredients in a single array entry
if len(ld_json['recipeIngredient']) == 1 and len(ld_json['recipeIngredient'][0]) > 30:
ld_json['recipeIngredient'] = ld_json['recipeIngredient'][0].split(',')
for x in ld_json['recipeIngredient']:
if '\n' in x:
ld_json['recipeIngredient'].remove(x)
for i in x.split('\n'):
ld_json['recipeIngredient'].insert(0, i)
ingredients = []
for x in ld_json['recipeIngredient']:
ingredient_split = x.split()
ingredient = None
amount = 0
unit = ''
if len(ingredient_split) > 2:
ingredient = " ".join(ingredient_split[2:])
unit = ingredient_split[1]
try:
amount = float(ingredient_split[0].replace(',', '.'))
except ValueError:
amount = 0
ingredient = " ".join(ingredient_split)
if len(ingredient_split) == 2:
ingredient = " ".join(ingredient_split[1:])
unit = ''
try:
amount = float(ingredient_split[0].replace(',', '.'))
except ValueError:
amount = 0
ingredient = " ".join(ingredient_split)
if len(ingredient_split) == 1:
ingredient = " ".join(ingredient_split)
if ingredient:
ingredients.append({'amount': amount, 'unit': {'text': unit, 'id': random.randrange(10000, 99999)}, 'ingredient': {'text': ingredient, 'id': random.randrange(10000, 99999)}, 'original': x})
ld_json['recipeIngredient'] = ingredients
else:
ld_json['recipeIngredient'] = []
if 'keywords' in ld_json:
keywords = []
# keywords as string
if type(ld_json['keywords']) == str:
ld_json['keywords'] = ld_json['keywords'].split(',')
# keywords as string in list
if type(ld_json['keywords']) == list and len(ld_json['keywords']) == 1 and ',' in ld_json['keywords'][0]:
ld_json['keywords'] = ld_json['keywords'][0].split(',')
# keywords as list
for kw in ld_json['keywords']:
if k := Keyword.objects.filter(name=kw).first():
keywords.append({'id': str(k.id), 'text': str(k).strip()})
else:
keywords.append({'id': "null", 'text': kw.strip()})
ld_json['keywords'] = keywords
else:
ld_json['keywords'] = []
if 'recipeInstructions' in ld_json:
instructions = ''
# flatten instructions if they are in a list
if type(ld_json['recipeInstructions']) == list:
for i in ld_json['recipeInstructions']:
if type(i) == str:
instructions += i
else:
if 'text' in i:
instructions += i['text'] + '\n\n'
elif 'itemListElement' in i:
for ile in i['itemListElement']:
if type(ile) == str:
instructions += ile + '\n\n'
elif 'text' in ile:
instructions += ile['text'] + '\n\n'
else:
instructions += str(i)
ld_json['recipeInstructions'] = instructions
ld_json['recipeInstructions'] = re.sub(r'\n\s*\n', '\n\n', ld_json['recipeInstructions'])
ld_json['recipeInstructions'] = re.sub(' +', ' ', ld_json['recipeInstructions'])
ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('<p>', '')
ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('</p>', '')
else:
ld_json['recipeInstructions'] = ''
ld_json['recipeInstructions'] += '\n\n' + _('Imported from ') + url
if 'image' in ld_json:
# check if list of images is returned, take first if so
if (type(ld_json['image'])) == list:
if type(ld_json['image'][0]) == str:
ld_json['image'] = ld_json['image'][0]
elif 'url' in ld_json['image'][0]:
ld_json['image'] = ld_json['image'][0]['url']
# ignore relative image paths
if 'http' not in ld_json['image']:
ld_json['image'] = ''
if 'cookTime' in ld_json:
try:
if type(ld_json['cookTime']) == list and len(ld_json['cookTime']) > 0:
ld_json['cookTime'] = ld_json['cookTime'][0]
ld_json['cookTime'] = round(parse_duration(ld_json['cookTime']).seconds / 60)
except TypeError:
ld_json['cookTime'] = 0
else:
ld_json['cookTime'] = 0
if 'prepTime' in ld_json:
try:
if type(ld_json['prepTime']) == list and len(ld_json['prepTime']) > 0:
ld_json['prepTime'] = ld_json['prepTime'][0]
ld_json['prepTime'] = round(parse_duration(ld_json['prepTime']).seconds / 60)
except TypeError:
ld_json['prepTime'] = 0
else:
ld_json['prepTime'] = 0
for key in list(ld_json):
if key not in ['prepTime', 'cookTime', 'image', 'recipeInstructions', 'keywords', 'name', 'recipeIngredient']:
ld_json.pop(key, None)
return JsonResponse(ld_json)

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.4 on 2020-03-17 17:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0026_auto_20200219_1605'),
]
operations = [
migrations.AddField(
model_name='ingredient',
name='recipe',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.Recipe'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.4 on 2020-03-17 18:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0027_ingredient_recipe'),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='ingredient',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.Ingredient'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.4 on 2020-03-17 18:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0028_auto_20200317_1901'),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='unit',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.Unit'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-03-17 18:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0029_auto_20200317_1901'),
]
operations = [
migrations.AddField(
model_name='recipeingredient',
name='note',
field=models.CharField(blank=True, max_length=64, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-04-07 16:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0030_recipeingredient_note'),
]
operations = [
migrations.AlterField(
model_name='keyword',
name='icon',
field=models.CharField(blank=True, max_length=16, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-04-13 20:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0031_auto_20200407_1841'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='default_unit',
field=models.CharField(default='g', max_length=32),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-04-13 20:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0032_userpreference_default_unit'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='default_page',
field=models.CharField(choices=[('SEARCH', 'Search'), ('PLAN', 'Meal-Plan')], default='SEARCH', max_length=64),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.0.5 on 2020-04-26 14:14
from django.db import migrations
def apply_migration(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Group.objects.bulk_create([
Group(name=u'guest'),
Group(name=u'user'),
Group(name=u'admin'),
])
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0033_userpreference_default_page'),
]
operations = [
migrations.RunPython(apply_migration)
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.0.5 on 2020-04-27 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0034_auto_20200426_1614'),
]
operations = [
migrations.RenameField(
model_name='mealplan',
old_name='user',
new_name='created_by',
),
migrations.RenameField(
model_name='recipebook',
old_name='user',
new_name='created_by',
),
migrations.AlterField(
model_name='userpreference',
name='default_page',
field=models.CharField(choices=[('SEARCH', 'Search'), ('PLAN', 'Meal-Plan'), ('BOOKS', 'Books')], default='SEARCH', max_length=64),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.0.5 on 2020-04-27 16:00
from django.db import migrations
def apply_migration(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
if u.groups.count() < 1:
u.groups.add(Group.objects.get(name='admin'))
u.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0035_auto_20200427_1637'),
]
operations = [
migrations.RunPython(apply_migration)
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-05-02 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0036_auto_20200427_1800'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='search_style',
field=models.CharField(choices=[('SMALL', 'Small'), ('LARGE', 'Large')], default='LARGE', max_length=64),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.5 on 2020-05-02 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0037_userpreference_search_style'),
]
operations = [
migrations.AddField(
model_name='recipebook',
name='description',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='recipebook',
name='icon',
field=models.CharField(blank=True, max_length=16, null=True),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-05-02 12:04
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0038_auto_20200502_1259'),
]
operations = [
migrations.AddField(
model_name='recipebook',
name='shared',
field=models.ManyToManyField(blank=True, related_name='shared_with', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.0.5 on 2020-05-02 12:33
import annoying.fields
from django.conf import settings
from django.db import migrations
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0039_recipebook_shared'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='user',
field=annoying.fields.AutoOneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.5 on 2020-05-02 12:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0040_auto_20200502_1433'),
]
operations = [
migrations.AddField(
model_name='mealplan',
name='title',
field=models.CharField(blank=True, default='', max_length=64),
),
migrations.AlterField(
model_name='mealplan',
name='recipe',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe'),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.5 on 2020-05-02 14:47
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', '0041_auto_20200502_1446'),
]
operations = [
migrations.CreateModel(
name='CookLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('rating', models.IntegerField(null=True)),
('servings', models.IntegerField(default=0)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
],
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.5 on 2020-05-07 21:02
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0042_cooklog'),
]
operations = [
migrations.AddField(
model_name='mealplan',
name='shared',
field=models.ManyToManyField(blank=True, related_name='plan_share', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='userpreference',
name='plan_share',
field=models.ManyToManyField(blank=True, related_name='plan_share_default', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.5 on 2020-05-11 10:21
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', '0043_auto_20200507_2302'),
]
operations = [
migrations.CreateModel(
name='ViewLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-06-02 08:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0044_viewlog'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='show_recent',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.5 on 2020-06-02 09:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0045_userpreference_show_recent'),
]
operations = [
migrations.CreateModel(
name='MealType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('order', models.IntegerField(default=0)),
],
),
migrations.AddField(
model_name='mealplan',
name='meal_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.MealType'),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 3.0.5 on 2020-06-02 09:33
from django.db import migrations
from django.utils.translation import gettext as _
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
breakfast = MealType.objects.create(
name=_('Breakfast'),
order=0,
)
lunch = MealType.objects.create(
name=_('Lunch'),
order=0,
)
dinner = MealType.objects.create(
name=_('Dinner'),
order=0,
)
other = MealType.objects.create(
name=_('Other'),
order=0,
)
for m in MealPlan.objects.all():
if m.meal == 'BREAKFAST':
m.meal_type = breakfast
if m.meal == 'LUNCH':
m.meal_type = lunch
if m.meal == 'DINNER':
m.meal_type = dinner
if m.meal == 'OTHER':
m.meal_type = other
m.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0046_auto_20200602_1133'),
]
operations = [
migrations.RunPython(migrate_meal_types),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.5 on 2020-06-02 09:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0047_auto_20200602_1133'),
]
operations = [
migrations.RemoveField(
model_name='mealplan',
name='meal',
),
migrations.AlterField(
model_name='mealplan',
name='meal_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.MealType'),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-06-11 13:08
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', '0048_auto_20200602_1140'),
]
operations = [
migrations.AddField(
model_name='mealtype',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.0.7 on 2020-06-11 13:09
from django.db import migrations
from django.db.models import Q
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
for t in MealType.objects.filter(created_by=None).all():
user_type = MealType.objects.create(
name=t.name,
created_by=u,
)
MealPlan.objects.filter(Q(created_by=u) and Q(meal_type=t)).update(meal_type=user_type)
MealType.objects.filter(created_by=None).delete()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0049_mealtype_created_by'),
]
operations = [
migrations.RunPython(migrate_meal_types),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-06-11 13:18
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', '0050_auto_20200611_1509'),
]
operations = [
migrations.AlterField(
model_name='mealtype',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0051_auto_20200611_1518'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='ingredient_decimals',
field=models.IntegerField(default=2),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-11 20:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0052_userpreference_ingredient_decimals'),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='amount',
field=models.DecimalField(decimal_places=16, default=0, max_digits=32),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.7 on 2020-06-16 08:57
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0053_auto_20200611_2217'),
]
operations = [
migrations.CreateModel(
name='ShareLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.UUID('dbbf5150-0795-4305-b9bd-3952dfa2264b'))),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
],
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-06-16 10:36
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0054_sharelink'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='comments',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.UUID('a6e8f192-cc03-4dd4-8a03-58d7ab6b7df7')),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.0.7 on 2020-06-25 19:18
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
atomic = False
dependencies = [
('cookbook', '0055_auto_20200616_1236'),
]
operations = [
migrations.RenameModel(
old_name='Ingredient',
new_name='Food',
),
migrations.AddField(
model_name='recipe',
name='ingredients',
field=models.ManyToManyField(blank=True, related_name='tmp_ingredients', to='cookbook.RecipeIngredient'),
),
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.UUID('a5f12617-9e4b-41e3-87ee-49db96090974')),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.7 on 2020-06-25 19:57
from django.db import migrations, models
import uuid
def invalidate_shares(apps, schema_editor):
ShareLink = apps.get_model('cookbook', 'ShareLink')
ShareLink.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0055_auto_20200616_1236'),
]
operations = [
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.uuid4),
),
migrations.RunPython(invalidate_shares)
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-06-25 19:27
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0056_auto_20200625_2118'),
]
operations = [
migrations.RenameField(
model_name='recipeingredient',
old_name='ingredient',
new_name='food',
),
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.UUID('4a604ec8-c158-4011-ab5d-ddad7604c1e3')),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-06-25 19:28
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
atomic = False
dependencies = [
('cookbook', '0057_auto_20200625_2127'),
]
operations = [
migrations.RenameModel(
old_name='RecipeIngredient',
new_name='Ingredient',
),
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.UUID('27328011-54fb-46fa-8846-424d5828d858')),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2020-06-25 19:37
from django.db import migrations
def migrate_ingredients(apps, schema_editor):
Recipe = apps.get_model('cookbook', 'Recipe')
Ingredient = apps.get_model('cookbook', 'Ingredient')
for r in Recipe.objects.all():
for i in Ingredient.objects.filter(recipe=r).all():
r.ingredients.add(i)
r.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0058_auto_20200625_2128'),
]
operations = [
migrations.RunPython(migrate_ingredients),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2020-06-25 19:44
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0059_auto_20200625_2137'),
]
operations = [
migrations.RemoveField(
model_name='ingredient',
name='recipe',
),
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.UUID('a7a91b2e-ad33-4159-a35e-828a5244ede9')),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.0.7 on 2020-06-25 20:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0056_auto_20200625_2157'),
('cookbook', '0060_auto_20200625_2144'),
]
operations = [
]

View File

@@ -0,0 +1,47 @@
# Generated by Django 3.0.7 on 2020-06-25 20:19
from django.db import migrations, models
def create_default_step(apps, schema_editor):
Recipe = apps.get_model('cookbook', 'Recipe')
Step = apps.get_model('cookbook', 'Step')
for r in Recipe.objects.filter(internal=True).all():
s = Step.objects.create(
instruction=r.instructions
)
for i in r.ingredients.all():
s.ingredients.add(i)
s.save()
r.steps.add(s)
r.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0061_merge_20200625_2209'),
]
operations = [
migrations.AlterField(
model_name='recipe',
name='ingredients',
field=models.ManyToManyField(blank=True, to='cookbook.Ingredient'),
),
migrations.CreateModel(
name='Step',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('kind', models.CharField(choices=[('TEXT', 'Text')], default='TEXT', max_length=16)),
('instruction', models.TextField(blank=True)),
('ingredients', models.ManyToManyField(blank=True, to='cookbook.Ingredient')),
],
),
migrations.AddField(
model_name='recipe',
name='steps',
field=models.ManyToManyField(blank=True, to='cookbook.Step'),
),
migrations.RunPython(create_default_step)
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-06-25 20:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0062_auto_20200625_2219'),
]
operations = [
migrations.RemoveField(
model_name='recipe',
name='ingredients',
),
migrations.RemoveField(
model_name='recipe',
name='instructions',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2020-06-25 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0063_auto_20200625_2230'),
]
operations = [
migrations.AlterField(
model_name='recipe',
name='file_path',
field=models.CharField(blank=True, default='', max_length=512),
),
migrations.AlterField(
model_name='recipe',
name='file_uid',
field=models.CharField(blank=True, default='', max_length=256),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2020-06-26 12:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0064_auto_20200625_2329'),
]
operations = [
migrations.AddField(
model_name='ingredient',
name='order',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='step',
name='order',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.0.7 on 2020-06-26 12:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0065_auto_20200626_1444'),
]
operations = [
migrations.AlterModelOptions(
name='ingredient',
options={'ordering': ['order', 'pk']},
),
migrations.AlterModelOptions(
name='step',
options={'ordering': ['order', 'pk']},
),
migrations.AddField(
model_name='step',
name='name',
field=models.CharField(blank=True, default='', max_length=128),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-29 13:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0066_auto_20200626_1455'),
]
operations = [
migrations.AlterField(
model_name='ingredient',
name='note',
field=models.CharField(blank=True, max_length=256, null=True),
),
]

View File

@@ -0,0 +1,55 @@
# Generated by Django 3.0.7 on 2020-06-29 19:27
from django.db import migrations, models
import django.db.models.deletion
def convert_old_specials(apps, schema_editor):
Ingredient = apps.get_model('cookbook', 'Ingredient')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
for i in Ingredient.objects.all():
if i.amount == 0:
i.no_amount = True
if i.unit.name == 'Special:Header':
i.header = True
i.unit = None
i.food = None
i.save()
try:
Unit.objects.filter(name='Special:Header').delete()
Food.objects.filter(name='Header').delete()
except Exception:
pass
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0067_auto_20200629_1508'),
]
operations = [
migrations.AddField(
model_name='ingredient',
name='header',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='ingredient',
name='no_amount',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='ingredient',
name='food',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.Food'),
),
migrations.AlterField(
model_name='ingredient',
name='unit',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.Unit'),
),
migrations.RunPython(convert_old_specials)
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-29 19:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0068_auto_20200629_2127'),
]
operations = [
migrations.RenameField(
model_name='ingredient',
old_name='header',
new_name='is_header',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2020-07-01 18:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0069_auto_20200629_2134'),
]
operations = [
migrations.AddField(
model_name='step',
name='time',
field=models.IntegerField(blank=True, default=0),
),
migrations.AlterField(
model_name='step',
name='kind',
field=models.CharField(choices=[('TEXT', 'Text'), ('TIME', 'Time')], default='TEXT', max_length=16),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-07-01 18:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0070_auto_20200701_2007'),
]
operations = [
migrations.RenameField(
model_name='step',
old_name='kind',
new_name='type',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-07-02 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0071_auto_20200701_2048'),
]
operations = [
migrations.AddField(
model_name='step',
name='show_as_header',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-07-08 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0072_step_show_as_header'),
]
operations = [
migrations.AlterField(
model_name='sync',
name='last_checked',
field=models.DateTimeField(null=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.0.7 on 2020-07-09 19:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0073_auto_20200708_2311'),
]
operations = [
migrations.RemoveField(
model_name='keyword',
name='created_by',
),
]

View File

@@ -1,7 +1,27 @@
import re
import uuid
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import User
from django.utils.translation import gettext as _
from django.db import models
from recipes.settings import COMMENT_PREF_DEFAULT
def get_user_name(self):
if not (name := f"{self.first_name} {self.last_name}") == " ":
return name
else:
return self.username
auth.models.User.add_to_class('get_user_name', get_user_name)
def get_model_name(model):
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
class UserPreference(models.Model):
# Themes
@@ -24,9 +44,32 @@ class UserPreference(models.Model):
COLORS = ((PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), (SUCCESS, 'Success'), (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), (DARK, 'Dark'))
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
# Default Page
SEARCH = 'SEARCH'
PLAN = 'PLAN'
BOOKS = 'BOOKS'
PAGES = ((SEARCH, _('Search')), (PLAN, _('Meal-Plan')), (BOOKS, _('Books')),)
# Search Style
SMALL = 'SMALL'
LARGE = 'LARGE'
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')),)
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
default_unit = models.CharField(max_length=32, default='g')
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
search_style = models.CharField(choices=SEARCH_STYLE, max_length=64, default=LARGE)
show_recent = models.BooleanField(default=True)
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
ingredient_decimals = models.IntegerField(default=2)
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
def __str__(self):
return str(self.user)
class Storage(models.Model):
@@ -50,7 +93,7 @@ class Sync(models.Model):
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
path = models.CharField(max_length=512, default="")
active = models.BooleanField(default=True)
last_checked = models.DateTimeField()
last_checked = models.DateTimeField(null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -64,42 +107,22 @@ class SyncLog(models.Model):
msg = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.created_at}:{self.sync} - {self.status}"
class Keyword(models.Model):
name = models.CharField(max_length=64, unique=True)
icon = models.CharField(max_length=1, blank=True, null=True)
icon = models.CharField(max_length=16, blank=True, null=True)
description = models.TextField(default="", blank=True)
created_by = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return "{0} {1}".format(self.icon, self.name)
class Recipe(models.Model):
name = models.CharField(max_length=128)
instructions = models.TextField(blank=True)
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
file_uid = models.CharField(max_length=256, default="")
file_path = models.CharField(max_length=512, default="")
link = models.CharField(max_length=512, null=True, blank=True)
cors_link = models.CharField(max_length=1024, null=True, blank=True)
keywords = models.ManyToManyField(Keyword, blank=True)
working_time = models.IntegerField(default=0)
waiting_time = models.IntegerField(default=0)
internal = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
@property
def all_tags(self):
return ' '.join([(x.icon + x.name) for x in self.keywords.all()])
if self.icon:
return f"{self.icon} {self.name}"
else:
return f"{self.name}"
class Unit(models.Model):
@@ -110,21 +133,65 @@ class Unit(models.Model):
return self.name
class Ingredient(models.Model):
class Food(models.Model):
name = models.CharField(unique=True, max_length=128)
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
def __str__(self):
return self.name
class RecipeIngredient(models.Model):
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT, null=True)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True)
amount = models.DecimalField(default=0, decimal_places=2, max_digits=16)
class Ingredient(models.Model):
food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
note = models.CharField(max_length=256, null=True, blank=True)
is_header = models.BooleanField(default=False)
no_amount = models.BooleanField(default=False)
order = models.IntegerField(default=0)
def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.ingredient)
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
class Meta:
ordering = ['order', 'pk']
class Step(models.Model):
TEXT = 'TEXT'
TIME = 'TIME'
name = models.CharField(max_length=128, default='', blank=True)
type = models.CharField(choices=((TEXT, _('Text')), (TIME, _('Time')),), default=TEXT, max_length=16)
instruction = models.TextField(blank=True)
ingredients = models.ManyToManyField(Ingredient, blank=True)
time = models.IntegerField(default=0, blank=True)
order = models.IntegerField(default=0)
show_as_header = models.BooleanField(default=True)
class Meta:
ordering = ['order', 'pk']
class Recipe(models.Model):
name = models.CharField(max_length=128)
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
file_uid = models.CharField(max_length=256, default="", blank=True)
file_path = models.CharField(max_length=512, default="", blank=True)
link = models.CharField(max_length=512, null=True, blank=True)
cors_link = models.CharField(max_length=1024, null=True, blank=True)
keywords = models.ManyToManyField(Keyword, blank=True)
steps = models.ManyToManyField(Step, blank=True)
working_time = models.IntegerField(default=0)
waiting_time = models.IntegerField(default=0)
internal = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class Comment(models.Model):
@@ -151,7 +218,10 @@ class RecipeImport(models.Model):
class RecipeBook(models.Model):
name = models.CharField(max_length=128)
user = models.ForeignKey(User, on_delete=models.CASCADE)
description = models.TextField(blank=True)
icon = models.CharField(max_length=16, blank=True, null=True)
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.name
@@ -165,18 +235,58 @@ class RecipeBookEntry(models.Model):
return self.recipe.name
class MealPlan(models.Model):
BREAKFAST = 'BREAKFAST'
LUNCH = 'LUNCH'
DINNER = 'DINNER'
OTHER = 'OTHER'
MEAL_TYPES = ((BREAKFAST, _('Breakfast')), (LUNCH, _('Lunch')), (DINNER, _('Dinner')), (OTHER, _('Other')),)
class MealType(models.Model):
name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
meal = models.CharField(choices=MEAL_TYPES, max_length=128, default=BREAKFAST)
def __str__(self):
return self.name
class MealPlan(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
title = models.CharField(max_length=64, blank=True, default='')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
note = models.TextField(blank=True)
date = models.DateField()
def get_label(self):
if self.title:
return self.title
return str(self.recipe)
def get_meal_name(self):
return self.meal_type.name
def __str__(self):
return self.meal + ' (' + str(self.date) + ') ' + str(self.recipe)
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
class ShareLink(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class CookLog(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
rating = models.IntegerField(null=True)
servings = models.IntegerField(default=0)
def __str__(self):
return self.recipe.name
class ViewLog(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.recipe.name

View File

@@ -1,4 +1,5 @@
import base64
import io
import os
from datetime import datetime
@@ -35,7 +36,7 @@ class Dropbox(Provider):
import_count = 0
for recipe in recipes['entries']: # TODO check if has_more is set and import that as well
path = recipe['path_lower']
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(
if not Recipe.objects.filter(file_path__iexact=path).exists() and not RecipeImport.objects.filter(
file_path=path).exists():
name = os.path.splitext(recipe['name'])[0]
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage, file_uid=recipe['id'])
@@ -90,14 +91,14 @@ class Dropbox(Provider):
return response['url']
@staticmethod
def get_base64_file(recipe):
def get_file(recipe):
if not recipe.link:
recipe.link = Dropbox.get_share_link(recipe)
recipe.save()
response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
return base64.b64encode(response.content)
return io.BytesIO(response.content)
@staticmethod
def rename_file(recipe, new_name):

View File

@@ -1,4 +1,5 @@
import base64
import io
import os
import tempfile
from datetime import datetime
@@ -16,9 +17,10 @@ class Nextcloud(Provider):
@staticmethod
def get_client(storage):
options = {
'webdav_hostname': storage.url + '/remote.php/dav/files/' + storage.username,
'webdav_hostname': storage.url,
'webdav_login': storage.username,
'webdav_password': storage.password
'webdav_password': storage.password,
'webdav_root': '/remote.php/dav/files/' + storage.username
}
return wc.Client(options)
@@ -32,7 +34,7 @@ class Nextcloud(Provider):
import_count = 0
for file in files:
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(
if not Recipe.objects.filter(file_path__iexact=path).exists() and not RecipeImport.objects.filter(
file_path=path).exists():
name = os.path.splitext(file)[0]
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage)
@@ -84,18 +86,17 @@ class Nextcloud(Provider):
return Nextcloud.create_share_link(recipe)
@staticmethod
def get_base64_file(recipe):
def get_file(recipe):
client = Nextcloud.get_client(recipe.storage)
tmp_file_path = tempfile.gettempdir() + '/' + recipe.name + '.pdf'
client.download_file(remote_path=recipe.file_path, local_path=tmp_file_path)
val = base64.b64encode(open(tmp_file_path, 'rb').read())
file = io.BytesIO(open(tmp_file_path, 'rb').read())
os.remove(tmp_file_path)
return val
return file
@staticmethod
def rename_file(recipe, new_name):

View File

@@ -12,7 +12,7 @@ class Provider:
raise Exception('Method not implemented in storage provider')
@staticmethod
def get_base64_file(recipe):
def get_file(recipe):
raise Exception('Method not implemented in storage provider')
@staticmethod

208
cookbook/serializer.py Normal file
View File

@@ -0,0 +1,208 @@
from django.contrib.auth.models import User
from drf_writable_nested import WritableNestedModelSerializer, UniqueFieldsMixin
from rest_framework import serializers
from rest_framework.exceptions import APIException, ValidationError
from rest_framework.fields import CurrentUserDefault
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Unit, Ingredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog, Food, Step
from cookbook.templatetags.custom_tags import markdown
class CustomDecimalField(serializers.Field):
"""
Custom decimal field to normalize useless decimal places and allow commas as decimal separators
"""
def to_representation(self, value):
return value.normalize()
def to_internal_value(self, data):
if type(data) == int or type(data) == float:
return data
elif type(data) == str:
try:
return float(data.replace(',', ''))
except ValueError:
raise ValidationError('A valid number is required')
class UserNameSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
def get_user_label(self, obj):
return obj.get_user_name()
class Meta:
model = User
fields = ('id', 'username')
class UserPreferenceSerializer(serializers.ModelSerializer):
class Meta:
model = UserPreference
fields = ('user', 'theme', 'nav_color', 'default_unit', 'default_page', 'search_style', 'show_recent',
'plan_share', 'ingredient_decimals', 'comments')
read_only_fields = ['user']
class StorageSerializer(serializers.ModelSerializer):
class Meta:
model = Storage
fields = ('id', 'name', 'method', 'username', 'password', 'token', 'created_by')
extra_kwargs = {
'password': {'write_only': True},
'token': {'write_only': True},
}
class SyncSerializer(serializers.ModelSerializer):
class Meta:
model = Sync
fields = ('id', 'storage', 'path', 'active', 'last_checked', 'created_at', 'updated_at')
class SyncLogSerializer(serializers.ModelSerializer):
class Meta:
model = SyncLog
fields = ('id', 'sync', 'status', 'msg', 'created_at')
class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
label = serializers.SerializerMethodField('get_label')
def get_label(self, obj):
return str(obj)
def create(self, validated_data):
# since multi select tags dont have id's duplicate names might be routed to create
obj, created = Keyword.objects.get_or_create(**validated_data)
return obj
class Meta:
model = Keyword
fields = ('id', 'name', 'icon', 'label', 'description', 'created_at', 'updated_at')
read_only_fields = ('id',)
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
def create(self, validated_data):
# since multi select tags dont have id's duplicate names might be routed to create
obj, created = Unit.objects.get_or_create(**validated_data)
return obj
class Meta:
model = Unit
fields = ('id', 'name', 'description')
read_only_fields = ('id',)
class FoodSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
def create(self, validated_data):
# since multi select tags dont have id's duplicate names might be routed to create
obj, created = Food.objects.get_or_create(**validated_data)
return obj
class Meta:
model = Food
fields = ('id', 'name', 'recipe')
read_only_fields = ('id',)
class IngredientSerializer(WritableNestedModelSerializer):
food = FoodSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True)
amount = CustomDecimalField()
class Meta:
model = Ingredient
fields = ('id', 'food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
class StepSerializer(WritableNestedModelSerializer):
ingredients = IngredientSerializer(many=True)
class Meta:
model = Step
fields = ('id', 'name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
class RecipeSerializer(WritableNestedModelSerializer):
steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True)
class Meta:
model = Recipe
fields = ['id', 'name', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'internal']
read_only_fields = ['image', 'created_by', 'created_at']
class RecipeImageSerializer(WritableNestedModelSerializer):
class Meta:
model = Recipe
fields = ['image', ]
class RecipeImportSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeImport
fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class RecipeBookSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeBook
fields = '__all__'
read_only_fields = ['id', 'created_by']
class RecipeBookEntrySerializer(serializers.ModelSerializer):
class Meta:
model = RecipeBookEntry
fields = '__all__'
class MealTypeSerializer(serializers.ModelSerializer):
class Meta:
model = MealType
fields = '__all__'
class MealPlanSerializer(serializers.ModelSerializer):
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
note_markdown = serializers.SerializerMethodField('get_note_markdown')
def get_note_markdown(self, obj):
return markdown(obj.note)
class Meta:
model = MealPlan
fields = ('id', 'title', 'recipe', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name')
class ShareLinkSerializer(serializers.ModelSerializer):
class Meta:
model = ShareLink
fields = '__all__'
class CookLogSerializer(serializers.ModelSerializer):
class Meta:
model = CookLog
fields = '__all__'
class ViewLogSerializer(serializers.ModelSerializer):
class Meta:
model = ViewLog
fields = '__all__'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,721 @@
/*!
* Select2 Bootstrap Theme v0.1.0-beta.10 (https://select2.github.io/select2-bootstrap-theme)
* Copyright 2015-2017 Florian Kissling and contributors (https://github.com/select2/select2-bootstrap-theme/graphs/contributors)
* Licensed under MIT (https://github.com/select2/select2-bootstrap-theme/blob/master/LICENSE)
*/
.select2-container--bootstrap {
display: block;
/*------------------------------------* #COMMON STYLES
\*------------------------------------*/
/**
* Search field in the Select2 dropdown.
*/
/**
* No outline for all search fields - in the dropdown
* and inline in multi Select2s.
*/
/**
* Adjust Select2's choices hover and selected styles to match
* Bootstrap 3's default dropdown styles.
*
* @see http://getbootstrap.com/components/#dropdowns
*/
/**
* Clear the selection.
*/
/**
* Address disabled Select2 styles.
*
* @see https://select2.github.io/examples.html#disabled
* @see http://getbootstrap.com/css/#forms-control-disabled
*/
/*------------------------------------* #DROPDOWN
\*------------------------------------*/
/**
* Dropdown border color and box-shadow.
*/
/**
* Limit the dropdown height.
*/
/*------------------------------------* #SINGLE SELECT2
\*------------------------------------*/
/*------------------------------------* #MULTIPLE SELECT2
\*------------------------------------*/
/**
* Address Bootstrap control sizing classes
*
* 1. Reset Bootstrap defaults.
* 2. Adjust the dropdown arrow button icon position.
*
* @see http://getbootstrap.com/css/#forms-control-sizes
*/
/* 1 */
/*------------------------------------* #RTL SUPPORT
\*------------------------------------*/
}
.select2-container--bootstrap .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
color: #555555;
font-size: 14px;
outline: 0;
}
.select2-container--bootstrap .select2-selection.form-control {
border-radius: 4px;
}
.select2-container--bootstrap .select2-search--dropdown .select2-search__field {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
color: #555555;
font-size: 14px;
}
.select2-container--bootstrap .select2-search__field {
outline: 0;
/* Firefox 18- */
/**
* Firefox 19+
*
* @see http://stackoverflow.com/questions/24236240/color-for-styled-placeholder-text-is-muted-in-firefox
*/
}
.select2-container--bootstrap .select2-search__field::-webkit-input-placeholder {
color: #999;
}
.select2-container--bootstrap .select2-search__field:-moz-placeholder {
color: #999;
}
.select2-container--bootstrap .select2-search__field::-moz-placeholder {
color: #999;
opacity: 1;
}
.select2-container--bootstrap .select2-search__field:-ms-input-placeholder {
color: #999;
}
.select2-container--bootstrap .select2-results__option {
padding: 6px 12px;
/**
* Disabled results.
*
* @see https://select2.github.io/examples.html#disabled-results
*/
/**
* Hover state.
*/
/**
* Selected state.
*/
}
.select2-container--bootstrap .select2-results__option[role=group] {
padding: 0;
}
.select2-container--bootstrap .select2-results__option[aria-disabled=true] {
color: #777777;
cursor: not-allowed;
}
.select2-container--bootstrap .select2-results__option[aria-selected=true] {
background-color: #f5f5f5;
color: #262626;
}
.select2-container--bootstrap .select2-results__option--highlighted[aria-selected] {
background-color: #337ab7;
color: #fff;
}
.select2-container--bootstrap .select2-results__option .select2-results__option {
padding: 6px 12px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option {
margin-left: -12px;
padding-left: 24px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -24px;
padding-left: 36px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -36px;
padding-left: 48px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -48px;
padding-left: 60px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -60px;
padding-left: 72px;
}
.select2-container--bootstrap .select2-results__group {
color: #777777;
display: block;
padding: 6px 12px;
font-size: 12px;
line-height: 1.42857143;
white-space: nowrap;
}
.select2-container--bootstrap.select2-container--focus .select2-selection, .select2-container--bootstrap.select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
border-color: #66afe9;
}
.select2-container--bootstrap.select2-container--open {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
/**
* Handle border radii of the container when the dropdown is showing.
*/
}
.select2-container--bootstrap.select2-container--open .select2-selection .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 4px 4px 4px;
}
.select2-container--bootstrap.select2-container--open.select2-container--below .select2-selection {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-color: transparent;
}
.select2-container--bootstrap.select2-container--open.select2-container--above .select2-selection {
border-top-right-radius: 0;
border-top-left-radius: 0;
border-top-color: transparent;
}
.select2-container--bootstrap .select2-selection__clear {
color: #999;
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px;
}
.select2-container--bootstrap .select2-selection__clear:hover {
color: #333;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection {
border-color: #ccc;
-webkit-box-shadow: none;
box-shadow: none;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection,
.select2-container--bootstrap.select2-container--disabled .select2-search__field {
cursor: not-allowed;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection,
.select2-container--bootstrap.select2-container--disabled .select2-selection--multiple .select2-selection__choice {
background-color: #eeeeee;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection__clear,
.select2-container--bootstrap.select2-container--disabled .select2-selection--multiple .select2-selection__choice__remove {
display: none;
}
.select2-container--bootstrap .select2-dropdown {
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
border-color: #66afe9;
overflow-x: hidden;
margin-top: -1px;
}
.select2-container--bootstrap .select2-dropdown--above {
-webkit-box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.175);
margin-top: 1px;
}
.select2-container--bootstrap .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
}
.select2-container--bootstrap .select2-selection--single {
height: 34px;
line-height: 1.42857143;
padding: 6px 24px 6px 12px;
/**
* Adjust the single Select2's dropdown arrow button appearance.
*/
}
.select2-container--bootstrap .select2-selection--single .select2-selection__arrow {
position: absolute;
bottom: 0;
right: 12px;
top: 0;
width: 4px;
}
.select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
border-color: #999 transparent transparent transparent;
border-style: solid;
border-width: 4px 4px 0 4px;
height: 0;
left: 0;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--bootstrap .select2-selection--single .select2-selection__rendered {
color: #555555;
padding: 0;
}
.select2-container--bootstrap .select2-selection--single .select2-selection__placeholder {
color: #999;
}
.select2-container--bootstrap .select2-selection--multiple {
min-height: 34px;
padding: 0;
height: auto;
/**
* Make Multi Select2's choices match Bootstrap 3's default button styles.
*/
/**
* Minus 2px borders.
*/
/**
* Clear the selection.
*/
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__rendered {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
display: block;
line-height: 1.42857143;
list-style: none;
margin: 0;
overflow: hidden;
padding: 0;
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__placeholder {
color: #999;
float: left;
margin-top: 5px;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
color: #555555;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
cursor: default;
float: left;
margin: 5px 0 0 6px;
padding: 0 6px;
}
.select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
background: transparent;
padding: 0 12px;
height: 32px;
line-height: 1.42857143;
margin-top: 0;
min-width: 5em;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 3px;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 6px;
}
.select2-container--bootstrap .select2-selection--single.input-sm,
.input-group-sm .select2-container--bootstrap .select2-selection--single,
.form-group-sm .select2-container--bootstrap .select2-selection--single {
border-radius: 3px;
font-size: 12px;
height: 30px;
line-height: 1.5;
padding: 5px 22px 5px 10px;
/* 2 */
}
.select2-container--bootstrap .select2-selection--single.input-sm .select2-selection__arrow b,
.input-group-sm .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b,
.form-group-sm .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
margin-left: -5px;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple {
min-height: 30px;
border-radius: 3px;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-selection__choice,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
font-size: 12px;
line-height: 1.5;
margin: 4px 0 0 5px;
padding: 0 5px;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-search--inline .select2-search__field,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
padding: 0 10px;
font-size: 12px;
height: 28px;
line-height: 1.5;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-selection__clear,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 5px;
}
.select2-container--bootstrap .select2-selection--single.input-lg,
.input-group-lg .select2-container--bootstrap .select2-selection--single,
.form-group-lg .select2-container--bootstrap .select2-selection--single {
border-radius: 6px;
font-size: 18px;
height: 46px;
line-height: 1.3333333;
padding: 10px 31px 10px 16px;
/* 1 */
}
.select2-container--bootstrap .select2-selection--single.input-lg .select2-selection__arrow,
.input-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow,
.form-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow {
width: 5px;
}
.select2-container--bootstrap .select2-selection--single.input-lg .select2-selection__arrow b,
.input-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b,
.form-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
border-width: 5px 5px 0 5px;
margin-left: -5px;
margin-left: -10px;
margin-top: -2.5px;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple {
min-height: 46px;
border-radius: 6px;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-selection__choice,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
font-size: 18px;
line-height: 1.3333333;
border-radius: 4px;
margin: 9px 0 0 8px;
padding: 0 10px;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-search--inline .select2-search__field,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
padding: 0 16px;
font-size: 18px;
height: 44px;
line-height: 1.3333333;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-selection__clear,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 10px;
}
.select2-container--bootstrap .select2-selection.input-lg.select2-container--open .select2-selection--single {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
}
.select2-container--bootstrap .select2-selection.input-lg.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 5px 5px 5px;
}
.input-group-lg .select2-container--bootstrap .select2-selection.select2-container--open .select2-selection--single {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
}
.input-group-lg .select2-container--bootstrap .select2-selection.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 5px 5px 5px;
}
.select2-container--bootstrap[dir="rtl"] {
/**
* Single Select2
*
* 1. Makes sure that .select2-selection__placeholder is positioned
* correctly.
*/
/**
* Multiple Select2
*/
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single {
padding-left: 24px;
padding-right: 12px;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 0;
padding-left: 0;
text-align: right;
/* 1 */
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 12px;
right: auto;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__arrow b {
margin-left: 0;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice,
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 0;
margin-right: 6px;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
/*------------------------------------* #ADDITIONAL GOODIES
\*------------------------------------*/
/**
* Address Bootstrap's validation states
*
* If a Select2 widget parent has one of Bootstrap's validation state modifier
* classes, adjust Select2's border colors and focus states accordingly.
* You may apply said classes to the Select2 dropdown (body > .select2-container)
* via JavaScript match Bootstraps' to make its styles match.
*
* @see http://getbootstrap.com/css/#forms-control-validation
*/
.has-warning .select2-dropdown,
.has-warning .select2-selection {
border-color: #8a6d3b;
}
.has-warning .select2-container--focus .select2-selection,
.has-warning .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
border-color: #66512c;
}
.has-warning.select2-drop-active {
border-color: #66512c;
}
.has-warning.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #66512c;
}
.has-error .select2-dropdown,
.has-error .select2-selection {
border-color: #a94442;
}
.has-error .select2-container--focus .select2-selection,
.has-error .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
border-color: #843534;
}
.has-error.select2-drop-active {
border-color: #843534;
}
.has-error.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #843534;
}
.has-success .select2-dropdown,
.has-success .select2-selection {
border-color: #3c763d;
}
.has-success .select2-container--focus .select2-selection,
.has-success .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
border-color: #2b542c;
}
.has-success.select2-drop-active {
border-color: #2b542c;
}
.has-success.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #2b542c;
}
/**
* Select2 widgets in Bootstrap Input Groups
*
* @see http://getbootstrap.com/components/#input-groups
* @see https://github.com/twbs/bootstrap/blob/master/less/input-groups.less
*/
/**
* Reset rounded corners
*/
.input-group > .select2-hidden-accessible:first-child + .select2-container--bootstrap > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:first-child + .select2-container--bootstrap > .selection > .select2-selection.form-control {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.input-group > .select2-hidden-accessible:not(:first-child) + .select2-container--bootstrap:not(:last-child) > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:not(:first-child) + .select2-container--bootstrap:not(:last-child) > .selection > .select2-selection.form-control {
border-radius: 0;
}
.input-group > .select2-hidden-accessible:not(:first-child):not(:last-child) + .select2-container--bootstrap:last-child > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:not(:first-child):not(:last-child) + .select2-container--bootstrap:last-child > .selection > .select2-selection.form-control {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.input-group > .select2-container--bootstrap {
display: table;
table-layout: fixed;
position: relative;
z-index: 2;
width: 100%;
margin-bottom: 0;
/**
* Adjust z-index like Bootstrap does to show the focus-box-shadow
* above appended buttons in .input-group and .form-group.
*/
/**
* Adjust alignment of Bootstrap buttons in Bootstrap Input Groups to address
* Multi Select2's height which - depending on how many elements have been selected -
* may grow taller than its initial size.
*
* @see http://getbootstrap.com/components/#input-groups
*/
}
.input-group > .select2-container--bootstrap > .selection > .select2-selection.form-control {
float: none;
}
.input-group > .select2-container--bootstrap.select2-container--open, .input-group > .select2-container--bootstrap.select2-container--focus {
z-index: 3;
}
.input-group > .select2-container--bootstrap,
.input-group > .select2-container--bootstrap .input-group-btn,
.input-group > .select2-container--bootstrap .input-group-btn .btn {
vertical-align: top;
}
/**
* Temporary fix for https://github.com/select2/select2-bootstrap-theme/issues/9
*
* Provides `!important` for certain properties of the class applied to the
* original `<select>` element to hide it.
*
* @see https://github.com/select2/select2/pull/3301
* @see https://github.com/fk/select2/commit/31830c7b32cb3d8e1b12d5b434dee40a6e753ada
*/
.form-control.select2-hidden-accessible {
position: absolute !important;
width: 1px !important;
}
/**
* Display override for inline forms
*/
@media (min-width: 768px) {
.form-inline .select2-container--bootstrap {
display: inline-block;
}
}

1
cookbook/static/css/select2.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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