Compare commits

..

191 Commits
0.3.2 ... 0.8.2

Author SHA1 Message Date
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
d09c6dbfed added automatic redirect for setup 2020-06-03 10:51:39 +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
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
139 changed files with 14224 additions and 1141 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,21 @@
VIRTUAL_HOST=
LETSENCRYPT_HOST=
LETSENCRYPT_EMAIL=
# only set this to true when testing/debugging
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.
GUNICORN_MEDIA=0

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

View File

@@ -0,0 +1,18 @@
name: publish dev image docker
on:
push:
branches:
- '*'
- '*/*'
- '!master'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- 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,19 @@
name: publish latest image docker
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- 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,25 @@
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 the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- 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>

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

@@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="vabene1111-PC">
<words>
<w>gunicorn</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>

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

@@ -0,0 +1,35 @@
<?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$/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$/templates" />
<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,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
@@ -12,63 +14,55 @@ Recipes is a Django application to manage, tag and search recipes using either b
- :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.

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 - recipes.wsgi

View File

@@ -2,10 +2,103 @@ 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')
@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 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(Ingredient)
class RecipeIngredientAdmin(admin.ModelAdmin):
list_display = ('recipe', 'ingredient', 'amount', 'unit')
admin.site.register(RecipeIngredient, RecipeIngredientAdmin)
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', 'date')
@staticmethod
def user(obj):
return obj.created_by.get_user_name()
admin.site.register(MealPlan, MealPlanAdmin)

View File

@@ -43,4 +43,12 @@ class RecipeFilter(django_filters.FilterSet):
class Meta:
model = Recipe
fields = ['name', 'keywords', 'ingredients']
fields = ['name', 'keywords', 'ingredients', 'internal']
class IngredientFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Ingredient
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,33 @@ 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')
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.'),
}
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')
}
@@ -71,11 +92,14 @@ class InternalRecipeForm(forms.ModelForm):
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
}
widgets = {'keywords': MultiSelectWidget}
help_texts = {
'instructions': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
}
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 +109,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'
@@ -141,6 +184,13 @@ class KeywordForm(forms.ModelForm):
widgets = {'icon': EmojiPickerTextInput}
class IngredientForm(forms.ModelForm):
class Meta:
model = Ingredient
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 +247,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', '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

@@ -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,68 @@
"""
Source: https://djangosnippets.org/snippets/1703/
"""
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse
def get_allowed_groups(groups_required):
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 group_required(*groups_required):
"""Requires user membership in at least one of the groups passed in."""
def in_groups(u):
groups_allowed = get_allowed_groups(groups_required)
if u.is_authenticated:
if u.is_superuser | bool(u.groups.filter(name__in=groups_allowed)):
return True
return False
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 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 request.user.is_superuser:
group_allowed = get_allowed_groups(self.groups_required)
user_groups = []
for group in request.user.groups.values_list('name', flat=True):
user_groups.append(group)
if len(set(user_groups).intersection(group_allowed)) <= 0:
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:
obj = self.get_object()
if not (obj.created_by == request.user or request.user.is_superuser):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)

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

@@ -1,8 +1,26 @@
import re
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
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
BOOTSTRAP = 'BOOTSTRAP'
@@ -24,9 +42,30 @@ 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')
def __str__(self):
return str(self.user)
class Storage(models.Model):
@@ -64,17 +103,23 @@ 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)
if self.icon:
return f"{self.icon} {self.name}"
else:
return f"{self.name}"
class Recipe(models.Model):
@@ -97,10 +142,6 @@ class Recipe(models.Model):
def __str__(self):
return self.name
@property
def all_tags(self):
return ' '.join([(x.icon + x.name) for x in self.keywords.all()])
class Unit(models.Model):
name = models.CharField(unique=True, max_length=128)
@@ -112,16 +153,18 @@ class Unit(models.Model):
class Ingredient(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)
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT)
amount = models.DecimalField(default=0, decimal_places=2, max_digits=16)
note = models.CharField(max_length=64, null=True, blank=True)
def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.ingredient)
@@ -151,7 +194,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
@@ -172,11 +218,39 @@ class MealPlan(models.Model):
OTHER = 'OTHER'
MEAL_TYPES = ((BREAKFAST, _('Breakfast')), (LUNCH, _('Lunch')), (DINNER, _('Dinner')), (OTHER, _('Other')),)
user = models.ForeignKey(User, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
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 = models.CharField(choices=MEAL_TYPES, max_length=128, default=BREAKFAST)
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):
meals = dict(self.MEAL_TYPES)
return meals.get(self.meal)
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.meal + ' (' + str(self.date) + ') ' + str(self.recipe)
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

@@ -35,7 +35,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'])

View File

@@ -16,9 +16,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 +33,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)

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

View File

@@ -0,0 +1,21 @@
/* css classes needed to render markdown blockquotes */
blockquote {
background: #f9f9f9;
border-left: 4px solid #ccc;
margin: 1.5em 10px;
padding: .5em 10px;
quotes: none;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: .1em;
margin-right: .25em;
vertical-align: -.4em;
}
blockquote p {
display: inline;
}

7
cookbook/static/js/bootstrap.min.js 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

1
cookbook/static/js/pdf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

5
cookbook/static/js/popper.min.js 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

2
cookbook/static/js/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="pizza-slice"
class="svg-inline--fa fa-pizza-slice fa-w-16"
role="img"
viewBox="0 0 512 512"
version="1.1"
id="svg4"
sodipodi:docname="recipe_no_image.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2066"
id="namedview6"
showgrid="false"
inkscape:zoom="0.921875"
inkscape:cx="309.52383"
inkscape:cy="214.71807"
inkscape:window-x="2869"
inkscape:window-y="54"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<rect
style="fill:#f5f5f6;fill-opacity:1;stroke:#d8dde0;stroke-width:3.77952766;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect817"
width="1717.1526"
height="1092.339"
x="-602.57629"
y="-290.16949" />
<path
d="m 198.99508,105.32039 c -9.4835,-0.89523 -18.30973,4.9591 -20.73342,14.20588 l -8.69126,33.14115 c 110.10488,3.23343 184.58794,76.92493 189.24752,186.70242 l 33.41527,-9.29389 c 9.22528,-2.56789 14.95881,-11.59086 13.8614,-21.1439 C 393.84116,202.45866 305.74904,115.43295 198.99508,105.32039 Z m -34.31314,65.96427 -58.59701,223.50695 a 9.5128449,9.5471493 0 0 0 11.73701,11.63209 l 222.4163,-61.9004 C 337.73239,241.51893 268.0087,172.46259 164.68194,171.30821 Z m 16.19707,178.95751 a 18.779213,18.846933 0 1 1 18.77921,-18.84693 18.779213,18.846933 0 0 1 -18.77921,18.84693 z m 28.16882,-89.52293 a 18.779213,18.846933 0 1 1 18.77921,-18.84693 18.779213,18.846933 0 0 1 -18.77921,18.84693 z m 61.03245,61.25253 a 18.779213,18.846933 0 1 1 18.77921,-18.84693 18.779213,18.846933 0 0 1 -18.77921,18.84693 z"
id="path2"
style="fill:#d9cfbe;fill-opacity:1;stroke-width:0.58790755"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,14 @@ from django_tables2.utils import A # alias for Accessor
from .models import *
class RecipeTable(tables.Table):
class ImageUrlColumn(tables.Column):
def render(self, value):
if value.url:
return value.url
return None
class RecipeTableSmall(tables.Table):
id = tables.LinkColumn('edit_recipe', args=[A('id')])
name = tables.LinkColumn('view_recipe', args=[A('id')])
all_tags = tables.Column(
@@ -18,6 +25,19 @@ class RecipeTable(tables.Table):
fields = ('id', 'name', 'all_tags')
class RecipeTable(tables.Table):
edit = tables.TemplateColumn("<a style='color: inherit' href='{% url 'edit_recipe' record.id %}' >" + _('Edit') + "</a>")
name = tables.LinkColumn('view_recipe', args=[A('id')])
all_tags = tables.Column(
attrs={'td': {'class': 'd-none d-lg-table-cell'}, 'th': {'class': 'd-none d-lg-table-cell'}})
image = ImageUrlColumn()
class Meta:
model = Recipe
template_name = 'recipes_table.html'
fields = ('id', 'name', 'all_tags', 'image', 'instructions', 'working_time', 'waiting_time', 'internal')
class KeywordTable(tables.Table):
id = tables.LinkColumn('edit_keyword', args=[A('id')])
@@ -27,6 +47,15 @@ class KeywordTable(tables.Table):
fields = ('id', 'icon', 'name')
class IngredientTable(tables.Table):
id = tables.LinkColumn('edit_ingredient', args=[A('id')])
class Meta:
model = Keyword
template_name = 'generic/table_template.html'
fields = ('id', 'name')
class StorageTable(tables.Table):
id = tables.LinkColumn('edit_storage', args=[A('id')])
@@ -44,7 +73,7 @@ class ImportLogTable(tables.Table):
if value == 'SUCCESS':
return format_html('<span class="badge badge-success">%s</span>' % value)
else:
return format_html('<span class="badge badge-error">%s</span>' % value)
return format_html('<span class="badge badge-danger">%s</span>' % value)
class Meta:
model = SyncLog
@@ -71,9 +100,27 @@ class SyncTable(tables.Table):
class RecipeImportTable(tables.Table):
id = tables.LinkColumn('new_recipe_import', args=[A('id')])
delete = tables.TemplateColumn("<a href='{% url 'delete_import' record.id %}' >" + _('Delete') + "</a>")
delete = tables.TemplateColumn("<a href='{% url 'delete_recipe_import' record.id %}' >" + _('Delete') + "</a>")
class Meta:
model = RecipeImport
template_name = 'generic/table_template.html'
fields = ('id', 'name', 'file_path')
class ViewLogTable(tables.Table):
recipe = tables.LinkColumn('view_recipe', args=[A('recipe_id')])
class Meta:
model = ViewLog
template_name = 'generic/table_template.html'
fields = ('recipe', 'created_at')
class CookLogTable(tables.Table):
recipe = tables.LinkColumn('view_recipe', args=[A('recipe_id')])
class Meta:
model = CookLog
template_name = 'generic/table_template.html'
fields = ('recipe', 'rating', 'serving', 'created_at')

View File

@@ -20,29 +20,19 @@
<!-- Bootstrap 4 -->
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.4.1.js"
integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
crossorigin="anonymous"></script>
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<!-- Select2 for use with django autocomplete light -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script>
<link href="{% static 'css/select2.min.css' %}" rel="stylesheet"/>
<script src="{% static 'js/select2.min.js' %}"></script>
<!-- Bootstrap theme for select2 -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/select2-bootstrap-theme/0.1.0-beta.10/select2-bootstrap.css"
integrity="sha256-zFnNbsU+u3l0K+MaY92RvJI6AdAVAxK3/QrBApHvlH8=" crossorigin="anonymous"/>
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}"/>
<link rel="stylesheet"
href="{% static 'themes/select2-bootstrap-theme.css' %}"
crossorigin="anonymous"/>
<link rel="stylesheet" href="{% static 'themes/select2-bootstrap-theme.css' %}"/>
<script type="text/javascript">
$.fn.select2.defaults.set("theme", "bootstrap");
@@ -54,110 +44,127 @@
{% block extra_head %} <!-- block for templates to put stuff into header -->
{% endblock %}
<style>
@media (max-width: 1025px) {
.container {
width: 95% !important;
margin-left: 20px !important;
margin-right: 20px !important;
max-width: 1200px !important;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}" id="id_main_nav">
<!--<a class="navbar-brand" href="{% url 'index' %}">{% trans 'Cookbook' %}</a>-->
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item {% if request.resolver_match.url_name == "index" %}active{% endif %}">
<a class="nav-link" href="{% url 'index' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
<li class="nav-item {% if request.resolver_match.url_name in 'view_search,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
<a class="nav-link" href="{% url 'view_search' %}"><i
class="fas fa-book"></i> {% trans 'Cookbook' %}<span
class="sr-only">(current)</span></a>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_books" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_books' %}"><i class="fas fa-bookmark"></i> {% trans 'Books' %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_books,view_plan,view_shopping,list_ingredient,view_plan_entry' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-mortar-pestle"></i> {% trans 'Utensils' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'view_books' %}"><i
class="fas fa-bookmark fa-fw"></i> {% trans 'Books' %}
</a>
<a class="dropdown-item" href="{% url 'view_plan' %}"><i
class="fas fa-calendar fa-fw"></i> {% trans 'Meal-Plan' %}
</a>
<a class="dropdown-item" href="{% url 'view_shopping' %}"><i
class="fas fa-shopping-cart fa-fw"></i> {% trans 'Shopping' %}
</a>
<a class="dropdown-item" href="{% url 'list_ingredient' %}"><i
class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %}
</a>
</div>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_plan" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_plan' %}"><i class="fas fa-calendar"></i> {% trans 'Meal-Plan' %}
</a>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_shopping" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_shopping' %}"><i class="fas fa-shopping-cart"></i> {% trans 'Shopping' %}
</a>
</li>
<li class="nav-item dropdown">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_keyword,data_batch_edit' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-tags"></i> {% trans 'Tags' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'list_keyword' %}"><i
class="fas fa-tags"></i> {% trans 'Keyword' %}</a>
class="fas fa-tags fa-fw"></i> {% trans 'Keyword' %}</a>
<a class="dropdown-item" href="{% url 'data_batch_edit' %}"><i
class="fas fa-edit"></i> {% trans 'Batch Edit' %}</a>
class="fas fa-edit fa-fw"></i> {% trans 'Batch Edit' %}</a>
</div>
</li>
<li class="nav-item dropdown">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_storage,data_sync,list_recipe_import,list_sync_log,data_stats,edit_ingredient' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i class="fas fa-database"></i> {% trans 'Storage Data' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'list_storage' %}"><i
class="fas fa-database"></i> {% trans 'Storage Backends' %}</a>
class="fas fa-database fa-fw"></i> {% trans 'Storage Backends' %}</a>
<a class="dropdown-item" href="{% url 'data_sync' %}"><i
class="fas fa-sync-alt"></i> {% trans 'Configure Sync' %}</a>
<a class="dropdown-item" href="{% url 'list_import' %}"><i
class="far fa-file-alt"></i> {% trans 'Import Recipes' %}</a>
<a class="dropdown-item" href="{% url 'list_import_log' %}"><i
class="fas fa-history"></i> {% trans 'Import Log' %}</a>
class="fas fa-sync-alt fa-fw"></i> {% trans 'Configure Sync' %}</a>
<a class="dropdown-item" href="{% url 'list_recipe_import' %}"><i
class="far fa-file-alt fa-fw"></i> {% trans 'Discovered Recipes' %}</a>
<a class="dropdown-item" href="{% url 'list_sync_log' %}"><i
class="fas fa-history fa-fw"></i> {% trans 'Discovery Log' %}</a>
<a class="dropdown-item" href="{% url 'data_stats' %}"><i
class="fas fa-chart-line"></i> {% trans 'Statistics' %}</a>
class="fas fa-chart-line fa-fw"></i> {% trans 'Statistics' %}</a>
<a class="dropdown-item" href="{% url 'edit_ingredient' %}"><i
class="fas fa-balance-scale"></i> {% trans 'Units & Ingredients' %}</a>
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units & Ingredients' %}</a>
<a class="dropdown-item" href="{% url 'view_import' %}"><i
class="fas fa-file-import"></i> {% trans 'Import Recipe' %}</a>
</div>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item {% if request.resolver_match.url_name == "view_settings" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_settings' %}"><i
class="fas fa-user-cog"></i> {% trans 'Settings' %}</a>
</li>
{% if user.is_superuser %}
{% if user.is_authenticated %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings,view_history' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i
class="fas fa-user-alt"></i> {{ user.get_user_name }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'view_settings' %}"><i
class="fas fa-user-cog fa-fw"></i> {% trans 'Settings' %}</a>
<a class="dropdown-item" href="{% url 'view_history' %}"><i
class="fas fa-history"></i> {% trans 'History' %}</a>
{% if user.is_superuser %}
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
{% endif %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i
class="fab fa-markdown fa-fw"></i> {% trans 'Markdown Help' %}</a>
<a class="dropdown-item" href="https://github.com/vabene1111/recipes"><i
class="fab fa-github fa-fw"></i> {% trans 'GitHub' %}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'logout' %}"><i
class="fas fa-sign-out-alt fa-fw"></i> {% trans 'Logout' %}</a>
</div>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield"></i> {% trans 'Admin' %}</a>
<a class="nav-link" href="{% url 'login' %}">{% trans 'Login' %} <i class="fas fa-sign-in-alt"></i></a>
</li>
{% endif %}
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'logout' %}">{% trans 'Logout' %} {{ user.get_username }} <i
class="fas fa-sign-out-alt"></i></a>
{% else %}
<a class="nav-link" href="{% url 'login' %}">{% trans 'Login' %} <i class="fas fa-sign-in-alt"></i></a>
{% endif %}
</li>
</ul>
</div>
</nav>
<br/>
<br/>
<div class="container">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<div class="row">
<div class="col col-md-12">
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
</div>
{% endfor %}

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load custom_tags %}
{% load i18n %}
{% block title %}{% trans 'Recipe Books' %}{% endblock %}
@@ -10,52 +11,71 @@
<h2>{% trans 'Recipe Books' %}</h2>
</div>
<div class="col col-md-3" style="text-align: right">
<a href="{% url 'new_book' %}" class="btn btn-success"><i
<a href="{% url 'new_recipe_book' %}" class="btn btn-success"><i
class="fas fa-plus-circle"></i> {% trans 'New Book' %}</a>
</div>
</div>
<br/>
<br/>
{% for b in book_list %}
<div class="row">
<div class="col col-md-10">
<a data-toggle="collapse" href="#collapse_{{ b.book.pk }}" role="button" aria-expanded="false"
aria-controls="collapse_{{ b.book.pk }}"><h4>{{ b.book.name }}</h4></a>
</div>
<div class="col col-md-2" style="text-align: right">
<h4>
<div class="col-12">
<div class="card" style="margin-top: 2px">
<div class="card-body">
<h5 class="card-title">{% if b.book.icon %}{{ b.book.icon }} {% endif %}{{ b.book.name }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{% if b.book.created_by != request.user %}
{% trans 'by' %} {{ b.book.created_by.get_user_name }}
{% endif %}</h6>
<a href="{% url 'edit_recipe_book' b.book.pk %}"> <i class="fas fa-pencil-alt"></i></a>
<a href="{% url 'delete_recipe_book' b.book.pk %}"><i class="fas fa-trash-alt"></i></a>
</h4>
</div>
<hr/>
</div>
{% if b.book.description %}
<p class="card-text">{{ b.book.description }}</p>
{% endif %}
<a data-toggle="collapse" href="#collapse_{{ b.book.pk }}" role="button" aria-expanded="false"
aria-controls="collapse_{{ b.book.pk }}" class="card-link">{% trans 'Toggle Recipes' %}</a>
{% if b.book.created_by == request.user or request.user.is_superuser %}
<a href="{% url 'edit_recipe_book' b.book.pk %}" class="card-link">{% trans 'Edit' %}</a>
<a href="{% url 'delete_recipe_book' b.book.pk %}"
class="card-link">{% trans 'Delete' %}</a>
{% endif %}
</div>
<div class="collapse" id="collapse_{{ b.book.pk }}">
{% if b.recipes %}
<ul class="list-group list-group-flush">
{% for r in b.recipes %}
<li class="list-group-item">
<div class="row">
<div class="col-10">
{% recipe_last r.recipe request.user as last_cooked %}
<a href="{% url 'view_recipe' r.recipe.pk %}">{{ r.recipe.name }}</a>
{% recipe_rating r.recipe request.user as rating %}
{{ rating|safe }}
{% if last_cooked %}
&nbsp;
<span class="badge badge-primary">{% trans 'Last cooked' %} {{ last_cooked|date }}</span>
{% endif %}
<div class="row">
<div class="col col-md-12">
<div class="collapse" id="collapse_{{ b.book.pk }}">
{% if b.recipes %}
<ul>
{% for r in b.recipes %}
<div class="row">
<div class="col col-md-10">
<li><a href="{% url 'view_recipe' r.recipe.pk %}">{{ r.recipe.name }}</a></li>
</div>
<div class="col col-md-2" style="text-align: right">
<a href="{% url 'delete_recipe_book_entry' r.pk %}"><i class="fas fa-trash-alt"></i></a>
</div>
</div>
{% endfor %}
</ul>
{% else %}
{% trans 'There are no recipes in this book yet.' %}
{% endif %}
</div>
{% if b.book.created_by == request.user or request.user.is_superuser %}
<div class="col-2" style="text-align: right">
<a href="{% url 'delete_recipe_book_entry' r.pk %}"
class="pull-right"><i class="fas fa-trash-alt"></i></a>
</div>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="card-body">
<p>
{% trans 'There are no recipes in this book yet.' %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<br/>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% load static %}
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
{% block extra_head %}
{{ form.media }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-12">
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-file-export"></i> {% trans 'Export' %}
</button>
</form>
</div>
</div>
{% if export %}
<br/>
<div class="row">
<div class="col col-md-12">
<label for="id_export">
{% trans 'Exported Recipe' %}</label>
<textarea id="id_export" class="form-control" rows="12">
{{ export }}
</textarea>
</div>
</div>
<br/>
<div class="row">
<div class="col col-md-12 text-center">
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
data-placement="right" title="{% trans 'Copy to clipboard' %}" id="id_btn_copy"
onmouseout="resetTooltip()"><i
class="far fa-copy"></i></button>
</div>
</div>
<script type="text/javascript">
function copy() {
let json = $('#id_export');
json.select();
$('#id_btn_copy').attr('data-original-title', '{% trans 'Copied!' %}').tooltip('show');
document.execCommand("copy");
}
function resetTooltip() {
setTimeout(function () {
$('#id_btn_copy').attr('data-original-title', '{% trans 'Copy list to clipboard' %}');
}, 300);
}
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endif %}
{% endblock %}

View File

@@ -26,11 +26,14 @@
</div>
{% if field.name == 'name' %}
<label>{% trans 'Ingredients' %}</label>
{{ form.ingredients.errors }}
<div id="ingredients-table"></div>
<br>
<div class="table-controls" style="text-align: center">
<button class="btn btn-success" id="new_empty" type="button" style="min-width: 20vw"><i
class="fas fa-plus-circle"></i></button>
<button class="btn btn-warning" id="new_header" type="button" data-toggle="tooltip"
data-placement="top" title="{% trans 'Insert a header between the ingredients.' %}"><i class="fas fa-heading"></i></button>
<button type="button" class="btn btn-secondary" data-container="body" data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
@@ -45,7 +48,7 @@
<hr>
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
<a href="{% url 'redirect_delete' form.instance|get_class|lower form.instance.pk %}"
<a href="{% delete_url form.instance|get_class form.instance.pk %}"
class="btn btn-danger"><i class="fas fa-trash-alt"></i> {% trans 'Delete' %}</a>
{% if view_url %}
<a href="{{ view_url }}" class="btn btn-info"><i class="far fa-eye"></i> {% trans 'View' %}</a>
@@ -56,7 +59,7 @@
{% endif %}
</form>
<script>
<script type="application/javascript">
$(function () {
$('[data-toggle="popover"]').popover()
@@ -108,23 +111,6 @@
return editor;
};
function selectText(node) {
if (document.body.createTextRange) {
const range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
} else if (window.getSelection) {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
} else {
console.warn("Could not select text in node: Unsupported browser.");
}
}
//converts multiselct in recipe edit to searchable multiselect
//shitty solution that needs to be redone at some point
$(document).ready(function () {
@@ -161,13 +147,14 @@
validator: "required",
editor: select2IngredientEditor
},
{title: "{% trans 'Amount' %}", field: "amount", validator: "required", editor: "input"},
{title: "{% trans 'Amount' %}", field: "amount", validator: "required", editor: "number"},
{
title: "{% trans 'Unit' %}",
field: "unit__name",
validator: "required",
editor: select2UnitEditor
},
{title: "{% trans 'Note' %}", field: "note", editor: "input"},
{
formatter: function (cell, formatterParams) {
return "<span style='color:red'><i class=\"fas fa-trash-alt\"></i></span>"
@@ -182,15 +169,6 @@
},
{title: "id", field: "id", visible: false}
],
dataEdited: function (data) {
$('#id_ingredients').val(JSON.stringify(data))
data.forEach(function (cur, i) {
if (cur.delete) {
table.deleteRow(cur.id);
}
})
},
cellClick: function (e, cell) {
if (cell._cell.column.definition.editor === "input") {
input = cell.getElement().childNodes[0];
@@ -200,17 +178,41 @@
},
});
// save ingredient data before submitting form
$('#id_form').submit(function () {
$('#id_ingredients').val(JSON.stringify(table.getData()));
return true;
});
// load initial value
$('#id_ingredients').val(JSON.stringify(data))
$('#id_ingredients').val(JSON.stringify(data));
function addIngredientRow() {
data.push({
ingredient__name: "{% trans 'Ingredient' %}",
amount: "100",
unit__name: "g",
unit__name: "{{ request.user.userpreference.default_unit }}",
note: "",
id: Math.floor(Math.random() * 10000000),
delete: false,
});
input = table.rowManager.rows[((table.rowManager.rows).length) - 1].cells[1].getElement()
input.focus();
input.select();
}
function addHeaderRow(type) {
data.push({
ingredient__name: '{% trans 'Header' %}',
amount: "0",
unit__name: "Special:Header",
note: "{% trans 'write header here' %}",
id: Math.floor(Math.random() * 10000000),
delete: false,
});
input = table.rowManager.rows[((table.rowManager.rows).length) - 1].cells[4].getElement()
input.focus();
input.select();
}
document.onkeyup = function (e) {
@@ -222,7 +224,12 @@
};
document.getElementById("new_empty").addEventListener("click", addIngredientRow);
document.getElementById("new_header").addEventListener("click", addHeaderRow);
});
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}

View File

@@ -22,21 +22,22 @@
<br/>
<h4>{% trans 'Units' %}</h4>
<form action="{% url 'edit_ingredient' %}" method="post">
<form action="{% url 'edit_ingredient' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two units ?' %}')">
{% csrf_token %}
{{ units_form|crispy }}
<button class="btn btn-danger" type="submit"
onclick="confirm('{% trans 'Are you sure that you want to merge these two units ?' %}')"><i
><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
<h4>{% trans 'Ingredients' %}</h4>
<form action="{% url 'edit_ingredient' %}" method="post">
<form action="{% url 'edit_ingredient' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two ingredients ?' %}')">
{% csrf_token %}
{{ ingredients_form|crispy }}
<button class="btn btn-danger" type="submit"
onclick="confirm('{% trans 'Are you sure that you want to merge these two ingredients ?' %}')"><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
<button class="btn btn-danger" type="submit">
<i class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
{% endblock %}

View File

@@ -13,7 +13,7 @@
<h3>{% trans 'Edit' %} {{ title }}</h3>
{% if form.Meta.model|get_class == 'Storage' %}
{% if form.Meta.model|get_class_name == 'Storage' %}
{% include 'include/storage_backend_warning.html' %}
{% endif %}
@@ -21,7 +21,7 @@
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
<a href="{% url 'redirect_delete' form.instance|get_class|lower form.instance.pk %}"
<a href="{% delete_url form.instance|get_class form.instance.pk %}"
class="btn btn-danger"><i class="fas fa-trash-alt"></i> {% trans 'Delete' %}</a>
{% if view_url %}
<a href="{{ view_url }}" class="btn btn-info"><i class="far fa-eye"></i> {% trans 'View' %}</a>

View File

@@ -15,6 +15,17 @@
</a>
{% endif %}
</h3>
{% if filter %}
<br/>
<br/>
<form action="." method="get">
{% csrf_token %}
{{ filter.form|crispy }}
<button type="submit" class="btn btn-success">{% trans 'Filter' %}</button>
</form>
{% endif %}
{% if import_btn %}
<a href="{% url 'data_batch_import' %}" class="btn btn-warning">{% trans 'Import all' %}</a>
<br/>

View File

@@ -13,7 +13,7 @@
<h3>{% trans 'New' %} {{ title }} </h3>
{% if form.Meta.model|get_class == 'Storage' %}
{% if form.Meta.model|get_class_name == 'Storage' %}
{% include 'include/storage_backend_warning.html' %}
{% endif %}

View File

@@ -66,7 +66,7 @@
{% block pagination %}
{% if table.page and table.paginator.num_pages > 1 %}
<nav aria-label="Table navigation">
<ul class="pagination justify-content-center">
<ul class="pagination justify-content-center flex-wrap">
{% if table.page.has_previous %}
{% block pagination.previous %}
<li class="previous page-item">

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "History" %}{% endblock %}
{% block extra_head %}
{% endblock %}
{% block content %}
<h3>{% trans 'History' %}</h3>
<br/>
<ul class="nav nav-tabs" id="id_tab_nav" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#view_log" role="tab" aria-controls="view_log"
aria-selected="true">{% trans 'View Log' %}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#cook_log" role="tab" aria-controls="cook_log"
aria-selected="false">{% trans 'Cook Log' %}</a>
</li>
</ul>
<div class="tab-content" id="id_tab_content">
<div class="tab-pane fade show active" id="view_log" role="tabpanel" aria-labelledby="view-log">
{% render_table view_log %}
</div>
<div class="tab-pane fade" id="cook_log" role="tabpanel" aria-labelledby="profile-tab">
{% render_table cook_log %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load static %}
{% block title %}{% trans 'Import Recipes' %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-12">
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-file-import"></i> {% trans 'Import' %}
</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,78 @@
{% load i18n %}
<div class="modal" tabindex="-1" role="dialog" id="id_modal_cook_log">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans 'Log Recipe Cooking' %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{% trans 'All fields are optional and can be left empty.' %}</p>
<form>
<label for="id_log_servings">{% trans 'Servings' %} </label>
<input class="form-control" type="number" id="id_log_servings">
<br/>
<label for="id_log_rating">{% trans 'Rating' %} - <span id="id_rating_show">0/5</span></label>
<input type="range" class="custom-range" min="0" max="5" id="id_log_rating" name="log_rating"
value="0">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans 'Close' %}</button>
<button type="button" class="btn btn-primary" onclick="logCook()">{% trans 'Save' %}</button>
</div>
</div>
</div>
</div>
<script type="application/javascript">
let modal = $('#id_modal_cook_log')
let rating = $('#id_log_rating')
function openCookLogModal(id) {
modal.data('recipe_id', id)
modal.modal('show')
}
//TODO there is definitely a nicer way to do this than this ugly shit
function logCook() {
let id = modal.data('recipe_id');
let url = "{% url 'api_log_cooking' recipe_id=12345 %}".replace(/12345/, id);
let val_servings = $('#id_log_servings').val()
if (val_servings !== '' && val_servings !== 0) {
url += '?s=' + val_servings
}
let val_rating = rating.val()
if (val_rating !== '' && val_rating !== 0) {
if (val_servings !== '' && val_servings !== 0) {
url += '&'
}else {
url += '?'
}
url += 'r=' + val_rating
}
let request = new XMLHttpRequest();
request.onreadystatechange = function () {
};
request.open("GET", url, true);
request.send();
modal.modal('hide')
}
rating.on("input", () => {
$('#id_rating_show').html(rating.val() + '/5')
});
</script>

View File

@@ -8,53 +8,81 @@
{% block extra_head %}
{{ filter.form.media }}
<style>
.dropdown-toggle-no-arrow::after {
display: none;
}
</style>
{% endblock %}
{% block content %}
{% if filter %}
<form action="" method="get" id="search_form">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="row">
<div class="col md-12">
<div class="input-group">
<input type="text" class="form-control" placeholder="{% trans 'Search recipe ...' %}"
id="{{ filter.form.name.id_for_label }}" name="{{ filter.form.name.name }}"
aria-describedby="button-addon4">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-primary" type="submit"><i class="fas fa-search"></i></button>
<button class="btn btn-warning" type="button" onclick="window.location = window.location.pathname;"><i class="fas fa-backspace"></i></button>
<button class="btn btn-success" type="button" onclick="location.href='{% url 'new_recipe' %}'"><i class="fas fa-plus-circle"></i></button>
<div class="row">
<div class="col">
<form action="" method="get" id="search_form">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="row">
<div class="col md-12">
<div class="input-group">
<input type="text" class="form-control" placeholder="{% trans 'Search recipe ...' %}"
id="{{ filter.form.name.id_for_label }}" name="{{ filter.form.name.name }}"
aria-describedby="button-addon4">
<div class="input-group-append">
<button class="btn btn-primary" type="submit"><i class="fas fa-search"></i></button>
<button type="button"
class="btn btn-light dropdown-toggle dropdown-toggle-split dropdown-toggle-no-arrow"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v"></i>
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<button class="dropdown-item" type="button"
onclick="location.href='{% url 'new_recipe' %}'"><i
class="fas fa-plus-circle fa-fw"></i> {% trans 'New Recipe' %}</button>
<button data-toggle="collapse" href="#collapse_adv_search"
role="button" class="dropdown-item"
aria-expanded="false" type="button"
aria-controls="collapse_adv_search"><i
class="fas fa-search-plus fa-fw"></i> {% trans 'Advanced Search' %}
</button>
<button class="dropdown-item" type="button"
onclick="window.location = window.location.pathname;"><i
class="fas fa-sync fa-fw"></i> {% trans 'Reset Search' %}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row ">
<div class="col-md-2 offset-md-10" style="text-align: right">
<a class="" data-toggle="collapse" href="#collapse_adv_search" role="button"
aria-expanded="false"
aria-controls="collapse_adv_search"><i
class="fas fa-search"></i> {% trans 'Advanced Search' %}</a>
</div>
</div>
<div class="row">
<div class="collapse col-md-12" id="collapse_adv_search">
<div>
{{ filter.form.keywords | as_crispy_field }}
<div class="row">
<div class="collapse col-md-12" id="collapse_adv_search">
<div style="margin-top: 1vh">
{{ filter.form.keywords | as_crispy_field }}
</div>
<div>
{{ filter.form.ingredients | as_crispy_field }}
</div>
<div>
{{ filter.form.internal | as_crispy_field }}
</div>
</div>
</div>
<div>
{{ filter.form.ingredients | as_crispy_field }}
</div>
</div>
</form>
</div>
</form>
</div>
{% endif %}
<br/>
{% if last_viewed %}
<h4>{% trans 'Last viewed' %}</h4>
{% render_table last_viewed %}
<h4>{% trans 'Recipes' %}</h4>
{% endif %}
{% if user.is_authenticated and recipes %}
{% render_table recipes %}
{% else %}
@@ -63,4 +91,5 @@
</div>
{% endif %}
{% include 'include/log_cooking.html' %}
{% endblock %}

View File

@@ -0,0 +1,189 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Markdown Info" %}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
{% endblock %}
{% block content %}
<h1>{% trans 'Markdown Info' %}</h1>
{% blocktrans %}
Markdown is lightweight markup language that can be used to format plain text easily.
This site uses the <a href="https://python-markdown.github.io/" target="_blank">Python Markdown</a> library to
convert your text into nice looking html. Its full markdown documentation can be found
<a href="https://daringfireball.net/projects/markdown/syntax" target="_blank">here</a>.
An incomplete but most likely sufficient documentation can be found below.
{% endblocktrans %}
<br/>
<br/>
<h2>{% trans 'Headers' %}</h2>
<pre class="intro-code code-block"><code>
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
</code></pre>
<div style="text-align: center">
<i class="fas fa-arrow-down fa-2x"></i>
<br/>
<br/>
</div>
<div class="card">
<div class="card-body">
<h1>Header 1</h1>
<h2>Header 2</h2>
<h3>Header 3</h3>
<h4>Header 4</h4>
<h5>Header 5</h5>
<h6>Header 6</h6>
</div>
</div>
<br/>
<h2>{% trans 'Formatting' %}</h2>
<pre class="intro-code code-block"><code>
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}
{% trans 'or by leaving a blank line inbetween.' %}
**{% trans 'This text is bold' %}**
*{% trans 'This text is in italics' %}*
> {% trans 'Blockquotes are also possible' %}
</code></pre>
<div style="text-align: center">
<i class="fas fa-arrow-down fa-2x"></i>
<br/>
<br/>
</div>
<div class="card">
<div class="card-body">
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}<br/>
{% trans 'or by leaving a blank line inbetween.' %}<br/><br/>
<b>{% trans 'This text is bold' %}</b><br/>
<i>{% trans 'This text is in italics' %}</i>
<blockquote>
<p>{% trans 'Blockquotes are also possible' %}</p>
</blockquote>
</div>
</div>
<br/>
<h2>{% trans 'Lists' %}</h2>
{% trans 'Lists can ordered or unorderd. It is <b>important to leave a blank line before the list!</b>' %}
<pre class="intro-code code-block"><code>
{% trans 'Ordered List' %}
- {% trans 'unordered list item' %}
- {% trans 'unordered list item' %}
- {% trans 'unordered list item' %}
{% trans 'Unordered List' %}
1. {% trans 'ordered list item' %}
2. {% trans 'ordered list item' %}
3. {% trans 'ordered list item' %}
</code></pre>
<div style="text-align: center">
<i class="fas fa-arrow-down fa-2x"></i>
<br/>
<br/>
</div>
<div class="card">
<div class="card-body">
{% trans 'Ordered List' %}
<ul>
<li>{% trans 'unordered list item' %}</li>
<li>{% trans 'unordered list item' %}</li>
<li>{% trans 'unordered list item' %}</li>
</ul>
{% trans 'Unordered List' %}
<ol>
<li>{% trans 'ordered list item' %}</li>
<li>{% trans 'ordered list item' %}</li>
<li>{% trans 'ordered list item' %}</li>
</ol>
</div>
</div>
<br/>
<h2>{% trans 'Images & Links' %}</h2>
{% trans 'Links can be formatted with Markdown. This applicaiton also allows to paste links directly into markdown fields without any formatting.' %}
<pre class="intro-code code-block"><code>
https://github.com/vabene1111/recipes
[](https://github.com/vabene1111/recipes)
[GitHub](https://github.com/vabene1111/recipes)
![{% trans 'This will become and Image' %}]({% static 'favicon.png' %})
</code></pre>
<div style="text-align: center">
<i class="fas fa-arrow-down fa-2x"></i>
<br/>
<br/>
</div>
<div class="card">
<div class="card-body">
<a href="https://github.com/vabene1111/recipes">https://github.com/vabene1111/recipes</a> <br/>
<a href="https://github.com/vabene1111/recipes">GitHub</a> <br/>
<img src="{% static 'favicon.png' %}" class="img-fluid" alt="{% trans 'This will become and Image' %}"
style="height: 3vw">
</div>
</div>
<br/>
<h2>{% trans 'Tables' %}</h2>
{% trans 'Markdown tables are hard to create by hand. It is recommended to use a table editor like <a href="https://www.tablesgenerator.com/markdown_tables" target="_blank">this</a> one.' %}
<pre class="intro-code code-block"><code>
| {% trans 'Table' %} | {% trans 'Header' %} |
|--------|---------|
| {% trans 'Table' %} | {% trans 'Cell' %} |
</code></pre>
<div style="text-align: center">
<i class="fas fa-arrow-down fa-2x"></i>
<br/>
<br/>
</div>
<div class="card">
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th>{% trans 'Table' %}</th>
<th>{% trans 'Header' %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans 'Table' %}</td>
<td>{% trans 'Cell' %}</td>
</tr>
</tbody>
</table>
</div>
</div>
<br/>
<br/>
{% endblock %}

View File

@@ -8,9 +8,28 @@
{% endblock %}
{% block content %}
<style>
.mealplan-cell .mealplan-add-button {
text-align: center;
display: block;
}
@media (hover: hover) {
.mealplan-cell .mealplan-add-button {
visibility: hidden;
float: right;
display: inline;
}
.mealplan-cell:hover .mealplan-add-button {
visibility: initial;
}
}
</style>
<h3>
{% trans 'Meal-Plan' %} <a href="{% url 'new_plan' %}"><i class="fas fa-plus-circle"></i></a>
{% trans 'Meal-Plan' %} <a href="{% url 'new_meal_plan' %}"><i class="fas fa-plus-circle"></i></a>
</h3>
<div class="row">
@@ -53,10 +72,18 @@
</tr>
<tr>
{% for day_key, days_value in plan_value.days.items %}
<td>
<td class="mealplan-cell">
<a class="mealplan-add-button"
href="{% url 'new_meal_plan' %}?date={{ day_key|date:'Y-m-d' }}&meal={{ plan_key }}"><i
class="fas fa-plus"></i></a>
{% for mp in days_value %}
<a href="{% url 'edit_plan' mp.pk %}"><i class="fas fa-edit"></i></a>
<a href="{% url 'view_recipe' mp.recipe.id %}">{{ mp.recipe.name }}</a><br/>
<a href="{% url 'view_plan_entry' mp.pk %}">
{% if mp.recipe %}
{{ mp.recipe }}
{% else %}
{{ mp.title }}
{% endif %}
</a><br/>
{% endfor %}
</td>
{% endfor %}

View File

@@ -0,0 +1,85 @@
{% extends "base.html" %}
{% load static %}
{% load custom_tags %}
{% load i18n %}
{% block title %}{% trans 'Meal Plan View' %}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12">
<h3>{{ plan.get_meal_name }} {{ plan.date }} <a href="{% url 'edit_meal_plan' plan.pk %}"
class="d-print-none"><i class="fas fa-pencil-alt"></i></a>
</h3>
<small class="text-muted">{% trans 'Created by' %} {{ plan.created_by.get_user_name }}</small>
{% if plan.shared.all %}
<br/><small class="text-muted">{% trans 'Shared with' %}
{% for x in plan.shared.all %}{{ x.get_user_name }}{% if not forloop.last %}, {% endif %} {% endfor %}</small>
{% endif %}
</div>
</div>
<br/>
<br/>
{% if plan.title %}
<div class="row">
<div class="col col-12">
<h4>{{ plan.title }}</h4>
</div>
</div>
{% endif %}
{% if plan.recipe %}
<div class="row">
<div class="col col-12">
<div class="card">
<div class="card-body">
{% recipe_rating plan.recipe request.user as rating %}
<h5 class="card-title"><a
href="{% url 'view_recipe' plan.recipe.pk %}">{{ plan.recipe }}</a> {{ rating|safe }}
</h5>
{% recipe_last plan.recipe request.user as last_cooked %}
{% if last_cooked %}
{% trans 'Last cooked' %} {{ last_cooked|date }}
{% else %}
{% trans 'Never cooked before.' %}
{% endif %}
{% if plan.recipe.keywords %}
<br/>
<br/>
{% for x in plan.recipe.keywords.all %}
<span class="badge badge-pill badge-light">{{ x }}</span>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% if plan.note %}
<br/>
<div class="row">
<div class="col col-12">
{{ plan.note | markdown | safe }}
</div>
</div>
{% endif %}
{% if same_day_plan %}
<br/>
<h4>{% trans 'Other meals on this day' %}</h4>
<ul class="list-group list-group-flush">
{% for x in same_day_plan %}
<li class="list-group-item"><a href="{% url 'view_plan_entry' x.pk %}">{{ x.get_label }}
({{ x.get_meal_name }})</a></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -1,18 +1,26 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load l10n %}
{% load custom_tags %}
{% block title %}{% trans 'View' %}{% endblock %}
{% block title %}{{ recipe.name }}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pretty-checkbox@3.0/dist/pretty-checkbox.min.css"
integrity="sha384-ICB8i/maQ/5+tGLDUEcswB7Ch+OO9Oj8Z4Ov/Gs0gxqfTgLLkD3F43MhcEJ2x6/D" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
<!-- prevent weired character stuff escaping the pdf box -->
<style>
/* fixes print layout being disturbed by print button tooltip */
@media print {
.tooltip {
display: none;
}
}
/* prevent weired character stuff escaping the pdf box */
.textLayer > span {
color: transparent;
position: absolute;
@@ -21,24 +29,38 @@
transform-origin: 0% 0%;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-9">
<h3>{{ recipe.name }} <a href="{% url 'edit_recipe' recipe.pk %}" class="d-print-none"><i
<div class="col col-md-8">
{% recipe_rating recipe request.user as rating %}
<h3>{{ recipe.name }} {{ rating|safe }} <a href="{% url 'edit_recipe' recipe.pk %}" class="d-print-none"><i
class="fas fa-pencil-alt"></i></a></h3>
</div>
<div class="col col-md-3 d-print-none" style="text-align: right">
<button class="btn btn-success" onclick="$('#bookmarkModal').modal({'show':true})"><i
<div class="col col-md-4 d-print-none" style="text-align: right">
<button class="btn btn-success" onclick="$('#bookmarkModal').modal({'show':true})" data-toggle="tooltip"
data-placement="top" title="{% trans 'Add to Book' %}"><i
class="fas fa-bookmark"></i></button>
{% if ingredients %}
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}"><i
<a class="btn btn-secondary" href="{% url 'view_shopping' %}?r={{ recipe.pk }}" data-toggle="tooltip"
data-placement="top" title="{% trans 'Generate shopping list' %}"><i
class="fas fa-shopping-cart"></i></a>
{% endif %}
<a class="btn btn-info" href="{% url 'new_plan' %}?recipe={{ recipe.pk }}"><i
<a class="btn btn-info" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}" data-toggle="tooltip"
data-placement="top" title="{% trans 'Add to Mealplan' %}"><i
class="fas fa-calendar"></i></a>
<button class="btn btn-warning" onclick="openCookLogModal({{ recipe.pk }})" data-toggle="tooltip"
data-placement="top" title="{% trans 'Log Cooking' %}"><i class="fas fa-clipboard-list"></i>
</button>
<a class="btn btn-light" onclick="window.print()" data-toggle="tooltip"
data-placement="top" title="{% trans 'Print' %}"><i
class="fas fa-print"></i></a>
<a class="btn btn-primary" href="{% url 'view_export' %}?r={{ recipe.pk }}" target="_blank"
data-toggle="tooltip"
data-placement="top" title="{% trans 'Export recipe' %}"><i
class="fas fa-file-export"></i></a>
</div>
</div>
@@ -48,26 +70,33 @@
{% endif %}
{% if recipe.internal %}
<small>{% trans 'by' %} {{ recipe.created_by.username }}<br/></small>
<br/>
<small>{% trans 'by' %} {{ recipe.created_by.get_user_name }}<br/></small>
{% endif %}
{% if recipe.all_tags %}
{{ recipe.all_tags }}
{% if recipe.keywords %}
{% for x in recipe.keywords.all %}
<span class="badge badge-pill badge-light">{{ x }}</span>
{% endfor %}
<br/>
<br/>
{% endif %}
{% if recipe.working_time and recipe.working_time != 0 %}
<span class="badge badge-secondary">{% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
<span class="badge badge-secondary"><i
class="fas fa-user-clock"></i> {% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
{% endif %}
{% if recipe.waiting_time and recipe.waiting_time != 0 %}
<span
class="badge badge-secondary">{% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
class="badge badge-secondary"><i
class="far fa-clock"></i> {% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
{% endif %}
{% recipe_last recipe request.user as last_cooked %}
{% if last_cooked %}
<span class="badge badge-primary">{% trans 'Last cooked' %} {{ last_cooked|date }}</span>
{% endif %}
{% if recipe.waiting_time and recipe.waiting_time != 0 or recipe.working_time and recipe.working_time != 0 %}
{% if recipe.waiting_time and recipe.waiting_time != 0 or recipe.working_time and recipe.working_time != 0 or last_cooked %}
<br/>
<br/>
{% endif %}
@@ -93,30 +122,72 @@
</div>
</div>
<br/>
<table class="">
<table class="table table-sm">
{% for i in ingredients %}
<tr>
<td style="font-size: large">
<div class="pretty p-default p-curve">
<input type="checkbox"/>
<div class="state p-success">
<label>
{% if i.amount != 0 %}
<span id="ing_{{ i.pk }}">{{ i.amount.normalize }}</span>
{{ i.unit }}
{% else %}
<span>&#x2063;</span>
{% endif %}
</label>
</div>
</div>
{% if i.unit.name == 'Special:Header' %}
<tr>
<td style="padding-top: 8px!important; ">
<b>{{ i.note }}</b>
</td>
<td>
</td>
<td style="font-size: large">{{ i.ingredient.name }}</td>
</tr>
</td>
<td></td>
</tr>
{% else %}
<tr>
<td style="vertical-align: middle!important;">
<div class="pretty p-default p-curve">
<input type="checkbox"/>
<div class="state p-success">
<label>
{% if i.amount != 0 %}
<span id="ing_{{ i.pk }}">{{ i.amount.normalize }}</span>
{{ i.unit }}
{% else %}
<span>&#x2063;</span>
{% endif %}
</label>
</div>
</div>
</td>
<td style="vertical-align: middle!important;">
{% if i.ingredient.recipe %}
<a href="{% url 'view_recipe' i.ingredient.recipe.pk %}"
target="_blank">
{% endif %}
{{ i.ingredient.name }}
{% if i.ingredient.recipe %}
</a>
{% endif %}
</td>
<td style="vertical-align: middle!important;">
{% if i.note %}
<button class="btn btn-light btn-sm d-print-none" type="button"
data-container="body"
data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
data-content="{{ i.note }}">
<i class="fas fa-info"></i>
</button>
<div class="d-none d-print-block">
<i class="far fa-comment-alt"></i> {{ i.note }}
</div>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
<!-- Bottom border -->
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<br/>
</div>
</div>
@@ -124,7 +195,7 @@
{% endif %}
{% if recipe.image %}
<div class="col-md-6 order-md-2 col-sm-12 order-sm-1 col-12 order-1 " style="text-align: center">
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2" style="text-align: center">
<img class="img img-fluid rounded" src="{{ recipe.image.url }}" style="max-height: 30vh;"
alt="{% trans 'Recipe Image' %}">
<br/>
@@ -147,8 +218,10 @@
{% if recipe.storage %}
<div class="row">
{% if recipe.internal %}
<a href='#' onClick='openRecipe({{ recipe.id }})'
class="d-print-none">{% trans 'View external recipe' %} <i class="fas fa-external-link-alt"></i></a>
<div class="col col-12" style="margin-top: 2vh">
<a href='#' onClick='openRecipe({{ recipe.id }})'
class="d-print-none">{% trans 'View external recipe' %} <i class="fas fa-external-link-alt"></i></a>
</div>
{% else %}
<div class="col col-12" style="margin-top: 2vh">
@@ -188,13 +261,9 @@
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf.min.js"
integrity="sha256-J4Z8Fhj2MITUakMQatkqOVdtqodUlwHtQ/ey6fSsudE="
crossorigin="anonymous"></script>
<script src="{% static 'js/pdf.min.js' %}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf_viewer.js"
integrity="sha256-JW7ackRikw8/UM/hHV6vKaZBYc+t2ZQ77sd3LWR8vh8="
crossorigin="anonymous"></script>
<script src="{% static 'js/pdf_viewer.js' %}"></script>
<script type="text/javascript">
var url = "{% url 'api_get_recipe_file' recipe_id=12345 %}".replace(/12345/, {{ recipe.id }});
@@ -243,7 +312,19 @@
<br/>
<br/>
<h5>{% trans 'Comments' %}</h5>
<h5 {% if not comments %}class="d-print-none" {% endif %}><i class="far fa-comments"></i> {% trans 'Comments' %}
</h5>
{% for c in comments %}
<div class="card">
<div class="card-body">
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i class="fas fa-pencil-alt"></i></a><br/>
{{ c.text }}
</div>
</div>
<br/>
{% endfor %}
<div class="d-print-none">
<form method="POST" class="post-form">
@@ -258,17 +339,6 @@
</form>
</div>
{% for c in comments %}
<div class="card">
<div class="card-body">
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i class="fas fa-pencil-alt"></i></a><br/>
{{ c.text }}
</div>
</div>
<br/>
{% endfor %}
{% if recipe.storage %}
{% include 'include/recipe_open_modal.html' %}
{% endif %}
@@ -301,8 +371,22 @@
</div>
</div>
{% include 'include/log_cooking.html' %}
<script type="text/javascript">
$(function () {
$('[data-toggle="popover"]').popover()
});
$('.popover-dismiss').popover({
trigger: 'focus'
});
function roundToTwo(num) {
return +(Math.round(num + "e+2") + "e-2");
}
function reloadIngredients() {
factor = Number($('#in_factor').val());
ingredients = {
@@ -312,9 +396,13 @@
}
for (var key in ingredients) {
$('#ing_' + key).html(Math.round(ingredients[key] * factor))
$('#ing_' + key).html(roundToTwo(ingredients[key] * factor))
}
}
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}

View File

@@ -0,0 +1,118 @@
{% load crispy_forms_tags %}
{% load i18n %}
{% load django_tables2 %}
{% load static %}
{% load custom_tags %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-container">
{% block table %}
<table {% render_attrs table.attrs class="table" %}>
{% for row in table.paginated_rows %}
<div class="card" style="margin-top: 2px;">
<div class="row no-gutters">
<div class="col-md-4">
<a href="{% url 'view_recipe' row.cells.id %}">
{% if row.cells.image|length > 1 %}
<img src=" {{ row.cells.image }}" alt="{% trans 'Recipe Image' %}"
class="card-img" style="object-fit: cover;height: 130px">
{% else %}
<img src="{% static 'recipe_no_image.svg' %}"
alt="{% trans 'Recipe Image' %}"
class="card-img d-none d-lg-block"
style="object-fit: inherit; height: 130px">
{% endif %}
</a>
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">{{ row.cells.name }}
{% recipe_rating row.record request.user as rating %}
{{ rating|safe }}
</h5>
<p class="card-text{% if not row.record.keywords %} d-none d-lg-block{% endif %}">
{% for x in row.record.keywords.all %}
<span class="badge badge-pill badge-light">{{ x }}</span>
{% endfor %}
</p>
<p class="card-text"><small class="text-muted">
{% if row.cells.working_time != 0 %}
<span class="badge badge-secondary"><i
class="fas fa-user-clock"></i> {% trans 'Preparation time ca.' %} {{ row.cells.working_time }} min </span>
{% endif %}
{% if row.cells.waiting_time != 0 %}
<span
class="badge badge-secondary"><i
class="far fa-clock"></i> {% trans 'Waiting time ca.' %} {{ row.cells.waiting_time }} min </span>
{% endif %}
{% if not row.record.internal %}
<span class="badge badge-info">{% trans 'External' %} </span>
{% endif %}
{% recipe_last row.record request.user as last_cooked %}
{% if last_cooked %}
<span class="badge badge-primary">{% trans 'Last cooked' %} {{ last_cooked|date }}</span>
{% endif %}
<span class="badge badge-light">{{ row.cells.edit }}</span>
<span class="badge badge-warning"><a href="#" style="color: inherit"
onclick="openCookLogModal({{ row.record.pk }})">{% trans 'Log' %}</a></span>
</small></p>
</div>
</div>
</div>
</div>
{% endfor %}
</table>
{% endblock table %}
</div>
</div>
</div>
{% block pagination %}
{% if table.page and table.paginator.num_pages > 1 %}
<nav aria-label="Table navigation">
<ul class="pagination justify-content-center flex-wrap">
{% if table.page.has_previous %}
{% block pagination.previous %}
<li class="previous page-item">
<a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
class="page-link">
<span aria-hidden="true">&laquo;</span>
{% trans 'previous' %}
</a>
</li>
{% endblock pagination.previous %}
{% endif %}
{% if table.page.has_previous or table.page.has_next %}
{% block pagination.range %}
{% for p in table.page|table_page_range:table.paginator %}
<li class="page-item{% if table.page.number == p %} active{% endif %}">
<a class="page-link"
{% if p != '...' %}href="{% querystring table.prefixed_page_field=p %}"{% endif %}>
{{ p }}
</a>
</li>
{% endfor %}
{% endblock pagination.range %}
{% endif %}
{% if table.page.has_next %}
{% block pagination.next %}
<li class="next page-item">
<a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"
class="page-link">
{% trans 'next' %}
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endblock pagination.next %}
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock pagination %}
{% endblock content %}

View File

@@ -5,6 +5,11 @@
{% block title %}{% trans 'Settings' %}{% endblock %}
{% block extra_head %}
{{ preference_form.media }}
{% endblock %}
{% block content %}
<h3>
@@ -14,7 +19,24 @@
<br/>
<br/>
<h4><i class="fas fa-language"></i> {% trans 'Language' %}</h4>
<h4><i class="fas fa-user-edit fa-fw"></i> {% trans 'Account' %}</h4>
<form action="." method="post">
{% csrf_token %}
{{ user_name_form|crispy }}
<button class="btn btn-success" type="submit" name="user_name_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<form action="." method="post">
{% csrf_token %}
{{ password_form|crispy }}
<button class="btn btn-success" type="submit" name="password_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<br/>
<br/>
<h4><i class="fas fa-language fa-fw"></i> {% trans 'Language' %}</h4>
<div class="row">
<div class="col-md-12">
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
@@ -39,12 +61,12 @@
<br/>
<br/>
<h4><i class="fas fa-palette"></i>{% trans 'Style' %}</h4>
<h4><i class="fas fa-palette fa-fw"></i> {% trans 'Style' %}</h4>
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
{{ preference_form|crispy }}
<button class="btn btn-success" type="submit" name="preference_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Cookbook Setup" %}{% endblock %}
{% block extra_head %}
{{ form.media }}
{% endblock %}
{% block content %}
<h1>{% trans 'Setup' %}</h1>
<p>{% blocktrans %}To start using this application you must first create a superuser.{% endblocktrans %}</p>
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create Superuser' %}</button>
</form>
{% endblock %}

View File

@@ -26,7 +26,7 @@
<div class="row">
<div class="col col-md-12">
<!--// @formatter:off-->
<textarea id="id_list" class="form-control" rows="{{ ingredients|length|add:1 }}">{% for i in ingredients %}{% if markdown_format %}- [ ]{% endif %} {{ i.amount.normalize }} {{ i.unit }} {{ i.ingredient.name }}&#10;{% endfor %}</textarea>
<textarea id="id_list" class="form-control" rows="{{ ingredients|length|add:1 }}">{% for i in ingredients %}{% if markdown_format %}- [ ] {% endif %}{{ i.amount.normalize }} {{ i.unit }} {{ i.ingredient.name }}&#13;&#10;{% endfor %}</textarea>
<!--// @formatter:on-->
</div>
</div>
@@ -34,7 +34,7 @@
<div class="row">
<div class="col col-md-12 text-center">
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
data-placement="top" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
data-placement="right" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
class="far fa-copy"></i></button>
</div>
</div>

View File

@@ -5,10 +5,12 @@
{% block content %}
<div class="row">
<div class="col col-12">
<h3>{% trans 'Statistics' %} </h3>
</div>
</div>
<h3>
{% trans 'Statistics' %}
</h3>
<div class="row">
<div class="col-md-6">
@@ -21,6 +23,10 @@
class="badge badge-pill badge-info">{{ counts.recipes }}</span></li>
<li class="list-group-item">{% trans 'Keywords' %} : <span
class="badge badge-pill badge-info">{{ counts.keywords }}</span></li>
<li class="list-group-item">{% trans 'Units' %} : <span
class="badge badge-pill badge-info">{{ counts.units }}</span></li>
<li class="list-group-item">{% trans 'Ingredients' %} : <span
class="badge badge-pill badge-info">{{ counts.ingredients }}</span></li>
<li class="list-group-item">{% trans 'Recipe Imports' %} : <span
class="badge badge-pill badge-info">{{ counts.recipe_import }}</span></li>
</ul>
@@ -33,8 +39,13 @@
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{% trans 'Recipes without Keywords' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_no_keyword}}</span></li>
class="badge badge-pill badge-info">{{ counts.recipes_no_keyword }}</span></li>
<li class="list-group-item">{% trans 'External Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_external }}</span></li>
<li class="list-group-item">{% trans 'Internal Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_internal }}</span></li>
<li class="list-group-item">{% trans 'Comments' %} : <span
class="badge badge-pill badge-info">{{ counts.comments }}</span></li>
</ul>
</div>
</div>

View File

@@ -1,24 +0,0 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<form>
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp"
placeholder="Enter email">
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Check me out</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}

View File

@@ -1,19 +1,61 @@
from django import template
import markdown as md
import bleach
import markdown as md
from bleach_whitelist import markdown_tags, markdown_attrs
from django import template
from django.db.models import Avg
from django.urls import reverse
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import get_model_name, CookLog
register = template.Library()
@register.filter(name='get_class')
def get_class(value):
@register.filter()
def get_class_name(value):
return value.__class__.__name__
@register.filter()
def get_class(value):
return value.__class__
@register.simple_tag
def delete_url(model, pk):
return reverse(f'delete_{get_model_name(model)}', args=[pk])
@register.filter()
def markdown(value):
return bleach.clean(md.markdown(value, extensions=['markdown.extensions.fenced_code']), markdown_tags, markdown_attrs)
tags = markdown_tags + ['pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead']
parsed_md = md.markdown(value, extensions=['markdown.extensions.fenced_code', 'tables', UrlizeExtension(), MarkdownFormatExtension()])
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
return bleach.clean(parsed_md, tags, markdown_attrs)
@register.simple_tag
def recipe_rating(recipe, user):
rating = recipe.cooklog_set.filter(created_by=user).aggregate(Avg('rating'))
if rating['rating__avg']:
rating_stars = ''
for i in range(int(rating['rating__avg'])):
rating_stars = rating_stars + '<i class="fas fa-star fa-xs"></i>'
if rating['rating__avg'] % 1 >= 0.5:
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>'
return rating_stars
else:
return ''
@register.simple_tag
def recipe_last(recipe, user):
last = recipe.cooklog_set.filter(created_by=user).last()
if last:
return last.created_at
else:
return ''

View File

@@ -8,41 +8,38 @@ register = template.Library()
@register.simple_tag
def theme_url(request):
try:
themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
UserPreference.FLATLY: 'themes/flatly.min.css',
UserPreference.DARKLY: 'themes/darkly.min.css',
UserPreference.SUPERHERO: 'themes/superhero.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
except AttributeError:
if not request.user.is_authenticated:
return static('themes/flatly.min.css')
themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
UserPreference.FLATLY: 'themes/flatly.min.css',
UserPreference.DARKLY: 'themes/darkly.min.css',
UserPreference.SUPERHERO: 'themes/superhero.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
@register.simple_tag
def nav_color(request):
try:
return request.user.userpreference.nav_color
except AttributeError:
if not request.user.is_authenticated:
return 'primary'
return request.user.userpreference.nav_color
@register.simple_tag
def tabulator_theme_url(request):
try:
themes = {
UserPreference.BOOTSTRAP: 'tabulator/tabulator_bootstrap4.min.css',
UserPreference.FLATLY: 'tabulator/tabulator_bootstrap4.min.css',
UserPreference.DARKLY: 'tabulator/tabulator_site.min.css',
UserPreference.SUPERHERO: 'tabulator/tabulator_site.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
except AttributeError:
if not request.user.is_authenticated:
return static('tabulator/tabulator_bootstrap4.min.css')
themes = {
UserPreference.BOOTSTRAP: 'tabulator/tabulator_bootstrap4.min.css',
UserPreference.FLATLY: 'tabulator/tabulator_bootstrap4.min.css',
UserPreference.DARKLY: 'tabulator/tabulator_site.min.css',
UserPreference.SUPERHERO: 'tabulator/tabulator_site.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError

View File

View File

@@ -0,0 +1,49 @@
from django.contrib import auth
from django.urls import reverse
from cookbook.models import Comment, Recipe
from cookbook.tests.views.test_views import TestViews
class TestEditsComment(TestViews):
def setUp(self):
super(TestEditsComment, self).setUp()
self.recipe = Recipe.objects.create(
internal=True,
instructions='Do something',
working_time=1,
waiting_time=1,
created_by=auth.get_user(self.user_client_1)
)
self.comment = Comment.objects.create(
text='TestStorage',
created_by=auth.get_user(self.guest_client_1),
recipe=self.recipe
)
self.url = reverse('edit_comment', args=[self.comment.pk])
def test_new_comment(self):
r = self.user_client_1.post(reverse('view_recipe', args=[self.recipe.pk]), {'comment-text': 'Test Comment Text', 'comment-recipe': self.recipe.pk})
self.assertEqual(r.status_code, 200)
def test_edit_comment_permissions(self):
r = self.anonymous_client.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.guest_client_1.get(self.url)
self.assertEqual(r.status_code, 200)
r = self.guest_client_2.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.user_client_1.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.admin_client_1.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.superuser_client.get(self.url)
self.assertEqual(r.status_code, 200)

View File

@@ -0,0 +1,128 @@
from django.contrib import auth
from django.urls import reverse
from cookbook.models import Recipe, RecipeIngredient, Ingredient, Unit, Storage
from cookbook.tests.views.test_views import TestViews
class TestEditsRecipe(TestViews):
def test_switch_recipe(self):
internal_recipe = Recipe.objects.create(
name='Test',
internal=True,
created_by=auth.get_user(self.user_client_1)
)
external_recipe = Recipe.objects.create(
name='Test',
internal=False,
created_by=auth.get_user(self.user_client_1)
)
url = reverse('edit_recipe', args=[internal_recipe.pk])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 302)
r = self.user_client_1.get(r.url)
self.assertTemplateUsed(r, 'forms/edit_internal_recipe.html')
url = reverse('edit_recipe', args=[external_recipe.pk])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 302)
r = self.user_client_1.get(r.url)
self.assertTemplateUsed(r, 'generic/edit_template.html')
def test_convert_recipe(self):
url = reverse('edit_convert_recipe', args=[42])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 404)
external_recipe = Recipe.objects.create(
name='Test',
internal=False,
created_by=auth.get_user(self.user_client_1)
)
url = reverse('edit_convert_recipe', args=[external_recipe.pk])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 302)
recipe = Recipe.objects.get(pk=external_recipe.pk)
self.assertTrue(recipe.internal)
url = reverse('edit_convert_recipe', args=[recipe.pk])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 302)
def test_internal_recipe_update(self):
recipe = Recipe.objects.create(
name='Test',
created_by=auth.get_user(self.user_client_1)
)
url = reverse('edit_internal_recipe', args=[recipe.pk])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
r = self.user_client_1.post(url, {'name': 'Changed', 'working_time': 15, 'waiting_time': 15, 'ingredients': '[]'})
self.assertEqual(r.status_code, 200)
recipe = Recipe.objects.get(pk=recipe.pk)
self.assertEqual('Changed', recipe.name)
Ingredient.objects.create(name='Egg')
Unit.objects.create(name='g')
r = self.user_client_1.post(url,
{'name': 'Changed', 'working_time': 15, 'waiting_time': 15,
'ingredients': '[{"ingredient__name":"Tomato","unit__name":"g","amount":100,"delete":false},{"ingredient__name":"Egg","unit__name":"Piece","amount":"2,5","delete":false}]'})
self.assertEqual(r.status_code, 200)
self.assertEqual(2, RecipeIngredient.objects.filter(recipe=recipe).count())
r = self.user_client_1.post(url,
{'name': "Test", 'working_time': "Test", 'waiting_time': 15,
'ingredients': '[{"ingredient__name":"Tomato","unit__name":"g","amount":100,"delete":false},{"ingredient__name":"Egg","unit__name":"Piece","amount":"2,5","delete":false}]'})
self.assertEqual(r.status_code, 403)
with open('cookbook/tests/resources/image.jpg', 'rb') as file:
r = self.user_client_1.post(url, {'name': "Changed", 'working_time': 15, 'waiting_time': 15, 'image': file})
self.assertEqual(r.status_code, 200)
with open('cookbook/tests/resources/image.png', 'rb') as file:
r = self.user_client_1.post(url, {'name': "Changed", 'working_time': 15, 'waiting_time': 15, 'image': file})
self.assertEqual(r.status_code, 200)
def test_external_recipe_update(self):
storage = Storage.objects.create(
name='TestStorage',
method=Storage.DROPBOX,
created_by=auth.get_user(self.user_client_1),
token='test',
username='test',
password='test',
)
recipe = Recipe.objects.create(
name='Test',
created_by=auth.get_user(self.user_client_1),
storage=storage,
)
url = reverse('edit_external_recipe', args=[recipe.pk])
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
r = self.user_client_1.post(url, {'name': 'Test', 'working_time': 15, 'waiting_time': 15, })
recipe.refresh_from_db()
self.assertEqual(recipe.working_time, 15)
self.assertEqual(recipe.waiting_time, 15)

View File

@@ -0,0 +1,46 @@
from django.contrib import auth
from django.urls import reverse
from cookbook.models import Storage
from cookbook.tests.views.test_views import TestViews
class TestEditsRecipe(TestViews):
def setUp(self):
super(TestEditsRecipe, self).setUp()
self.storage = Storage.objects.create(
name='TestStorage',
method=Storage.DROPBOX,
created_by=auth.get_user(self.admin_client_1),
token='test',
username='test',
password='test',
)
self.url = reverse('edit_storage', args=[self.storage.pk])
def test_edit_storage(self):
r = self.admin_client_1.post(self.url, {'name': 'NewStorage', 'password': '1234_pw', 'token': '1234_token', 'method': Storage.DROPBOX})
self.storage.refresh_from_db()
self.assertEqual(self.storage.password, '1234_pw')
self.assertEqual(self.storage.token, '1234_token')
r = self.admin_client_1.post(self.url, {'name': 'NewStorage', 'password': '1234_pw', 'token': '1234_token', 'method': 'not_a_valid_method'})
self.assertFormError(r, 'form', 'method', ['Select a valid choice. not_a_valid_method is not one of the available choices.'])
def test_edit_storage_permissions(self):
r = self.anonymous_client.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.guest_client_1.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.user_client_1.get(self.url)
self.assertEqual(r.status_code, 302)
r = self.admin_client_1.get(self.url)
self.assertEqual(r.status_code, 200)
r = self.superuser_client.get(self.url)
self.assertEqual(r.status_code, 200)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,38 @@
from django.contrib import auth
from django.contrib.auth.models import User, Group
from django.test import TestCase, Client
class TestBase(TestCase):
user_client_1 = None
user_client_2 = None
admin_client_1 = None
admin_client_2 = None
guest_client_1 = None
guest_client_2 = None
superuser_client = None
def create_login_user(self, name, group):
client = Client()
setattr(self, name, client)
client.force_login(User.objects.get_or_create(username=name)[0])
user = auth.get_user(getattr(self, name))
user.groups.add(Group.objects.get(name=group))
self.assertTrue(user.is_authenticated)
return user
def setUp(self):
self.create_login_user('admin_client_1', 'admin')
self.create_login_user('admin_client_2', 'admin')
self.create_login_user('user_client_1', 'user')
self.create_login_user('user_client_2', 'user')
self.create_login_user('guest_client_1', 'guest')
self.create_login_user('guest_client_2', 'guest')
self.anonymous_client = Client()
user = self.create_login_user('superuser_client', 'admin')
user.is_superuser = True
user.save()

View File

@@ -1,73 +0,0 @@
from django.contrib import auth
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.urls import reverse
from cookbook.models import Recipe, RecipeIngredient
class TestViews(TestCase):
def setUp(self):
self.client = Client()
self.anonymous_client = Client()
self.client.force_login(User.objects.get_or_create(username='test')[0])
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated)
def test_index(self):
r = self.client.get(reverse('index'))
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(reverse('index'))
self.assertEqual(r.status_code, 200)
def test_books(self):
url = reverse('view_books')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_plan(self):
url = reverse('view_plan')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_shopping(self):
url = reverse('view_shopping')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_internal_recipe_update(self):
recipe = Recipe.objects.create(
name='Test',
created_by=auth.get_user(self.client)
)
url = reverse('edit_internal_recipe', args=[recipe.pk])
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
r = self.client.post(url, {'name': 'Changed', 'working_time': 15, 'waiting_time': 15, 'ingredients': '[]'})
self.assertEqual(r.status_code, 200)
recipe = Recipe.objects.get(pk=recipe.pk)
self.assertEqual('Changed', recipe.name)
r = self.client.post(url,
{'name': 'Changed', 'working_time': 15, 'waiting_time': 15,
'ingredients': '[{"ingredient__name":"Tomato","unit__name":"g","amount":100,"delete":false},{"ingredient__name":"Egg","unit__name":"Piece","amount":2,"delete":false}]'})
self.assertEqual(r.status_code, 200)
self.assertEqual(2, RecipeIngredient.objects.filter(recipe=recipe).count())

View File

View File

@@ -0,0 +1,5 @@
from cookbook.tests.test_setup import TestBase
class TestViews(TestBase):
pass

View File

@@ -0,0 +1,37 @@
from django.contrib import auth
from django.urls import reverse
from cookbook.models import Recipe
from cookbook.tests.views.test_views import TestViews
class TestViewsApi(TestViews):
def test_external_link_permission(self):
recipe = Recipe.objects.create(
internal=False,
link='test',
instructions='Do something',
working_time=1,
waiting_time=1,
created_by=auth.get_user(self.user_client_1)
)
url = reverse('api_get_external_file_link', args=[recipe.pk])
self.assertEqual(self.anonymous_client.get(url).status_code, 302)
self.assertEqual(self.guest_client_1.get(url).status_code, 302)
self.assertEqual(self.user_client_1.get(url).status_code, 200)
self.assertEqual(self.admin_client_1.get(url).status_code, 200)
self.assertEqual(self.superuser_client.get(url).status_code, 200)
def test_file_permission(self):
url = reverse('api_get_recipe_file', args=[1])
self.assertEqual(self.anonymous_client.get(url).status_code, 302)
self.assertEqual(self.guest_client_1.get(url).status_code, 302)
def test_sync_permission(self):
url = reverse('api_sync')
self.assertEqual(self.anonymous_client.get(url).status_code, 302)
self.assertEqual(self.guest_client_1.get(url).status_code, 302)

View File

@@ -0,0 +1,37 @@
from django.urls import reverse
from cookbook.tests.views.test_views import TestViews
class TestViewsGeneral(TestViews):
def test_index(self):
r = self.user_client_1.get(reverse('index'))
self.assertEqual(r.status_code, 302)
r = self.anonymous_client.get(reverse('index'))
self.assertEqual(r.status_code, 302)
def test_books(self):
url = reverse('view_books')
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_plan(self):
url = reverse('view_plan')
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_shopping(self):
url = reverse('view_shopping')
r = self.user_client_1.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)

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