mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 03:13:13 -05:00
Compare commits
417 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad1e64fb9a | ||
|
|
add600f3ca | ||
|
|
ad036d7e6c | ||
|
|
d7017902ab | ||
|
|
35743e8be9 | ||
|
|
75523c06f6 | ||
|
|
7e2aee53db | ||
|
|
9c74730461 | ||
|
|
977d2822bc | ||
|
|
31f93285d8 | ||
|
|
da9002a7fd | ||
|
|
1bf7af7027 | ||
|
|
1145a8cf26 | ||
|
|
5e918297f8 | ||
|
|
69adad70c8 | ||
|
|
d3905f1e80 | ||
|
|
1a1ff52725 | ||
|
|
731958fdaa | ||
|
|
8a19c8eeb0 | ||
|
|
4e7368f7b6 | ||
|
|
b00f1009a6 | ||
|
|
2d3ecaaf3c | ||
|
|
339049c785 | ||
|
|
d0481ed18c | ||
|
|
bcee66c7a4 | ||
|
|
7e993ca50e | ||
|
|
638dc845c0 | ||
|
|
4aea0fea8c | ||
|
|
2ba94df9a8 | ||
|
|
5723d87768 | ||
|
|
24b1f4028f | ||
|
|
984c863ff6 | ||
|
|
0f207c2fa7 | ||
|
|
58e70c982e | ||
|
|
4cb94a1759 | ||
|
|
3e568f7bb5 | ||
|
|
b69c6bc97a | ||
|
|
9132ab8f33 | ||
|
|
45858d5107 | ||
|
|
1e332977c5 | ||
|
|
7b70ffab5f | ||
|
|
9b367e5d08 | ||
|
|
243cac0389 | ||
|
|
8205812c84 | ||
|
|
94279b74c9 | ||
|
|
8a588db429 | ||
|
|
5150807ab7 | ||
|
|
225ddc8eeb | ||
|
|
a6965fb3c4 | ||
|
|
90354305c4 | ||
|
|
220d98a85c | ||
|
|
7fb4155ebe | ||
|
|
a39e6e8a6a | ||
|
|
97fc15ded3 | ||
|
|
8bee2e3976 | ||
|
|
e89c1742fb | ||
|
|
6680fbb644 | ||
|
|
4cec643e08 | ||
|
|
03bd51893e | ||
|
|
66b0e381ec | ||
|
|
fd70adf19d | ||
|
|
eb8422cb51 | ||
|
|
1008d880c9 | ||
|
|
9cb1c21cd8 | ||
|
|
5149cb0609 | ||
|
|
08adf4eb6f | ||
|
|
62f38d00f3 | ||
|
|
43a55c8c82 | ||
|
|
d09eb64a41 | ||
|
|
8bbbc1b9ef | ||
|
|
cc367bfed2 | ||
|
|
b18aa831ac | ||
|
|
6205fbe1c4 | ||
|
|
879a54524c | ||
|
|
36678692be | ||
|
|
de6285e5f8 | ||
|
|
7ff7409f56 | ||
|
|
50b3636c86 | ||
|
|
1f72a3f62f | ||
|
|
aea796bd6d | ||
|
|
edcddc3183 | ||
|
|
45a24a4720 | ||
|
|
bed95105f3 | ||
|
|
a39fdb4226 | ||
|
|
9d629b03b3 | ||
|
|
4eeb87cb95 | ||
|
|
3ce7e43b46 | ||
|
|
a3a995ef77 | ||
|
|
a386b45a03 | ||
|
|
74bd2ba2c0 | ||
|
|
695f467126 | ||
|
|
6270b46951 | ||
|
|
a3ad131e6a | ||
|
|
e2d5287cc6 | ||
|
|
1c39d8089c | ||
|
|
2230b9e9ab | ||
|
|
339d7b1c96 | ||
|
|
4e8c955555 | ||
|
|
221c466c18 | ||
|
|
2c8e029811 | ||
|
|
019825bfcb | ||
|
|
8e5ea47d5e | ||
|
|
425ac7f379 | ||
|
|
f0caef4759 | ||
|
|
c56a76f264 | ||
|
|
429886e6a6 | ||
|
|
339ab57df7 | ||
|
|
cb6d98a357 | ||
|
|
e746b44f3b | ||
|
|
bc63ba6713 | ||
|
|
ea8661ab03 | ||
|
|
bc9a5c9435 | ||
|
|
365ffa29fa | ||
|
|
b3aeee6a63 | ||
|
|
d503dc77c3 | ||
|
|
fee364ee4a | ||
|
|
680a8d0fce | ||
|
|
0944d72e32 | ||
|
|
6809ded468 | ||
|
|
64d07a65dc | ||
|
|
745abb57a8 | ||
|
|
dfe5083451 | ||
|
|
9377e208e8 | ||
|
|
40f38e6c6d | ||
|
|
3ee0717d84 | ||
|
|
47155ce338 | ||
|
|
611080b739 | ||
|
|
416d1badda | ||
|
|
0ef5d3ad92 | ||
|
|
efb8784b91 | ||
|
|
a1a6f476e0 | ||
|
|
ccd0667f04 | ||
|
|
38cf825816 | ||
|
|
a8dc8e7190 | ||
|
|
76aca6cf38 | ||
|
|
89c31a018f | ||
|
|
e54f55b6d0 | ||
|
|
fff7cb607c | ||
|
|
54c2478869 | ||
|
|
a7795092b3 | ||
|
|
538fb8b42e | ||
|
|
1f0cd58d7d | ||
|
|
78b1386a1c | ||
|
|
aba7f8db5c | ||
|
|
d7fadffbfd | ||
|
|
22c7f5d85d | ||
|
|
c18d8daece | ||
|
|
d91c4b33f3 | ||
|
|
2ad6f21b9c | ||
|
|
554170a84e | ||
|
|
d43a6e551d | ||
|
|
02cb6d1be7 | ||
|
|
45b1eca48b | ||
|
|
6dacd44f1f | ||
|
|
1b97472368 | ||
|
|
d467352029 | ||
|
|
a0256b607e | ||
|
|
847fceaf10 | ||
|
|
9e831a22df | ||
|
|
768a5ea237 | ||
|
|
36403ecbae | ||
|
|
4620ebaf30 | ||
|
|
c907da84c1 | ||
|
|
9b5e39415e | ||
|
|
2679a22464 | ||
|
|
8bae21025b | ||
|
|
4120adc546 | ||
|
|
30c891abfc | ||
|
|
b8317c2c29 | ||
|
|
39253cfd02 | ||
|
|
7c0b8b151c | ||
|
|
8e1b8923af | ||
|
|
52eb876a08 | ||
|
|
a820b9c09e | ||
|
|
bcfe6ca707 | ||
|
|
4aa3e04df0 | ||
|
|
f8f08ae337 | ||
|
|
c8fc6b5237 | ||
|
|
52909e8117 | ||
|
|
c72bf57ccb | ||
|
|
d3c21cf97f | ||
|
|
942edd9336 | ||
|
|
8fa6c98254 | ||
|
|
73c6bfce44 | ||
|
|
c105909933 | ||
|
|
13baf4f30a | ||
|
|
da5fd16338 | ||
|
|
83a52bd204 | ||
|
|
fe4bd6a127 | ||
|
|
d193d91e6a | ||
|
|
a2f9ef2e74 | ||
|
|
3f63eab68c | ||
|
|
9b6ed7a63a | ||
|
|
e2f8efb521 | ||
|
|
ce29283a52 | ||
|
|
dcf9d59b06 | ||
|
|
794f9cf5b9 | ||
|
|
9954bb9410 | ||
|
|
e57be4a704 | ||
|
|
ffaecc066f | ||
|
|
94f398a7f6 | ||
|
|
65d670a995 | ||
|
|
15ed040533 | ||
|
|
5d3f44ffee | ||
|
|
9ee4be621b | ||
|
|
d33b0d2254 | ||
|
|
1a20c4bef5 | ||
|
|
b350ab1b59 | ||
|
|
687e8a1f6a | ||
|
|
64b9605871 | ||
|
|
8320473606 | ||
|
|
88228ab853 | ||
|
|
dcfb269909 | ||
|
|
4a1ec5adf7 | ||
|
|
56cdc14cc1 | ||
|
|
b8959036bf | ||
|
|
ab24177c89 | ||
|
|
4ffc9cc72f | ||
|
|
7f62ec28e3 | ||
|
|
d42d784aeb | ||
|
|
ce84b3b385 | ||
|
|
74fbcb03a1 | ||
|
|
b1aa70787c | ||
|
|
8675143cc1 | ||
|
|
75e23106fc | ||
|
|
2ad89b5b22 | ||
|
|
36074c9c35 | ||
|
|
05560c5730 | ||
|
|
6ba4db6ff9 | ||
|
|
6353885f9c | ||
|
|
833ebf8c0c | ||
|
|
0662255b27 | ||
|
|
fde4ea8c4c | ||
|
|
132815496c | ||
|
|
a7a6abe3d2 | ||
|
|
2f617aa40f | ||
|
|
9b50ea4c22 | ||
|
|
cde8dd8b53 | ||
|
|
8411537f87 | ||
|
|
479cf1a042 | ||
|
|
8fa00972bd | ||
|
|
5d5eb45b5a | ||
|
|
87beed48c9 | ||
|
|
cf7cc6c637 | ||
|
|
3d45a068e4 | ||
|
|
01ce658883 | ||
|
|
92d648c3a3 | ||
|
|
17fa3c8d7c | ||
|
|
c1ae4e3905 | ||
|
|
d819cbc20e | ||
|
|
f255397bbd | ||
|
|
2f0929e90e | ||
|
|
6785033a21 | ||
|
|
0345b7720c | ||
|
|
7163c33b2a | ||
|
|
934df3c5f7 | ||
|
|
2888b18819 | ||
|
|
c01081255b | ||
|
|
2e606dc166 | ||
|
|
835c5a1d3a | ||
|
|
8580aea43f | ||
|
|
db4f2db236 | ||
|
|
7e9cef6075 | ||
|
|
75612781da | ||
|
|
f5fb4e563d | ||
|
|
1ecb57e795 | ||
|
|
c4a0df26fc | ||
|
|
8ff5142149 | ||
|
|
716976453a | ||
|
|
f07dec6062 | ||
|
|
ffc96890ac | ||
|
|
a8fd703d1d | ||
|
|
4592cc85a5 | ||
|
|
4a835c38d8 | ||
|
|
ef72a07acb | ||
|
|
246b9c4a02 | ||
|
|
c18a77bc9b | ||
|
|
3d7e2b1aa5 | ||
|
|
28f18fbc42 | ||
|
|
ba361a8a27 | ||
|
|
fc2ce6e488 | ||
|
|
d7f77a572a | ||
|
|
64e28fd01a | ||
|
|
714d5e5184 | ||
|
|
640500c82d | ||
|
|
8bf661c1ab | ||
|
|
1d29e435d5 | ||
|
|
6eac48633b | ||
|
|
743fae1ba7 | ||
|
|
b3565451ff | ||
|
|
4a93681870 | ||
|
|
d83b0484d8 | ||
|
|
c0d67dbc58 | ||
|
|
3a8ea4b4c9 | ||
|
|
4b14a099df | ||
|
|
dae7cbfb85 | ||
|
|
0c62b80e3a | ||
|
|
678963e6dd | ||
|
|
6d84c718fd | ||
|
|
b8e1ed8967 | ||
|
|
d87633433a | ||
|
|
fe33adbba0 | ||
|
|
baa84cf481 | ||
|
|
ecd828008e | ||
|
|
2b8c607b78 | ||
|
|
df684f591a | ||
|
|
cb5b51bde3 | ||
|
|
7f27419215 | ||
|
|
312cd077d0 | ||
|
|
eac059ca85 | ||
|
|
782dd4cb17 | ||
|
|
f7b60f2c52 | ||
|
|
ca28e52698 | ||
|
|
0c2c12d536 | ||
|
|
113c40c243 | ||
|
|
0688f46d8b | ||
|
|
2fdcdba889 | ||
|
|
6a39148e5f | ||
|
|
22dfb40fd5 | ||
|
|
2b5a86ce53 | ||
|
|
e77016ea9b | ||
|
|
9988a61da7 | ||
|
|
f34fb8eec3 | ||
|
|
7853357065 | ||
|
|
6f1befc43c | ||
|
|
c18386b9b5 | ||
|
|
d5ba2e6716 | ||
|
|
b30f8c245e | ||
|
|
74c86f1b6b | ||
|
|
cf9d599536 | ||
|
|
14a67fd6c2 | ||
|
|
19f1225249 | ||
|
|
7f33f82b60 | ||
|
|
6880c0a967 | ||
|
|
814f4157db | ||
|
|
0f5e53526e | ||
|
|
413da01c5c | ||
|
|
a73d231bd4 | ||
|
|
4f2392faac | ||
|
|
2321dcec6c | ||
|
|
c2cf7ba758 | ||
|
|
239dd4aa60 | ||
|
|
a653b2e777 | ||
|
|
d8faee7e93 | ||
|
|
69417425e9 | ||
|
|
e8574a49a7 | ||
|
|
fe624cd218 | ||
|
|
1f10a66c74 | ||
|
|
a8f1cd26cd | ||
|
|
a497a6b7f5 | ||
|
|
9dc144f2b5 | ||
|
|
7d50f3cf21 | ||
|
|
315af4911c | ||
|
|
35704c69c7 | ||
|
|
a24628c771 | ||
|
|
e9748a160a | ||
|
|
7bc78e104f | ||
|
|
6f0dccfec9 | ||
|
|
76d6981dab | ||
|
|
5df37c52dd | ||
|
|
c78b7a6928 | ||
|
|
7a2ccc075c | ||
|
|
237054c23e | ||
|
|
ac1d641bd5 | ||
|
|
3545b6e98a | ||
|
|
d3a56e00ea | ||
|
|
e9f8578c25 | ||
|
|
dccfc436be | ||
|
|
1e85c8587b | ||
|
|
b8f92ab054 | ||
|
|
766ed31f8e | ||
|
|
cad78e115d | ||
|
|
c2def3eb9d | ||
|
|
ad7ebf1cd5 | ||
|
|
b599c4f6a9 | ||
|
|
439539f56d | ||
|
|
237bcb92c9 | ||
|
|
ce02a23dbb | ||
|
|
8e81512735 | ||
|
|
c69f0394a8 | ||
|
|
d7ca9e05de | ||
|
|
64534ff810 | ||
|
|
d0164a6c28 | ||
|
|
0f898ddf4a | ||
|
|
e903382034 | ||
|
|
0d225450da | ||
|
|
c077a64484 | ||
|
|
6c16094b42 | ||
|
|
5aa80746f9 | ||
|
|
cc64717818 | ||
|
|
6acd892116 | ||
|
|
3955408aa4 | ||
|
|
3de2468df3 | ||
|
|
b1d983fbc3 | ||
|
|
5f443d2593 | ||
|
|
436158f596 | ||
|
|
dcc56fc138 | ||
|
|
0eef10079b | ||
|
|
2b839dfb19 | ||
|
|
491b678d6e | ||
|
|
151dce006d | ||
|
|
d4f538b4aa | ||
|
|
a727439c57 | ||
|
|
ac17b84a7a | ||
|
|
9756b7b653 | ||
|
|
ee38d93e3b | ||
|
|
ee5c7d0ef4 | ||
|
|
991a51d55e | ||
|
|
6c9227faac | ||
|
|
693b43af2e | ||
|
|
4fb5ce550e | ||
|
|
497321799c | ||
|
|
78f1ee175b | ||
|
|
4a390b5824 | ||
|
|
785dc15cd9 | ||
|
|
31f3425354 | ||
|
|
689eb426ea |
@@ -3,7 +3,6 @@ npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
|
||||
@@ -13,9 +13,18 @@ DEBUG_TOOLBAR=0
|
||||
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
|
||||
ALLOWED_HOSTS=*
|
||||
|
||||
# Cross Site Request Forgery protection
|
||||
# (https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS)
|
||||
# CSRF_TRUSTED_ORIGINS = []
|
||||
|
||||
# Cross Origin Resource Sharing
|
||||
# (https://github.com/adamchainz/django-cors-header)
|
||||
# CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
|
||||
# ---------------------------- REQUIRED -------------------------
|
||||
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
|
||||
SECRET_KEY=
|
||||
SECRET_KEY_FILE=
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
|
||||
@@ -27,8 +36,9 @@ DB_ENGINE=django.db.backends.postgresql
|
||||
POSTGRES_HOST=db_recipes
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=djangouser
|
||||
# ---------------------------- REQUIRED -------------------------
|
||||
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_PASSWORD_FILE=
|
||||
# ---------------------------------------------------------------
|
||||
POSTGRES_DB=djangodb
|
||||
|
||||
@@ -100,10 +110,12 @@ GUNICORN_MEDIA=0
|
||||
# prefix used for account related emails (default "[Tandoor Recipes] ")
|
||||
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
|
||||
|
||||
# allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing
|
||||
# see docs for more information https://docs.tandoor.dev/features/authentication/
|
||||
# allow authentication via the REMOTE-USER header (can be used for e.g. authelia).
|
||||
# ATTENTION: Leave off if you don't know what you are doing! Enabling this without proper configuration will enable anybody
|
||||
# to login with any username!
|
||||
# See docs for additional information: https://docs.tandoor.dev/features/authentication/#reverse-proxy-authentication
|
||||
# when unset: 0 (false)
|
||||
REVERSE_PROXY_AUTH=0
|
||||
REMOTE_USER_AUTH=0
|
||||
|
||||
# Default settings for spaces, apply per space and can be changed in the admin view
|
||||
# SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
|
||||
@@ -111,7 +123,8 @@ REVERSE_PROXY_AUTH=0
|
||||
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
|
||||
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
|
||||
|
||||
# allow people to create accounts on your application instance (without an invite link)
|
||||
# allow people to create local accounts on your application instance (without an invite link)
|
||||
# social accounts will always be able to sign up
|
||||
# when unset: 0 (false)
|
||||
# ENABLE_SIGNUP=0
|
||||
|
||||
|
||||
22
.github/workflows/build-docker-open-data.yml
vendored
22
.github/workflows/build-docker-open-data.yml
vendored
@@ -34,16 +34,6 @@ jobs:
|
||||
echo VERSION=develop >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.2
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}-open-data'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
|
||||
# clone open data plugin
|
||||
- name: clone open data plugin repo
|
||||
uses: actions/checkout@master
|
||||
@@ -74,17 +64,17 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -92,7 +82,7 @@ jobs:
|
||||
password: ${{ github.token }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
vabene1111/recipes
|
||||
@@ -107,7 +97,7 @@ jobs:
|
||||
type=semver,suffix=-open-data-plugin,pattern={{major}}
|
||||
type=ref,suffix=-open-data-plugin,event=branch
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
|
||||
30
.github/workflows/build-docker.yml
vendored
30
.github/workflows/build-docker.yml
vendored
@@ -17,15 +17,9 @@ jobs:
|
||||
# Standard build config
|
||||
- name: Standard
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
suffix: ""
|
||||
continue-on-error: false
|
||||
# Raspi build config
|
||||
- name: Raspi
|
||||
dockerfile: Dockerfile-raspi
|
||||
platforms: linux/arm/v7
|
||||
suffix: "-raspi"
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -40,16 +34,6 @@ jobs:
|
||||
echo VERSION=develop >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.2
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -64,17 +48,17 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -82,7 +66,7 @@ jobs:
|
||||
password: ${{ github.token }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
vabene1111/recipes
|
||||
@@ -97,7 +81,7 @@ jobs:
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -74,6 +74,7 @@ mediafiles/
|
||||
\.env
|
||||
staticfiles/
|
||||
postgresql/
|
||||
data/
|
||||
|
||||
|
||||
/docker-compose.override.yml
|
||||
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,7 +1,7 @@
|
||||
FROM python:3.10-alpine3.15
|
||||
FROM python:3.10-alpine3.18
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
@@ -15,7 +15,11 @@ WORKDIR /opt/recipes
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev git && \
|
||||
RUN \
|
||||
if [ `apk --print-arch` = "armv7" ]; then \
|
||||
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
|
||||
fi
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
|
||||
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
|
||||
python -m venv venv && \
|
||||
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
|
||||
@@ -26,5 +30,11 @@ RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-de
|
||||
|
||||
#Copy project and execute it.
|
||||
COPY . ./
|
||||
|
||||
# collect information from git repositories
|
||||
RUN /opt/recipes/venv/bin/python version.py
|
||||
# delete git repositories to reduce image size
|
||||
RUN find . -type d -name ".git" | xargs rm -rf
|
||||
|
||||
RUN chmod +x boot.sh
|
||||
ENTRYPOINT ["/opt/recipes/boot.sh"]
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# builds of cryptography for raspberry pi (or better arm v7) fail for some
|
||||
FROM python:3.9-alpine3.15
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap gcompat
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
#This port will be used by gunicorn.
|
||||
EXPOSE 8080
|
||||
|
||||
#Create app dir and install requirements.
|
||||
RUN mkdir /opt/recipes
|
||||
WORKDIR /opt/recipes
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN \
|
||||
if [ `apk --print-arch` = "armv7" ]; then \
|
||||
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
|
||||
fi
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev zlib-dev jpeg-dev libwebp-dev python3-dev git && \
|
||||
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
|
||||
python -m venv venv && \
|
||||
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
|
||||
venv/bin/pip install wheel==0.37.1 && \
|
||||
venv/bin/pip install -r requirements.txt --no-cache-dir --no-binary=Pillow && \
|
||||
apk --purge del .build-deps
|
||||
|
||||
#Copy project and execute it.
|
||||
COPY . ./
|
||||
RUN chmod +x boot.sh
|
||||
ENTRYPOINT ["/opt/recipes/boot.sh"]
|
||||
20
boot.sh
20
boot.sh
@@ -19,9 +19,14 @@ if [ ! -f "$NGINX_CONF_FILE" ] && [ $GUNICORN_MEDIA -eq 0 ]; then
|
||||
display_warning "Nginx configuration file could not be found at the default location!\nPath: ${NGINX_CONF_FILE}"
|
||||
fi
|
||||
|
||||
# SECRET_KEY must be set in .env file
|
||||
# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file
|
||||
|
||||
if [ -f "${SECRET_KEY_FILE}" ]; then
|
||||
export SECRET_KEY=$(cat "$SECRET_KEY_FILE")
|
||||
fi
|
||||
|
||||
if [ -z "${SECRET_KEY}" ]; then
|
||||
display_warning "The environment variable 'SECRET_KEY' is not set but REQUIRED for running Tandoor!"
|
||||
display_warning "The environment variable 'SECRET_KEY' (or 'SECRET_KEY_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
|
||||
fi
|
||||
|
||||
|
||||
@@ -30,11 +35,16 @@ echo "Waiting for database to be ready..."
|
||||
attempt=0
|
||||
max_attempts=20
|
||||
|
||||
if [ "${DB_ENGINE}" != 'django.db.backends.sqlite3' ]; then
|
||||
if [ "${DB_ENGINE}" == 'django.db.backends.postgresql' ] || [ "${DATABASE_URL}" == 'postgres'* ]; then
|
||||
|
||||
# POSTGRES_PASSWORD (or a valid file at POSTGRES_PASSWORD_FILE) must be set in .env file
|
||||
|
||||
if [ -f "${POSTGRES_PASSWORD_FILE}" ]; then
|
||||
export POSTGRES_PASSWORD=$(cat "$POSTGRES_PASSWORD_FILE")
|
||||
fi
|
||||
|
||||
# POSTGRES_PASSWORD must be set in .env file
|
||||
if [ -z "${POSTGRES_PASSWORD}" ]; then
|
||||
display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!"
|
||||
display_warning "The environment variable 'POSTGRES_PASSWORD' (or 'POSTGRES_PASSWORD_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
|
||||
fi
|
||||
|
||||
while pg_isready --host=${POSTGRES_HOST} --port=${POSTGRES_PORT} --user=${POSTGRES_USER} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do
|
||||
|
||||
@@ -10,13 +10,13 @@ from treebeard.forms import movenodeform_factory
|
||||
|
||||
from cookbook.managers import DICTIONARY
|
||||
|
||||
from .models import (Automation, BookmarkletImport, Comment, CookLog, Food, FoodInheritField,
|
||||
ImportLog, Ingredient, InviteLink, Keyword, MealPlan, MealType,
|
||||
NutritionInformation, Property, PropertyType, Recipe, RecipeBook,
|
||||
RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket,
|
||||
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, TelegramBot,
|
||||
Unit, UnitConversion, UserFile, UserPreference, UserSpace, ViewLog)
|
||||
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
|
||||
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
|
||||
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||
ViewLog)
|
||||
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
@@ -39,6 +39,8 @@ def delete_space_action(modeladmin, request, queryset):
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
autocomplete_fields = ('created_by',)
|
||||
filter_horizontal = ('food_inherit',)
|
||||
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
date_hierarchy = 'created_at'
|
||||
actions = [delete_space_action]
|
||||
@@ -50,6 +52,8 @@ admin.site.register(Space, SpaceAdmin)
|
||||
class UserSpaceAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'space',)
|
||||
search_fields = ('user__username', 'space__name',)
|
||||
filter_horizontal = ('groups',)
|
||||
autocomplete_fields = ('user', 'space',)
|
||||
|
||||
|
||||
admin.site.register(UserSpace, UserSpaceAdmin)
|
||||
@@ -60,6 +64,7 @@ class UserPreferenceAdmin(admin.ModelAdmin):
|
||||
search_fields = ('user__username',)
|
||||
list_filter = ('theme', 'nav_color', 'default_page',)
|
||||
date_hierarchy = 'created_at'
|
||||
filter_horizontal = ('plan_share', 'shopping_share',)
|
||||
|
||||
@staticmethod
|
||||
def name(obj):
|
||||
@@ -187,7 +192,7 @@ class RecipeAdmin(admin.ModelAdmin):
|
||||
def created_by(obj):
|
||||
return obj.created_by.get_user_display_name()
|
||||
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
|
||||
actions = [rebuild_index]
|
||||
|
||||
|
||||
@@ -272,7 +277,7 @@ admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin)
|
||||
|
||||
|
||||
class MealPlanAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'recipe', 'meal_type', 'date')
|
||||
list_display = ('user', 'recipe', 'meal_type', 'from_date', 'to_date')
|
||||
|
||||
@staticmethod
|
||||
def user(obj):
|
||||
@@ -309,6 +314,7 @@ admin.site.register(InviteLink, InviteLinkAdmin)
|
||||
|
||||
class CookLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
|
||||
search_fields = ('recipe__name', 'space__name',)
|
||||
|
||||
|
||||
admin.site.register(CookLog, CookLogAdmin)
|
||||
|
||||
@@ -9,8 +9,8 @@ from django_scopes import scopes_disabled
|
||||
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
|
||||
from hcaptcha.fields import hCaptchaField
|
||||
|
||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
|
||||
RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference)
|
||||
from .models import (Comment, Food, InviteLink, Keyword, Recipe, RecipeBook, RecipeBookEntry,
|
||||
SearchPreference, Space, Storage, Sync, User, UserPreference)
|
||||
|
||||
|
||||
class SelectWidget(widgets.Select):
|
||||
@@ -45,7 +45,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
model = UserPreference
|
||||
fields = (
|
||||
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
||||
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
|
||||
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
|
||||
)
|
||||
|
||||
labels = {
|
||||
@@ -60,29 +60,29 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
'ingredient_decimals': _('Ingredient decimal places'),
|
||||
'shopping_auto_sync': _('Shopping list auto sync period'),
|
||||
'comments': _('Comments'),
|
||||
'left_handed': _('Left-handed mode')
|
||||
'left_handed': _('Left-handed mode'),
|
||||
'show_step_ingredients': _('Show step ingredients table')
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
|
||||
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
|
||||
'use_fractions': _(
|
||||
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
|
||||
|
||||
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
|
||||
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
|
||||
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
|
||||
'shopping_share': _('Users with whom to share shopping lists.'),
|
||||
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
||||
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
||||
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
||||
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
||||
'shopping_auto_sync': _(
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
),
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
|
||||
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
|
||||
'left_handed': _('Will optimize the UI for use with your left hand.')
|
||||
'left_handed': _('Will optimize the UI for use with your left hand.'),
|
||||
'show_step_ingredients': _('Add ingredients table next to recipe steps. Applies at creation time for manually created and URL imported recipes. Individual steps can be overridden in the edit recipe view.')
|
||||
}
|
||||
|
||||
widgets = {
|
||||
@@ -184,6 +184,7 @@ class MultipleFileField(forms.FileField):
|
||||
result = single_file_clean(data, initial)
|
||||
return result
|
||||
|
||||
|
||||
class ImportForm(ImportExportBase):
|
||||
files = MultipleFileField(required=True)
|
||||
duplicates = forms.BooleanField(help_text=_(
|
||||
@@ -322,50 +323,6 @@ class ImportRecipeForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
# TODO deprecate
|
||||
class MealPlanForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
space = kwargs.pop('space')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
|
||||
self.fields['meal_type'].queryset = MealType.objects.filter(space=space).all()
|
||||
self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
|
||||
|
||||
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', 'title', 'meal_type', 'note',
|
||||
'servings', 'date', 'shared'
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
field_classes = {
|
||||
'recipe': SafeModelChoiceField,
|
||||
'meal_type': SafeModelChoiceField,
|
||||
'shared': SafeModelMultipleChoiceField,
|
||||
}
|
||||
|
||||
|
||||
class InviteLinkForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
@@ -506,8 +463,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
||||
help_texts = {
|
||||
'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
|
||||
'shopping_auto_sync': _(
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
),
|
||||
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
|
||||
@@ -551,11 +508,10 @@ class SpacePreferenceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Space
|
||||
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count', 'use_plural')
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'use_plural')
|
||||
|
||||
help_texts = {
|
||||
'food_inherit': _('Fields on food that should be inherited by default.'),
|
||||
'show_facet_count': _('Show recipe counts on search filters'),
|
||||
'use_plural': _('Use the plural form for units and food inside this space.'),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from gettext import gettext as _
|
||||
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.cache import caches
|
||||
from gettext import gettext as _
|
||||
|
||||
from cookbook.models import InviteLink
|
||||
|
||||
@@ -17,10 +16,13 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
|
||||
Whether to allow sign-ups.
|
||||
"""
|
||||
signup_token = False
|
||||
if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
|
||||
if 'signup_token' in request.session and InviteLink.objects.filter(
|
||||
valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
|
||||
signup_token = True
|
||||
|
||||
if (request.resolver_match.view_name == 'account_signup' or request.resolver_match.view_name == 'socialaccount_signup') and not settings.ENABLE_SIGNUP and not signup_token:
|
||||
if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP and not signup_token:
|
||||
return False
|
||||
elif request.resolver_match.view_name == 'socialaccount_signup' and len(settings.SOCIAL_PROVIDERS) < 1:
|
||||
return False
|
||||
else:
|
||||
return super(AllAuthCustomAdapter, self).is_open_for_signup(request)
|
||||
@@ -33,7 +35,7 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
|
||||
if c == default:
|
||||
try:
|
||||
super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context)
|
||||
except Exception: # dont fail signup just because confirmation mail could not be send
|
||||
except Exception: # dont fail signup just because confirmation mail could not be send
|
||||
pass
|
||||
else:
|
||||
messages.add_message(self.request, messages.ERROR, _('In order to prevent spam, the requested email was not send. Please wait a few minutes and try again.'))
|
||||
|
||||
227
cookbook/helper/automation_helper.py
Normal file
227
cookbook/helper/automation_helper.py
Normal file
@@ -0,0 +1,227 @@
|
||||
import re
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.db.models.functions import Lower
|
||||
|
||||
from cookbook.models import Automation
|
||||
|
||||
|
||||
class AutomationEngine:
|
||||
request = None
|
||||
source = None
|
||||
use_cache = None
|
||||
food_aliases = None
|
||||
keyword_aliases = None
|
||||
unit_aliases = None
|
||||
never_unit = None
|
||||
transpose_words = None
|
||||
regex_replace = {
|
||||
Automation.DESCRIPTION_REPLACE: None,
|
||||
Automation.INSTRUCTION_REPLACE: None,
|
||||
Automation.FOOD_REPLACE: None,
|
||||
Automation.UNIT_REPLACE: None,
|
||||
Automation.NAME_REPLACE: None,
|
||||
}
|
||||
|
||||
def __init__(self, request, use_cache=True, source=None):
|
||||
self.request = request
|
||||
self.use_cache = use_cache
|
||||
if not source:
|
||||
self.source = "default_string_to_avoid_false_regex_match"
|
||||
else:
|
||||
self.source = source
|
||||
|
||||
def apply_keyword_automation(self, keyword):
|
||||
keyword = keyword.strip()
|
||||
if self.use_cache and self.keyword_aliases is None:
|
||||
self.keyword_aliases = {}
|
||||
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{self.request.space.pk}'
|
||||
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
|
||||
self.keyword_aliases = c
|
||||
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.keyword_aliases[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(KEYWORD_CACHE_KEY, self.keyword_aliases, 30)
|
||||
else:
|
||||
self.keyword_aliases = {}
|
||||
if self.keyword_aliases:
|
||||
try:
|
||||
keyword = self.keyword_aliases[keyword.lower()]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.KEYWORD_ALIAS, param_1__iexact=keyword, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return keyword
|
||||
|
||||
def apply_unit_automation(self, unit):
|
||||
unit = unit.strip()
|
||||
if self.use_cache and self.unit_aliases is None:
|
||||
self.unit_aliases = {}
|
||||
UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
|
||||
if c := caches['default'].get(UNIT_CACHE_KEY, None):
|
||||
self.unit_aliases = c
|
||||
caches['default'].touch(UNIT_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.unit_aliases[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
||||
else:
|
||||
self.unit_aliases = {}
|
||||
if self.unit_aliases:
|
||||
try:
|
||||
unit = self.unit_aliases[unit.lower()]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return self.apply_regex_replace_automation(unit, Automation.UNIT_REPLACE)
|
||||
|
||||
def apply_food_automation(self, food):
|
||||
food = food.strip()
|
||||
if self.use_cache and self.food_aliases is None:
|
||||
self.food_aliases = {}
|
||||
FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
|
||||
if c := caches['default'].get(FOOD_CACHE_KEY, None):
|
||||
self.food_aliases = c
|
||||
caches['default'].touch(FOOD_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.food_aliases[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
||||
else:
|
||||
self.food_aliases = {}
|
||||
|
||||
if self.food_aliases:
|
||||
try:
|
||||
return self.food_aliases[food.lower()]
|
||||
except KeyError:
|
||||
return food
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return self.apply_regex_replace_automation(food, Automation.FOOD_REPLACE)
|
||||
|
||||
def apply_never_unit_automation(self, tokens):
|
||||
"""
|
||||
Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
|
||||
e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
|
||||
or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
|
||||
:param1 string: string that should never be considered a unit, will be moved to token[2]
|
||||
:param2 (optional) unit as string: will insert unit string into token[1]
|
||||
:return: unit as string (possibly changed by automation)
|
||||
"""
|
||||
|
||||
if self.use_cache and self.never_unit is None:
|
||||
self.never_unit = {}
|
||||
NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}'
|
||||
if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None):
|
||||
self.never_unit = c
|
||||
caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all():
|
||||
self.never_unit[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30)
|
||||
else:
|
||||
self.never_unit = {}
|
||||
|
||||
new_unit = None
|
||||
alt_unit = self.apply_unit_automation(tokens[1])
|
||||
never_unit = False
|
||||
if self.never_unit:
|
||||
try:
|
||||
new_unit = self.never_unit[tokens[1].lower()]
|
||||
never_unit = True
|
||||
except KeyError:
|
||||
return tokens
|
||||
else:
|
||||
if a := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
|
||||
tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
|
||||
new_unit = a.param_2
|
||||
never_unit = True
|
||||
|
||||
if never_unit:
|
||||
tokens.insert(1, new_unit)
|
||||
return tokens
|
||||
|
||||
def apply_transpose_automation(self, string):
|
||||
"""
|
||||
If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string
|
||||
:param 1: first word to detect
|
||||
:param 2: second word to detect
|
||||
return: new ingredient string
|
||||
"""
|
||||
if self.use_cache and self.transpose_words is None:
|
||||
self.transpose_words = {}
|
||||
TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}'
|
||||
if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None):
|
||||
self.transpose_words = c
|
||||
caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30)
|
||||
else:
|
||||
i = 0
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only(
|
||||
'param_1', 'param_2').order_by('order').all()[:512]:
|
||||
self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()]
|
||||
i += 1
|
||||
caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30)
|
||||
else:
|
||||
self.transpose_words = {}
|
||||
|
||||
tokens = [x.lower() for x in string.replace(',', ' ').split()]
|
||||
if self.transpose_words:
|
||||
for key, value in self.transpose_words.items():
|
||||
if value[0] in tokens and value[1] in tokens:
|
||||
string = re.sub(rf"\b({value[0]})\W*({value[1]})\b", r"\2 \1", string, flags=re.IGNORECASE)
|
||||
else:
|
||||
for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \
|
||||
.annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \
|
||||
.filter(param_1_lower__in=tokens, param_2_lower__in=tokens).order_by('order')[:512]:
|
||||
if rule.param_1 in tokens and rule.param_2 in tokens:
|
||||
string = re.sub(rf"\b({rule.param_1})\W*({rule.param_2})\b", r"\2 \1", string, flags=re.IGNORECASE)
|
||||
return string
|
||||
|
||||
def apply_regex_replace_automation(self, string, automation_type):
|
||||
# TODO add warning - maybe on SPACE page? when a max of 512 automations of a specific type is exceeded (ALIAS types excluded?)
|
||||
"""
|
||||
Replaces strings in a recipe field that are from a matched source
|
||||
field_type are Automation.type that apply regex replacements
|
||||
Automation.DESCRIPTION_REPLACE
|
||||
Automation.INSTRUCTION_REPLACE
|
||||
Automation.FOOD_REPLACE
|
||||
Automation.UNIT_REPLACE
|
||||
Automation.NAME_REPLACE
|
||||
|
||||
regex replacment utilized the following fields from the Automation model
|
||||
:param 1: source that should apply the automation in regex format ('.*' for all)
|
||||
:param 2: regex pattern to match ()
|
||||
:param 3: replacement string (leave blank to delete)
|
||||
return: new string
|
||||
"""
|
||||
if self.use_cache and self.regex_replace[automation_type] is None:
|
||||
self.regex_replace[automation_type] = {}
|
||||
REGEX_REPLACE_CACHE_KEY = f'automation_regex_replace_{self.request.space.pk}'
|
||||
if c := caches['default'].get(REGEX_REPLACE_CACHE_KEY, None):
|
||||
self.regex_replace[automation_type] = c[automation_type]
|
||||
caches['default'].touch(REGEX_REPLACE_CACHE_KEY, 30)
|
||||
else:
|
||||
i = 0
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
|
||||
'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
|
||||
self.regex_replace[automation_type][i] = [a.param_1, a.param_2, a.param_3]
|
||||
i += 1
|
||||
caches['default'].set(REGEX_REPLACE_CACHE_KEY, self.regex_replace, 30)
|
||||
else:
|
||||
self.regex_replace[automation_type] = {}
|
||||
|
||||
if self.regex_replace[automation_type]:
|
||||
for rule in self.regex_replace[automation_type].values():
|
||||
if re.match(rule[0], (self.source)[:512]):
|
||||
string = re.sub(rule[1], rule[2], string, flags=re.IGNORECASE)
|
||||
else:
|
||||
for rule in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
|
||||
'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
|
||||
if re.match(rule.param_1, (self.source)[:512]):
|
||||
string = re.sub(rule.param_2, rule.param_3, string, flags=re.IGNORECASE)
|
||||
return string
|
||||
@@ -1,8 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def rescale_image_jpeg(image_object, base_width=1020):
|
||||
@@ -11,7 +10,7 @@ def rescale_image_jpeg(image_object, base_width=1020):
|
||||
width_percent = (base_width / float(img.size[0]))
|
||||
height = int((float(img.size[1]) * float(width_percent)))
|
||||
|
||||
img = img.resize((base_width, height), Image.ANTIALIAS)
|
||||
img = img.resize((base_width, height), Image.LANCZOS)
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, 'JPEG', quality=90, optimize=True, icc_profile=icc_profile)
|
||||
|
||||
@@ -22,7 +21,7 @@ def rescale_image_png(image_object, base_width=1020):
|
||||
image_object = Image.open(image_object)
|
||||
wpercent = (base_width / float(image_object.size[0]))
|
||||
hsize = int((float(image_object.size[1]) * float(wpercent)))
|
||||
img = image_object.resize((base_width, hsize), Image.ANTIALIAS)
|
||||
img = image_object.resize((base_width, hsize), Image.LANCZOS)
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'PNG', quality=90)
|
||||
|
||||
@@ -2,18 +2,16 @@ import re
|
||||
import string
|
||||
import unicodedata
|
||||
|
||||
from django.core.cache import caches
|
||||
|
||||
from cookbook.models import Unit, Food, Automation, Ingredient
|
||||
from cookbook.helper.automation_helper import AutomationEngine
|
||||
from cookbook.models import Food, Ingredient, Unit
|
||||
|
||||
|
||||
class IngredientParser:
|
||||
request = None
|
||||
ignore_rules = False
|
||||
food_aliases = {}
|
||||
unit_aliases = {}
|
||||
automation = None
|
||||
|
||||
def __init__(self, request, cache_mode, ignore_automations=False):
|
||||
def __init__(self, request, cache_mode=True, ignore_automations=False):
|
||||
"""
|
||||
Initialize ingredient parser
|
||||
:param request: request context (to control caching, rule ownership, etc.)
|
||||
@@ -22,65 +20,8 @@ class IngredientParser:
|
||||
"""
|
||||
self.request = request
|
||||
self.ignore_rules = ignore_automations
|
||||
if cache_mode:
|
||||
FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
|
||||
if c := caches['default'].get(FOOD_CACHE_KEY, None):
|
||||
self.food_aliases = c
|
||||
caches['default'].touch(FOOD_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.food_aliases[a.param_1] = a.param_2
|
||||
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
||||
|
||||
UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
|
||||
if c := caches['default'].get(UNIT_CACHE_KEY, None):
|
||||
self.unit_aliases = c
|
||||
caches['default'].touch(UNIT_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.unit_aliases[a.param_1] = a.param_2
|
||||
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
||||
else:
|
||||
self.food_aliases = {}
|
||||
self.unit_aliases = {}
|
||||
|
||||
def apply_food_automation(self, food):
|
||||
"""
|
||||
Apply food alias automations to passed food
|
||||
:param food: unit as string
|
||||
:return: food as string (possibly changed by automation)
|
||||
"""
|
||||
if self.ignore_rules:
|
||||
return food
|
||||
else:
|
||||
if self.food_aliases:
|
||||
try:
|
||||
return self.food_aliases[food]
|
||||
except KeyError:
|
||||
return food
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return food
|
||||
|
||||
def apply_unit_automation(self, unit):
|
||||
"""
|
||||
Apply unit alias automations to passed unit
|
||||
:param unit: unit as string
|
||||
:return: unit as string (possibly changed by automation)
|
||||
"""
|
||||
if self.ignore_rules:
|
||||
return unit
|
||||
else:
|
||||
if self.unit_aliases:
|
||||
try:
|
||||
return self.unit_aliases[unit]
|
||||
except KeyError:
|
||||
return unit
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return unit
|
||||
if not self.ignore_rules:
|
||||
self.automation = AutomationEngine(self.request, use_cache=cache_mode)
|
||||
|
||||
def get_unit(self, unit):
|
||||
"""
|
||||
@@ -91,7 +32,10 @@ class IngredientParser:
|
||||
if not unit:
|
||||
return None
|
||||
if len(unit) > 0:
|
||||
u, created = Unit.objects.get_or_create(name=self.apply_unit_automation(unit), space=self.request.space)
|
||||
if self.ignore_rules:
|
||||
u, created = Unit.objects.get_or_create(name=unit.strip(), space=self.request.space)
|
||||
else:
|
||||
u, created = Unit.objects.get_or_create(name=self.automation.apply_unit_automation(unit), space=self.request.space)
|
||||
return u
|
||||
return None
|
||||
|
||||
@@ -104,7 +48,10 @@ class IngredientParser:
|
||||
if not food:
|
||||
return None
|
||||
if len(food) > 0:
|
||||
f, created = Food.objects.get_or_create(name=self.apply_food_automation(food), space=self.request.space)
|
||||
if self.ignore_rules:
|
||||
f, created = Food.objects.get_or_create(name=food.strip(), space=self.request.space)
|
||||
else:
|
||||
f, created = Food.objects.get_or_create(name=self.automation.apply_food_automation(food), space=self.request.space)
|
||||
return f
|
||||
return None
|
||||
|
||||
@@ -133,10 +80,10 @@ class IngredientParser:
|
||||
end = 0
|
||||
while (end < len(x) and (x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
))):
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
))):
|
||||
end += 1
|
||||
if end > 0:
|
||||
if "/" in x[:end]:
|
||||
@@ -160,7 +107,8 @@ class IngredientParser:
|
||||
if unit is not None and unit.strip() == '':
|
||||
unit = None
|
||||
|
||||
if unit is not None and (unit.startswith('(') or unit.startswith('-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
|
||||
if unit is not None and (unit.startswith('(') or unit.startswith(
|
||||
'-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
|
||||
unit = None
|
||||
note = x
|
||||
return amount, unit, note
|
||||
@@ -230,8 +178,8 @@ class IngredientParser:
|
||||
|
||||
# if the string contains parenthesis early on remove it and place it at the end
|
||||
# because its likely some kind of note
|
||||
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', ingredient):
|
||||
match = re.search('\((.[^\(])+\)', ingredient)
|
||||
if re.match('(.){1,6}\\s\\((.[^\\(\\)])+\\)\\s', ingredient):
|
||||
match = re.search('\\((.[^\\(])+\\)', ingredient)
|
||||
ingredient = ingredient[:match.start()] + ingredient[match.end():] + ' ' + ingredient[match.start():match.end()]
|
||||
|
||||
# leading spaces before commas result in extra tokens, clean them out
|
||||
@@ -239,12 +187,15 @@ class IngredientParser:
|
||||
|
||||
# handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description
|
||||
# "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)"
|
||||
ingredient = re.sub("^(\d+|\d+[\\.,]\d+) - (\d+|\d+[\\.,]\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)
|
||||
ingredient = re.sub("^(\\d+|\\d+[\\.,]\\d+) - (\\d+|\\d+[\\.,]\\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)
|
||||
|
||||
# if amount and unit are connected add space in between
|
||||
if re.match('([0-9])+([A-z])+\s', ingredient):
|
||||
if re.match('([0-9])+([A-z])+\\s', ingredient):
|
||||
ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient)
|
||||
|
||||
if not self.ignore_rules:
|
||||
ingredient = self.automation.apply_transpose_automation(ingredient)
|
||||
|
||||
tokens = ingredient.split() # split at each space into tokens
|
||||
if len(tokens) == 1:
|
||||
# there only is one argument, that must be the food
|
||||
@@ -257,6 +208,8 @@ class IngredientParser:
|
||||
# three arguments if it already has a unit there can't be
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
if not self.ignore_rules:
|
||||
tokens = self.automation.apply_never_unit_automation(tokens)
|
||||
try:
|
||||
if unit is not None:
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
@@ -303,10 +256,11 @@ class IngredientParser:
|
||||
if unit_note not in note:
|
||||
note += ' ' + unit_note
|
||||
|
||||
if unit:
|
||||
unit = self.apply_unit_automation(unit.strip())
|
||||
if unit and not self.ignore_rules:
|
||||
unit = self.automation.apply_unit_automation(unit)
|
||||
|
||||
food = self.apply_food_automation(food.strip())
|
||||
if food and not self.ignore_rules:
|
||||
food = self.automation.apply_food_automation(food)
|
||||
if len(food) > Food._meta.get_field('name').max_length: # test if food name is to long
|
||||
# try splitting it at a space and taking only the first arg
|
||||
if len(food.split()) > 1 and len(food.split()[0]) < Food._meta.get_field('name').max_length:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from cookbook.models import Unit, SupermarketCategory, Property, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion, FoodProperty
|
||||
from cookbook.models import (Food, FoodProperty, Property, PropertyType, Supermarket,
|
||||
SupermarketCategory, SupermarketCategoryRelation, Unit, UnitConversion)
|
||||
|
||||
|
||||
class OpenDataImporter:
|
||||
@@ -33,7 +32,8 @@ class OpenDataImporter:
|
||||
))
|
||||
|
||||
if self.update_existing:
|
||||
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
|
||||
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=(
|
||||
'name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
|
||||
else:
|
||||
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',))
|
||||
|
||||
@@ -116,27 +116,25 @@ class OpenDataImporter:
|
||||
self._update_slug_cache(Unit, 'unit')
|
||||
self._update_slug_cache(PropertyType, 'property')
|
||||
|
||||
# pref_unit_key = 'preferred_unit_metric'
|
||||
# pref_shopping_unit_key = 'preferred_packaging_unit_metric'
|
||||
# if not self.use_metric:
|
||||
# pref_unit_key = 'preferred_unit_imperial'
|
||||
# pref_shopping_unit_key = 'preferred_packaging_unit_imperial'
|
||||
|
||||
insert_list = []
|
||||
insert_list_flat = []
|
||||
update_list = []
|
||||
update_field_list = []
|
||||
for k in list(self.data[datatype].keys()):
|
||||
if not (self.data[datatype][k]['name'] in existing_objects_flat or self.data[datatype][k]['plural_name'] in existing_objects_flat):
|
||||
insert_list.append({'data': {
|
||||
'name': self.data[datatype][k]['name'],
|
||||
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
|
||||
# 'preferred_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
|
||||
# 'preferred_shopping_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
|
||||
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
|
||||
'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
|
||||
'open_data_slug': k,
|
||||
'space': self.request.space.id,
|
||||
}})
|
||||
if not (self.data[datatype][k]['name'] in insert_list_flat or self.data[datatype][k]['plural_name'] in insert_list_flat):
|
||||
insert_list.append({'data': {
|
||||
'name': self.data[datatype][k]['name'],
|
||||
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
|
||||
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
|
||||
'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
|
||||
'open_data_slug': k,
|
||||
'space': self.request.space.id,
|
||||
}})
|
||||
# build a fake second flat array to prevent duplicate foods from being inserted.
|
||||
# trying to insert a duplicate would throw a db error :(
|
||||
insert_list_flat.append(self.data[datatype][k]['name'])
|
||||
insert_list_flat.append(self.data[datatype][k]['plural_name'])
|
||||
else:
|
||||
if self.data[datatype][k]['name'] in existing_objects:
|
||||
existing_food_id = existing_objects[self.data[datatype][k]['name']][0]
|
||||
@@ -149,8 +147,6 @@ class OpenDataImporter:
|
||||
id=existing_food_id,
|
||||
name=self.data[datatype][k]['name'],
|
||||
plural_name=self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
|
||||
# preferred_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
|
||||
# preferred_shopping_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
|
||||
supermarket_category_id=self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
|
||||
fdc_id=self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
|
||||
open_data_slug=k,
|
||||
@@ -166,23 +162,20 @@ class OpenDataImporter:
|
||||
self._update_slug_cache(Food, 'food')
|
||||
|
||||
food_property_list = []
|
||||
alias_list = []
|
||||
# alias_list = []
|
||||
|
||||
for k in list(self.data[datatype].keys()):
|
||||
for fp in self.data[datatype][k]['properties']['type_values']:
|
||||
food_property_list.append(Property(
|
||||
property_type_id=self.slug_id_cache['property'][fp['property_type']],
|
||||
property_amount=fp['property_value'],
|
||||
import_food_id=self.slug_id_cache['food'][k],
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
# for a in self.data[datatype][k]['alias']:
|
||||
# alias_list.append(Automation(
|
||||
# param_1=a,
|
||||
# param_2=self.data[datatype][k]['name'],
|
||||
# space=self.request.space,
|
||||
# created_by=self.request.user,
|
||||
# ))
|
||||
# try catch here because somettimes key "k" is not set for he food cache
|
||||
try:
|
||||
food_property_list.append(Property(
|
||||
property_type_id=self.slug_id_cache['property'][fp['property_type']],
|
||||
property_amount=fp['property_value'],
|
||||
import_food_id=self.slug_id_cache['food'][k],
|
||||
space=self.request.space,
|
||||
))
|
||||
except KeyError:
|
||||
print(str(k) + ' is not in self.slug_id_cache["food"]')
|
||||
|
||||
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
|
||||
|
||||
@@ -192,7 +185,6 @@ class OpenDataImporter:
|
||||
|
||||
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
|
||||
|
||||
# Automation.objects.bulk_create(alias_list, ignore_conflicts=True, unique_fields=('space', 'param_1', 'param_2',))
|
||||
return insert_list + update_list
|
||||
|
||||
def import_conversion(self):
|
||||
@@ -200,15 +192,19 @@ class OpenDataImporter:
|
||||
|
||||
insert_list = []
|
||||
for k in list(self.data[datatype].keys()):
|
||||
insert_list.append(UnitConversion(
|
||||
base_amount=self.data[datatype][k]['base_amount'],
|
||||
base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
|
||||
converted_amount=self.data[datatype][k]['converted_amount'],
|
||||
converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
|
||||
food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
|
||||
open_data_slug=k,
|
||||
space=self.request.space,
|
||||
created_by=self.request.user,
|
||||
))
|
||||
# try catch here because sometimes key "k" is not set for he food cache
|
||||
try:
|
||||
insert_list.append(UnitConversion(
|
||||
base_amount=self.data[datatype][k]['base_amount'],
|
||||
base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
|
||||
converted_amount=self.data[datatype][k]['converted_amount'],
|
||||
converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
|
||||
food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
|
||||
open_data_slug=k,
|
||||
space=self.request.space,
|
||||
created_by=self.request.user,
|
||||
))
|
||||
except KeyError:
|
||||
print(str(k) + ' is not in self.slug_id_cache["food"]')
|
||||
|
||||
return UnitConversion.objects.bulk_create(insert_list, ignore_conflicts=True, unique_fields=('space', 'base_unit', 'converted_unit', 'food', 'open_data_slug'))
|
||||
|
||||
@@ -4,16 +4,16 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.contrib.rest_framework import TokenHasScope, TokenHasReadWriteScope
|
||||
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
|
||||
from oauth2_provider.models import AccessToken
|
||||
from rest_framework import permissions
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
|
||||
from cookbook.models import ShareLink, Recipe, UserSpace
|
||||
from cookbook.models import Recipe, ShareLink, UserSpace
|
||||
|
||||
|
||||
def get_allowed_groups(groups_required):
|
||||
@@ -255,9 +255,6 @@ class CustomIsShared(permissions.BasePermission):
|
||||
return request.user.is_authenticated
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# # temporary hack to make old shopping list work with new shopping list
|
||||
# if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
|
||||
# return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
|
||||
return is_object_shared(request.user, obj)
|
||||
|
||||
|
||||
@@ -322,7 +319,8 @@ class CustomRecipePermission(permissions.BasePermission):
|
||||
|
||||
def has_permission(self, request, view): # user is either at least a guest or a share link is given and the request is safe
|
||||
share = request.query_params.get('share', None)
|
||||
return has_group_permission(request.user, ['guest']) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
|
||||
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(
|
||||
request.user, ['user'])) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
share = request.query_params.get('share', None)
|
||||
@@ -332,7 +330,8 @@ class CustomRecipePermission(permissions.BasePermission):
|
||||
if obj.private:
|
||||
return ((obj.created_by == request.user) or (request.user in obj.shared.all())) and obj.space == request.space
|
||||
else:
|
||||
return has_group_permission(request.user, ['guest']) and obj.space == request.space
|
||||
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS)
|
||||
or has_group_permission(request.user, ['user'])) and obj.space == request.space
|
||||
|
||||
|
||||
class CustomUserPermission(permissions.BasePermission):
|
||||
@@ -361,7 +360,7 @@ class CustomTokenHasScope(TokenHasScope):
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if type(request.auth) == AccessToken:
|
||||
if isinstance(request.auth, AccessToken):
|
||||
return super().has_permission(request, view)
|
||||
else:
|
||||
return request.user.is_authenticated
|
||||
@@ -375,7 +374,7 @@ class CustomTokenHasReadWriteScope(TokenHasReadWriteScope):
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if type(request.auth) == AccessToken:
|
||||
if isinstance(request.auth, AccessToken):
|
||||
return super().has_permission(request, view)
|
||||
else:
|
||||
return True
|
||||
@@ -434,3 +433,10 @@ def switch_user_active_space(user, space):
|
||||
return us
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class IsReadOnlyDRF(permissions.BasePermission):
|
||||
message = 'You cannot interact with this object as it is not owned by you!'
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.method in SAFE_METHODS
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.core.cache import caches
|
||||
|
||||
from cookbook.helper.cache_helper import CacheHelper
|
||||
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
||||
from cookbook.models import PropertyType, Unit, Food, Property, Recipe, Step
|
||||
from cookbook.models import PropertyType
|
||||
|
||||
|
||||
class FoodPropertyHelper:
|
||||
@@ -31,10 +31,12 @@ class FoodPropertyHelper:
|
||||
|
||||
if not property_types:
|
||||
property_types = PropertyType.objects.filter(space=self.space).all()
|
||||
caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60) # cache is cleared on property type save signal so long duration is fine
|
||||
# cache is cleared on property type save signal so long duration is fine
|
||||
caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60)
|
||||
|
||||
for fpt in property_types:
|
||||
computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0, 'missing_value': False}
|
||||
computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'description': fpt.description,
|
||||
'unit': fpt.unit, 'order': fpt.order, 'food_values': {}, 'total_value': 0, 'missing_value': False}
|
||||
|
||||
uch = UnitConversionHelper(self.space)
|
||||
|
||||
@@ -53,7 +55,8 @@ class FoodPropertyHelper:
|
||||
if c.unit == i.food.properties_food_unit:
|
||||
found_property = True
|
||||
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
|
||||
computed_properties[pt.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
|
||||
computed_properties[pt.id]['food_values'] = self.add_or_create(
|
||||
computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
|
||||
if not found_property:
|
||||
computed_properties[pt.id]['missing_value'] = True
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
# import json
|
||||
# import re
|
||||
# from json import JSONDecodeError
|
||||
# from urllib.parse import unquote
|
||||
|
||||
# from bs4 import BeautifulSoup
|
||||
# from bs4.element import Tag
|
||||
# from recipe_scrapers import scrape_html, scrape_me
|
||||
# from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
|
||||
# from recipe_scrapers._utils import get_host_name, normalize_string
|
||||
|
||||
# from cookbook.helper import recipe_url_import as helper
|
||||
# from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
|
||||
|
||||
# def get_recipe_from_source(text, url, request):
|
||||
# def build_node(k, v):
|
||||
# if isinstance(v, dict):
|
||||
# node = {
|
||||
# 'name': k,
|
||||
# 'value': k,
|
||||
# 'children': get_children_dict(v)
|
||||
# }
|
||||
# elif isinstance(v, list):
|
||||
# node = {
|
||||
# 'name': k,
|
||||
# 'value': k,
|
||||
# 'children': get_children_list(v)
|
||||
# }
|
||||
# else:
|
||||
# node = {
|
||||
# 'name': k + ": " + normalize_string(str(v)),
|
||||
# 'value': normalize_string(str(v))
|
||||
# }
|
||||
# return node
|
||||
|
||||
# def get_children_dict(children):
|
||||
# kid_list = []
|
||||
# for k, v in children.items():
|
||||
# kid_list.append(build_node(k, v))
|
||||
# return kid_list
|
||||
|
||||
# def get_children_list(children):
|
||||
# kid_list = []
|
||||
# for kid in children:
|
||||
# if type(kid) == list:
|
||||
# node = {
|
||||
# 'name': "unknown list",
|
||||
# 'value': "unknown list",
|
||||
# 'children': get_children_list(kid)
|
||||
# }
|
||||
# kid_list.append(node)
|
||||
# elif type(kid) == dict:
|
||||
# for k, v in kid.items():
|
||||
# kid_list.append(build_node(k, v))
|
||||
# else:
|
||||
# kid_list.append({
|
||||
# 'name': normalize_string(str(kid)),
|
||||
# 'value': normalize_string(str(kid))
|
||||
# })
|
||||
# return kid_list
|
||||
|
||||
# recipe_tree = []
|
||||
# parse_list = []
|
||||
# soup = BeautifulSoup(text, "html.parser")
|
||||
# html_data = get_from_html(soup)
|
||||
# images = get_images_from_source(soup, url)
|
||||
# text = unquote(text)
|
||||
# scrape = None
|
||||
|
||||
# if url and not text:
|
||||
# try:
|
||||
# scrape = scrape_me(url_path=url, wild_mode=True)
|
||||
# except(NoSchemaFoundInWildMode):
|
||||
# pass
|
||||
|
||||
# if not scrape:
|
||||
# try:
|
||||
# parse_list.append(remove_graph(json.loads(text)))
|
||||
# if not url and 'url' in parse_list[0]:
|
||||
# url = parse_list[0]['url']
|
||||
# scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)
|
||||
|
||||
# except JSONDecodeError:
|
||||
# for el in soup.find_all('script', type='application/ld+json'):
|
||||
# el = remove_graph(el)
|
||||
# if not url and 'url' in el:
|
||||
# url = el['url']
|
||||
# if type(el) == list:
|
||||
# for le in el:
|
||||
# parse_list.append(le)
|
||||
# elif type(el) == dict:
|
||||
# parse_list.append(el)
|
||||
# for el in soup.find_all(type='application/json'):
|
||||
# el = remove_graph(el)
|
||||
# if type(el) == list:
|
||||
# for le in el:
|
||||
# parse_list.append(le)
|
||||
# elif type(el) == dict:
|
||||
# parse_list.append(el)
|
||||
# scrape = text_scraper(text, url=url)
|
||||
|
||||
# recipe_json = helper.get_from_scraper(scrape, request)
|
||||
|
||||
# # TODO: DEPRECATE recipe_tree & html_data. first validate it isn't used anywhere
|
||||
# for el in parse_list:
|
||||
# temp_tree = []
|
||||
# if isinstance(el, Tag):
|
||||
# try:
|
||||
# el = json.loads(el.string)
|
||||
# except TypeError:
|
||||
# continue
|
||||
|
||||
# for k, v in el.items():
|
||||
# if isinstance(v, dict):
|
||||
# node = {
|
||||
# 'name': k,
|
||||
# 'value': k,
|
||||
# 'children': get_children_dict(v)
|
||||
# }
|
||||
# elif isinstance(v, list):
|
||||
# node = {
|
||||
# 'name': k,
|
||||
# 'value': k,
|
||||
# 'children': get_children_list(v)
|
||||
# }
|
||||
# else:
|
||||
# node = {
|
||||
# 'name': k + ": " + normalize_string(str(v)),
|
||||
# 'value': normalize_string(str(v))
|
||||
# }
|
||||
# temp_tree.append(node)
|
||||
|
||||
# if '@type' in el and el['@type'] == 'Recipe':
|
||||
# recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
|
||||
# else:
|
||||
# recipe_tree += [{'name': 'json', 'children': temp_tree}]
|
||||
|
||||
# return recipe_json, recipe_tree, html_data, images
|
||||
|
||||
|
||||
# def get_from_html(soup):
|
||||
# INVISIBLE_ELEMS = ('style', 'script', 'head', 'title')
|
||||
# html = []
|
||||
# for s in soup.strings:
|
||||
# if ((s.parent.name not in INVISIBLE_ELEMS) and (len(s.strip()) > 0)):
|
||||
# html.append(s)
|
||||
# return html
|
||||
|
||||
|
||||
# def get_images_from_source(soup, url):
|
||||
# sources = ['src', 'srcset', 'data-src']
|
||||
# images = []
|
||||
# img_tags = soup.find_all('img')
|
||||
# if url:
|
||||
# site = get_host_name(url)
|
||||
# prot = url.split(':')[0]
|
||||
|
||||
# urls = []
|
||||
# for img in img_tags:
|
||||
# for src in sources:
|
||||
# try:
|
||||
# urls.append(img[src])
|
||||
# except KeyError:
|
||||
# pass
|
||||
|
||||
# for u in urls:
|
||||
# u = u.split('?')[0]
|
||||
# filename = re.search(r'/([\w_-]+[.](jpg|jpeg|gif|png))$', u)
|
||||
# if filename:
|
||||
# if (('http' not in u) and (url)):
|
||||
# # sometimes an image source can be relative
|
||||
# # if it is provide the base url
|
||||
# u = '{}://{}{}'.format(prot, site, u)
|
||||
# if 'http' in u:
|
||||
# images.append(u)
|
||||
# return images
|
||||
|
||||
|
||||
# def remove_graph(el):
|
||||
# # recipes type might be wrapped in @graph type
|
||||
# if isinstance(el, Tag):
|
||||
# try:
|
||||
# el = json.loads(el.string)
|
||||
# if '@graph' in el:
|
||||
# for x in el['@graph']:
|
||||
# if '@type' in x and x['@type'] == 'Recipe':
|
||||
# el = x
|
||||
# except (TypeError, JSONDecodeError):
|
||||
# pass
|
||||
# return el
|
||||
@@ -1,14 +1,11 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||
from django.core.cache import cache, caches
|
||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
|
||||
When)
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Avg, Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, When
|
||||
from django.db.models.functions import Coalesce, Lower, Substr
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.managers import DICTIONARY
|
||||
@@ -17,21 +14,25 @@ from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, Searc
|
||||
from recipes import settings
|
||||
|
||||
|
||||
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||
class RecipeSearch():
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in [
|
||||
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'
|
||||
|
||||
def __init__(self, request, **params):
|
||||
self._request = request
|
||||
self._queryset = None
|
||||
if f := params.get('filter', None):
|
||||
custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
|
||||
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
|
||||
custom_filter = (
|
||||
CustomFilter.objects.filter(id=f, space=self._request.space)
|
||||
.filter(Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user))
|
||||
.first()
|
||||
)
|
||||
if custom_filter:
|
||||
self._params = {**json.loads(custom_filter.search)}
|
||||
self._original_params = {**(params or {})}
|
||||
# json.loads casts rating as an integer, expecting string
|
||||
if isinstance(self._params.get('rating', None), int):
|
||||
self._params['rating'] = str(self._params['rating'])
|
||||
else:
|
||||
self._params = {**(params or {})}
|
||||
else:
|
||||
@@ -85,9 +86,9 @@ class RecipeSearch():
|
||||
self._viewedon = self._params.get('viewedon', None)
|
||||
self._makenow = self._params.get('makenow', None)
|
||||
# this supports hidden feature to find recipes missing X ingredients
|
||||
if type(self._makenow) == bool and self._makenow == True:
|
||||
if isinstance(self._makenow, bool) and self._makenow == True:
|
||||
self._makenow = 0
|
||||
elif type(self._makenow) == str and self._makenow in ["yes", "true"]:
|
||||
elif isinstance(self._makenow, str) and self._makenow in ["yes", "true"]:
|
||||
self._makenow = 0
|
||||
else:
|
||||
try:
|
||||
@@ -98,24 +99,18 @@ class RecipeSearch():
|
||||
self._search_type = self._search_prefs.search or 'plain'
|
||||
if self._string:
|
||||
if self._postgres:
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list(
|
||||
'field', flat=True)
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||
else:
|
||||
self._unaccent_include = []
|
||||
self._icontains_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._trigram_include = None
|
||||
self._fulltext_include = None
|
||||
self._trigram = False
|
||||
if self._postgres and self._string:
|
||||
self._language = DICTIONARY.get(
|
||||
translation.get_language(), 'simple')
|
||||
self._trigram_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list(
|
||||
'field', flat=True) or None
|
||||
self._language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
|
||||
|
||||
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||
self._trigram = True
|
||||
@@ -150,7 +145,7 @@ class RecipeSearch():
|
||||
self.unit_filters(units=self._units)
|
||||
self._makenow_filter(missing=self._makenow)
|
||||
self.string_filters(string=self._string)
|
||||
return self._queryset.filter(space=self._request.space).distinct().order_by(*self.orderby)
|
||||
return self._queryset.filter(space=self._request.space).order_by(*self.orderby)
|
||||
|
||||
def _sort_includes(self, *args):
|
||||
for x in args:
|
||||
@@ -166,7 +161,7 @@ class RecipeSearch():
|
||||
else:
|
||||
order = []
|
||||
# TODO add userpreference for default sort order and replace '-favorite'
|
||||
default_order = ['-name']
|
||||
default_order = ['name']
|
||||
# recent and new_recipe are always first; they float a few recipes to the top
|
||||
if self._num_recent:
|
||||
order += ['-recent']
|
||||
@@ -175,7 +170,6 @@ class RecipeSearch():
|
||||
|
||||
# if a sort order is provided by user - use that order
|
||||
if self._sort_order:
|
||||
|
||||
if not isinstance(self._sort_order, list):
|
||||
order += [self._sort_order]
|
||||
else:
|
||||
@@ -215,24 +209,18 @@ class RecipeSearch():
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
if self._fulltext_include:
|
||||
if self._fuzzy_match is None:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(
|
||||
rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
|
||||
if self._fuzzy_match is not None:
|
||||
simularity = self._fuzzy_match.filter(
|
||||
pk=OuterRef('pk')).values('simularity')
|
||||
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
|
||||
if not self._fulltext_include:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(
|
||||
simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=F('rank') + F('simularity'))
|
||||
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
|
||||
else:
|
||||
query_filter = Q()
|
||||
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
||||
@@ -241,78 +229,69 @@ class RecipeSearch():
|
||||
|
||||
def _cooked_on_filter(self, cooked_date=None):
|
||||
if self._sort_includes('lastcooked') or cooked_date:
|
||||
lessthan = self._sort_includes(
|
||||
'-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
if lessthan:
|
||||
default = timezone.now() - timedelta(days=100000)
|
||||
else:
|
||||
default = timezone.now()
|
||||
self._queryset = self._queryset.annotate(lastcooked=Coalesce(
|
||||
Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
|
||||
)
|
||||
if cooked_date is None:
|
||||
return
|
||||
|
||||
cooked_date = date(*[int(x)
|
||||
for x in cooked_date.split('-') if x != ''])
|
||||
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
|
||||
def _created_on_filter(self, created_date=None):
|
||||
if created_date is None:
|
||||
return
|
||||
lessthan = '-' in created_date[:1]
|
||||
created_date = date(*[int(x)
|
||||
for x in created_date.split('-') if x != ''])
|
||||
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__lte=created_date)
|
||||
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__gte=created_date)
|
||||
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
|
||||
|
||||
def _updated_on_filter(self, updated_date=None):
|
||||
if updated_date is None:
|
||||
return
|
||||
lessthan = '-' in updated_date[:1]
|
||||
updated_date = date(*[int(x)
|
||||
for x in updated_date.split('-') if x != ''])
|
||||
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__lte=updated_date)
|
||||
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__gte=updated_date)
|
||||
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
|
||||
|
||||
def _viewed_on_filter(self, viewed_date=None):
|
||||
if self._sort_includes('lastviewed') or viewed_date:
|
||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
|
||||
)
|
||||
if viewed_date is None:
|
||||
return
|
||||
lessthan = '-' in viewed_date[:1]
|
||||
viewed_date = date(*[int(x)
|
||||
for x in viewed_date.split('-') if x != ''])
|
||||
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
|
||||
def _new_recipes(self, new_days=7):
|
||||
# TODO make new days a user-setting
|
||||
if not self._new:
|
||||
return
|
||||
self._queryset = (
|
||||
self._queryset.annotate(new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
|
||||
self._queryset = self._queryset.annotate(
|
||||
new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
|
||||
default=Value(0),
|
||||
)
|
||||
)
|
||||
|
||||
def _recently_viewed(self, num_recent=None):
|
||||
@@ -322,34 +301,35 @@ class RecipeSearch():
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||
return
|
||||
|
||||
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
|
||||
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
|
||||
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
num_recent_recipes = (
|
||||
ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
|
||||
.values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
)
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
|
||||
def _favorite_recipes(self, times_cooked=None):
|
||||
if self._sort_includes('favorite') or times_cooked:
|
||||
less_than = '-' in (times_cooked or []
|
||||
) and not self._sort_includes('-favorite')
|
||||
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
default = 1000
|
||||
else:
|
||||
default = 0
|
||||
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
|
||||
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
|
||||
self._queryset = self._queryset.annotate(
|
||||
favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
favorite_recipes = (
|
||||
CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
|
||||
.values('recipe')
|
||||
.annotate(count=Count('pk', distinct=True))
|
||||
.values('count')
|
||||
)
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
if times_cooked is None:
|
||||
return
|
||||
|
||||
if times_cooked == '0':
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif less_than:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(
|
||||
times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
favorite__gte=int(times_cooked))
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
|
||||
|
||||
def keyword_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@@ -382,8 +362,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in kw_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def food_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@@ -397,8 +376,7 @@ class RecipeSearch():
|
||||
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||
if 'or' in fd_filter:
|
||||
if self._include_children:
|
||||
f_or = Q(
|
||||
steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
else:
|
||||
f_or = Q(steps__ingredients__food__in=foods)
|
||||
|
||||
@@ -410,8 +388,7 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for food in foods:
|
||||
if self._include_children:
|
||||
f_and = Q(
|
||||
steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
else:
|
||||
f_and = Q(steps__ingredients__food=food)
|
||||
if 'not' in fd_filter:
|
||||
@@ -419,8 +396,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in fd_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def unit_filters(self, units=None, operator=True):
|
||||
if operator != True:
|
||||
@@ -429,27 +405,25 @@ class RecipeSearch():
|
||||
return
|
||||
if not isinstance(units, list):
|
||||
units = [units]
|
||||
self._queryset = self._queryset.filter(
|
||||
steps__ingredients__unit__in=units)
|
||||
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
|
||||
|
||||
def rating_filter(self, rating=None):
|
||||
if rating or self._sort_includes('rating'):
|
||||
lessthan = self._sort_includes('-rating') or '-' in (rating or [])
|
||||
if lessthan:
|
||||
lessthan = '-' in (rating or [])
|
||||
reverse = 'rating' in (self._sort_order or []) and '-rating' not in (self._sort_order or [])
|
||||
if lessthan or reverse:
|
||||
default = 100
|
||||
else:
|
||||
default = 0
|
||||
# TODO make ratings a settings user-only vs all-users
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
if rating is None:
|
||||
return
|
||||
|
||||
if rating == '0':
|
||||
self._queryset = self._queryset.filter(rating=0)
|
||||
elif lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(rating__gte=int(rating))
|
||||
|
||||
@@ -477,14 +451,11 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for book in kwargs[bk_filter]:
|
||||
if 'not' in bk_filter:
|
||||
recipes = recipes.filter(
|
||||
recipebookentry__book__id=book)
|
||||
recipes = recipes.filter(recipebookentry__book__id=book)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
recipebookentry__book__id=book)
|
||||
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
|
||||
if 'not' in bk_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def step_filters(self, steps=None, operator=True):
|
||||
if operator != True:
|
||||
@@ -503,25 +474,20 @@ class RecipeSearch():
|
||||
rank = []
|
||||
if 'name' in self._fulltext_include:
|
||||
vectors.append('name_search_vector')
|
||||
rank.append(SearchRank('name_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
|
||||
if 'description' in self._fulltext_include:
|
||||
vectors.append('desc_search_vector')
|
||||
rank.append(SearchRank('desc_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
|
||||
if 'steps__instruction' in self._fulltext_include:
|
||||
vectors.append('steps__search_vector')
|
||||
rank.append(SearchRank('steps__search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
|
||||
if 'keywords__name' in self._fulltext_include:
|
||||
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
||||
vectors.append('keywords__name__unaccent')
|
||||
rank.append(SearchRank('keywords__name__unaccent',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
|
||||
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||
vectors.append('steps__ingredients__food__name__unaccent')
|
||||
rank.append(SearchRank('steps__ingredients__food__name',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
|
||||
|
||||
for r in rank:
|
||||
if self.search_rank is None:
|
||||
@@ -529,8 +495,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self.search_rank += r
|
||||
# modifying queryset will annotation creates duplicate results
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(
|
||||
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
|
||||
def build_text_filters(self, string=None):
|
||||
if not string:
|
||||
@@ -555,15 +520,19 @@ class RecipeSearch():
|
||||
trigram += TrigramSimilarity(f, self._string)
|
||||
else:
|
||||
trigram = TrigramSimilarity(f, self._string)
|
||||
self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
|
||||
).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
self._fuzzy_match = (
|
||||
Recipe.objects.annotate(trigram=trigram)
|
||||
.distinct()
|
||||
.annotate(simularity=Max('trigram'))
|
||||
.values('id', 'simularity')
|
||||
.filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
)
|
||||
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
|
||||
|
||||
def _makenow_filter(self, missing=None):
|
||||
if missing is None or (type(missing) == bool and missing == False):
|
||||
if missing is None or (isinstance(missing, bool) and missing == False):
|
||||
return
|
||||
shopping_users = [
|
||||
*self._request.user.get_shopping_share(), self._request.user]
|
||||
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||
|
||||
onhand_filter = (
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
@@ -573,264 +542,40 @@ class RecipeSearch():
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
)
|
||||
makenow_recipes = Recipe.objects.annotate(
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(
|
||||
steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk',
|
||||
filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||
steps__ingredients__food__recipe__isnull=True), distinct=True),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0))
|
||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
|
||||
self._queryset = self._queryset.distinct().filter(
|
||||
id__in=makenow_recipes.values('id'))
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count(
|
||||
'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), distinct=True
|
||||
),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood__lte=missing)
|
||||
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
|
||||
|
||||
@staticmethod
|
||||
def __children_substitute_filter(shopping_users=None):
|
||||
children_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=OuterRef('path'),
|
||||
depth__gt=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_children=True)
|
||||
.annotate(child_onhand_count=Exists(children_onhand_subquery))
|
||||
.filter(child_onhand_count=True)
|
||||
)
|
||||
return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0
|
||||
).filter(substitute_children=True
|
||||
).annotate(child_onhand_count=Exists(children_onhand_subquery)
|
||||
).filter(child_onhand_count=True)
|
||||
|
||||
@staticmethod
|
||||
def __sibling_substitute_filter(shopping_users=None):
|
||||
sibling_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(
|
||||
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
|
||||
)
|
||||
return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0
|
||||
).filter(substitute_siblings=True
|
||||
).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
|
||||
).filter(sibling_onhand=True)
|
||||
|
||||
|
||||
class RecipeFacet():
|
||||
class CacheEmpty(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||
if hash_key is None and queryset is None:
|
||||
raise ValueError(_("One of queryset or hash_key must be provided"))
|
||||
|
||||
self._request = request
|
||||
self._queryset = queryset
|
||||
self.hash_key = hash_key or str(hash(self._queryset.query))
|
||||
self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||
self._cache_timeout = cache_timeout
|
||||
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||
if self._cache is None and self._queryset is None:
|
||||
raise self.CacheEmpty("No queryset provided and cache empty")
|
||||
|
||||
self.Keywords = self._cache.get('Keywords', None)
|
||||
self.Foods = self._cache.get('Foods', None)
|
||||
self.Books = self._cache.get('Books', None)
|
||||
self.Ratings = self._cache.get('Ratings', None)
|
||||
# TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
|
||||
self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
if self._queryset is not None:
|
||||
self._recipe_list = list(
|
||||
self._queryset.values_list('id', flat=True))
|
||||
self._search_params = {
|
||||
'keyword_list': self._request.query_params.getlist('keywords', []),
|
||||
'food_list': self._request.query_params.getlist('foods', []),
|
||||
'book_list': self._request.query_params.getlist('book', []),
|
||||
'search_keywords_or': str2bool(self._request.query_params.get('keywords_or', True)),
|
||||
'search_foods_or': str2bool(self._request.query_params.get('foods_or', True)),
|
||||
'search_books_or': str2bool(self._request.query_params.get('books_or', True)),
|
||||
'space': self._request.space,
|
||||
}
|
||||
elif self.hash_key is not None:
|
||||
self._recipe_list = self._cache.get('recipe_list', [])
|
||||
self._search_params = {
|
||||
'keyword_list': self._cache.get('keyword_list', None),
|
||||
'food_list': self._cache.get('food_list', None),
|
||||
'book_list': self._cache.get('book_list', None),
|
||||
'search_keywords_or': self._cache.get('search_keywords_or', None),
|
||||
'search_foods_or': self._cache.get('search_foods_or', None),
|
||||
'search_books_or': self._cache.get('search_books_or', None),
|
||||
'space': self._cache.get('space', None),
|
||||
}
|
||||
|
||||
self._cache = {
|
||||
**self._search_params,
|
||||
'recipe_list': self._recipe_list,
|
||||
'Ratings': self.Ratings,
|
||||
'Recent': self.Recent,
|
||||
'Keywords': self.Keywords,
|
||||
'Foods': self.Foods,
|
||||
'Books': self.Books
|
||||
|
||||
}
|
||||
caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||
self._cache, self._cache_timeout)
|
||||
|
||||
def get_facets(self, from_cache=False):
|
||||
if from_cache:
|
||||
return {
|
||||
'cache_key': self.hash_key or '',
|
||||
'Ratings': self.Ratings or {},
|
||||
'Recent': self.Recent or [],
|
||||
'Keywords': self.Keywords or [],
|
||||
'Foods': self.Foods or [],
|
||||
'Books': self.Books or []
|
||||
}
|
||||
return {
|
||||
'cache_key': self.hash_key,
|
||||
'Ratings': self.get_ratings(),
|
||||
'Recent': self.get_recent(),
|
||||
'Keywords': self.get_keywords(),
|
||||
'Foods': self.get_foods(),
|
||||
'Books': self.get_books()
|
||||
}
|
||||
|
||||
def set_cache(self, key, value):
|
||||
self._cache = {**self._cache, key: value}
|
||||
caches['default'].set(
|
||||
self._SEARCH_CACHE_KEY,
|
||||
self._cache,
|
||||
self._cache_timeout
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_siblings=True)
|
||||
.annotate(sibling_onhand=Exists(sibling_onhand_subquery))
|
||||
.filter(sibling_onhand=True)
|
||||
)
|
||||
|
||||
def get_books(self):
|
||||
if self.Books is None:
|
||||
self.Books = []
|
||||
return self.Books
|
||||
|
||||
def get_keywords(self):
|
||||
if self.Keywords is None:
|
||||
if self._search_params['search_keywords_or']:
|
||||
keywords = Keyword.objects.filter(
|
||||
space=self._request.space).distinct()
|
||||
else:
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# set keywords to root objects only
|
||||
keywords = self._keyword_queryset(keywords)
|
||||
self.Keywords = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.Keywords
|
||||
|
||||
def get_foods(self):
|
||||
if self.Foods is None:
|
||||
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||
if self._search_params['search_foods_or']:
|
||||
foods = Food.objects.filter(
|
||||
space=self._request.space).distinct()
|
||||
else:
|
||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# set keywords to root objects only
|
||||
foods = self._food_queryset(foods)
|
||||
|
||||
self.Foods = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.Foods
|
||||
|
||||
def get_ratings(self):
|
||||
if self.Ratings is None:
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
if self._queryset is None:
|
||||
self._queryset = Recipe.objects.filter(
|
||||
id__in=self._recipe_list)
|
||||
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
||||
self.Ratings = dict(Counter(r.rating for r in rating_qs))
|
||||
else:
|
||||
self.Rating = {}
|
||||
self.set_cache('Ratings', self.Ratings)
|
||||
return self.Ratings
|
||||
|
||||
def get_recent(self):
|
||||
if self.Recent is None:
|
||||
# TODO make days of recent recipe a setting
|
||||
recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space, created_at__gte=timezone.now() - timedelta(days=14)
|
||||
).values_list('recipe__pk', flat=True)
|
||||
self.Recent = list(recent_recipes)
|
||||
self.set_cache('Recent', self.Recent)
|
||||
return self.Recent
|
||||
|
||||
def add_food_children(self, id):
|
||||
try:
|
||||
food = Food.objects.get(id=id)
|
||||
nodes = food.get_ancestors()
|
||||
except Food.DoesNotExist:
|
||||
return self.get_facets()
|
||||
foods = self._food_queryset(food.get_children(), food)
|
||||
deep_search = self.Foods
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == food.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.get_facets()
|
||||
|
||||
def add_keyword_children(self, id):
|
||||
try:
|
||||
keyword = Keyword.objects.get(id=id)
|
||||
nodes = keyword.get_ancestors()
|
||||
except Keyword.DoesNotExist:
|
||||
return self.get_facets()
|
||||
keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||
deep_search = self.Keywords
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search)
|
||||
if x["id"] == keyword.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.get_facets()
|
||||
|
||||
def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
|
||||
).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
|
||||
|
||||
def _keyword_queryset(self, queryset, keyword=None):
|
||||
depth = getattr(keyword, 'depth', 0) + 1
|
||||
steplen = depth * Keyword.steplen
|
||||
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
||||
).filter(depth=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
else:
|
||||
return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
|
||||
def _food_queryset(self, queryset, food=None):
|
||||
depth = getattr(food, 'depth', 0) + 1
|
||||
steplen = depth * Food.steplen
|
||||
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
||||
).filter(depth__lte=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
else:
|
||||
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# import random
|
||||
import re
|
||||
import traceback
|
||||
from html import unescape
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.utils.dateparse import parse_duration
|
||||
from django.utils.translation import gettext as _
|
||||
from isodate import parse_duration as iso_parse_duration
|
||||
@@ -11,20 +9,37 @@ from isodate.isoerror import ISO8601Error
|
||||
from pytube import YouTube
|
||||
from recipe_scrapers._utils import get_host_name, get_minutes
|
||||
|
||||
# from cookbook.helper import recipe_url_import as helper
|
||||
from cookbook.helper.automation_helper import AutomationEngine
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import Automation, Keyword, PropertyType
|
||||
|
||||
|
||||
# from unicodedata import decomposition
|
||||
|
||||
|
||||
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||
|
||||
|
||||
def get_from_scraper(scrape, request):
|
||||
# converting the scrape_me object to the existing json format based on ld+json
|
||||
recipe_json = {}
|
||||
|
||||
recipe_json = {
|
||||
'steps': [],
|
||||
'internal': True
|
||||
}
|
||||
keywords = []
|
||||
|
||||
# assign source URL
|
||||
try:
|
||||
source_url = scrape.canonical_url()
|
||||
except Exception:
|
||||
try:
|
||||
source_url = scrape.url
|
||||
except Exception:
|
||||
pass
|
||||
if source_url:
|
||||
recipe_json['source_url'] = source_url
|
||||
try:
|
||||
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
||||
except Exception:
|
||||
recipe_json['source_url'] = ''
|
||||
|
||||
automation_engine = AutomationEngine(request, source=recipe_json.get('source_url'))
|
||||
# assign recipe name
|
||||
try:
|
||||
recipe_json['name'] = parse_name(scrape.title()[:128] or None)
|
||||
except Exception:
|
||||
@@ -38,6 +53,10 @@ def get_from_scraper(scrape, request):
|
||||
if isinstance(recipe_json['name'], list) and len(recipe_json['name']) > 0:
|
||||
recipe_json['name'] = recipe_json['name'][0]
|
||||
|
||||
recipe_json['name'] = automation_engine.apply_regex_replace_automation(recipe_json['name'], Automation.NAME_REPLACE)
|
||||
|
||||
# assign recipe description
|
||||
# TODO notify user about limit if reached - >256 description will be truncated
|
||||
try:
|
||||
description = scrape.description() or None
|
||||
except Exception:
|
||||
@@ -48,16 +67,20 @@ def get_from_scraper(scrape, request):
|
||||
except Exception:
|
||||
description = ''
|
||||
|
||||
recipe_json['internal'] = True
|
||||
recipe_json['description'] = parse_description(description)
|
||||
recipe_json['description'] = automation_engine.apply_regex_replace_automation(recipe_json['description'], Automation.DESCRIPTION_REPLACE)
|
||||
|
||||
# assign servings attributes
|
||||
try:
|
||||
servings = scrape.schema.data.get('recipeYield') or 1 # dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
|
||||
# dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
|
||||
servings = scrape.schema.data.get('recipeYield') or 1
|
||||
except Exception:
|
||||
servings = 1
|
||||
|
||||
recipe_json['servings'] = parse_servings(servings)
|
||||
recipe_json['servings_text'] = parse_servings_text(servings)
|
||||
|
||||
# assign time attributes
|
||||
try:
|
||||
recipe_json['working_time'] = get_minutes(scrape.prep_time()) or 0
|
||||
except Exception:
|
||||
@@ -82,6 +105,7 @@ def get_from_scraper(scrape, request):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# assign image
|
||||
try:
|
||||
recipe_json['image'] = parse_image(scrape.image()) or None
|
||||
except Exception:
|
||||
@@ -92,7 +116,7 @@ def get_from_scraper(scrape, request):
|
||||
except Exception:
|
||||
recipe_json['image'] = ''
|
||||
|
||||
keywords = []
|
||||
# assign keywords
|
||||
try:
|
||||
if scrape.schema.data.get("keywords"):
|
||||
keywords += listify_keywords(scrape.schema.data.get("keywords"))
|
||||
@@ -117,20 +141,6 @@ def get_from_scraper(scrape, request):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
source_url = scrape.canonical_url()
|
||||
except Exception:
|
||||
try:
|
||||
source_url = scrape.url
|
||||
except Exception:
|
||||
pass
|
||||
if source_url:
|
||||
recipe_json['source_url'] = source_url
|
||||
try:
|
||||
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
||||
except Exception:
|
||||
recipe_json['source_url'] = ''
|
||||
|
||||
try:
|
||||
if scrape.author():
|
||||
keywords.append(scrape.author())
|
||||
@@ -138,33 +148,25 @@ def get_from_scraper(scrape, request):
|
||||
pass
|
||||
|
||||
try:
|
||||
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
|
||||
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request)
|
||||
except AttributeError:
|
||||
recipe_json['keywords'] = keywords
|
||||
|
||||
ingredient_parser = IngredientParser(request, True)
|
||||
|
||||
recipe_json['steps'] = []
|
||||
# assign steps
|
||||
try:
|
||||
for i in parse_instructions(scrape.instructions()):
|
||||
recipe_json['steps'].append({'instruction': i, 'ingredients': [], })
|
||||
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients, })
|
||||
except Exception:
|
||||
pass
|
||||
if len(recipe_json['steps']) == 0:
|
||||
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
|
||||
|
||||
parsed_description = parse_description(description)
|
||||
# TODO notify user about limit if reached
|
||||
# limits exist to limit the attack surface for dos style attacks
|
||||
automations = Automation.objects.filter(type=Automation.DESCRIPTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').all().order_by('order')[:512]
|
||||
for a in automations:
|
||||
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
|
||||
parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1)
|
||||
|
||||
if len(parsed_description) > 256: # split at 256 as long descriptions don't look good on recipe cards
|
||||
recipe_json['steps'][0]['instruction'] = f'*{parsed_description}* \n\n' + recipe_json['steps'][0]['instruction']
|
||||
if len(recipe_json['description']) > 256: # split at 256 as long descriptions don't look good on recipe cards
|
||||
recipe_json['steps'][0]['instruction'] = f"*{recipe_json['description']}* \n\n" + recipe_json['steps'][0]['instruction']
|
||||
else:
|
||||
recipe_json['description'] = parsed_description[:512]
|
||||
recipe_json['description'] = recipe_json['description'][:512]
|
||||
|
||||
try:
|
||||
for x in scrape.ingredients():
|
||||
@@ -205,12 +207,9 @@ def get_from_scraper(scrape, request):
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
if 'source_url' in recipe_json and recipe_json['source_url']:
|
||||
automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512]
|
||||
for a in automations:
|
||||
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
|
||||
for s in recipe_json['steps']:
|
||||
s['instruction'] = re.sub(a.param_2, a.param_3, s['instruction'])
|
||||
for s in recipe_json['steps']:
|
||||
s['instruction'] = automation_engine.apply_regex_replace_automation(s['instruction'], Automation.INSTRUCTION_REPLACE)
|
||||
# re.sub(a.param_2, a.param_3, s['instruction'])
|
||||
|
||||
return recipe_json
|
||||
|
||||
@@ -260,11 +259,14 @@ def get_from_youtube_scraper(url, request):
|
||||
]
|
||||
}
|
||||
|
||||
# TODO add automation here
|
||||
try:
|
||||
automation_engine = AutomationEngine(request, source=url)
|
||||
video = YouTube(url=url)
|
||||
default_recipe_json['name'] = video.title
|
||||
default_recipe_json['name'] = automation_engine.apply_regex_replace_automation(video.title, Automation.NAME_REPLACE)
|
||||
default_recipe_json['image'] = video.thumbnail_url
|
||||
default_recipe_json['steps'][0]['instruction'] = video.description
|
||||
default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -272,7 +274,7 @@ def get_from_youtube_scraper(url, request):
|
||||
|
||||
|
||||
def parse_name(name):
|
||||
if type(name) == list:
|
||||
if isinstance(name, list):
|
||||
try:
|
||||
name = name[0]
|
||||
except Exception:
|
||||
@@ -316,16 +318,16 @@ def parse_instructions(instructions):
|
||||
"""
|
||||
instruction_list = []
|
||||
|
||||
if type(instructions) == list:
|
||||
if isinstance(instructions, list):
|
||||
for i in instructions:
|
||||
if type(i) == str:
|
||||
if isinstance(i, str):
|
||||
instruction_list.append(clean_instruction_string(i))
|
||||
else:
|
||||
if 'text' in i:
|
||||
instruction_list.append(clean_instruction_string(i['text']))
|
||||
elif 'itemListElement' in i:
|
||||
for ile in i['itemListElement']:
|
||||
if type(ile) == str:
|
||||
if isinstance(ile, str):
|
||||
instruction_list.append(clean_instruction_string(ile))
|
||||
elif 'text' in ile:
|
||||
instruction_list.append(clean_instruction_string(ile['text']))
|
||||
@@ -341,13 +343,13 @@ def parse_image(image):
|
||||
# check if list of images is returned, take first if so
|
||||
if not image:
|
||||
return None
|
||||
if type(image) == list:
|
||||
if isinstance(image, list):
|
||||
for pic in image:
|
||||
if (type(pic) == str) and (pic[:4] == 'http'):
|
||||
if (isinstance(pic, str)) and (pic[:4] == 'http'):
|
||||
image = pic
|
||||
elif 'url' in pic:
|
||||
image = pic['url']
|
||||
elif type(image) == dict:
|
||||
elif isinstance(image, dict):
|
||||
if 'url' in image:
|
||||
image = image['url']
|
||||
|
||||
@@ -358,12 +360,12 @@ def parse_image(image):
|
||||
|
||||
|
||||
def parse_servings(servings):
|
||||
if type(servings) == str:
|
||||
if isinstance(servings, str):
|
||||
try:
|
||||
servings = int(re.search(r'\d+', servings).group())
|
||||
except AttributeError:
|
||||
servings = 1
|
||||
elif type(servings) == list:
|
||||
elif isinstance(servings, list):
|
||||
try:
|
||||
servings = int(re.findall(r'\b\d+\b', servings[0])[0])
|
||||
except KeyError:
|
||||
@@ -372,12 +374,12 @@ def parse_servings(servings):
|
||||
|
||||
|
||||
def parse_servings_text(servings):
|
||||
if type(servings) == str:
|
||||
if isinstance(servings, str):
|
||||
try:
|
||||
servings = re.sub("\d+", '', servings).strip()
|
||||
servings = re.sub("\\d+", '', servings).strip()
|
||||
except Exception:
|
||||
servings = ''
|
||||
if type(servings) == list:
|
||||
if isinstance(servings, list):
|
||||
try:
|
||||
servings = parse_servings_text(servings[1])
|
||||
except Exception:
|
||||
@@ -394,7 +396,7 @@ def parse_time(recipe_time):
|
||||
recipe_time = round(iso_parse_duration(recipe_time).seconds / 60)
|
||||
except ISO8601Error:
|
||||
try:
|
||||
if (type(recipe_time) == list and len(recipe_time) > 0):
|
||||
if (isinstance(recipe_time, list) and len(recipe_time) > 0):
|
||||
recipe_time = recipe_time[0]
|
||||
recipe_time = round(parse_duration(recipe_time).seconds / 60)
|
||||
except AttributeError:
|
||||
@@ -403,18 +405,9 @@ def parse_time(recipe_time):
|
||||
return recipe_time
|
||||
|
||||
|
||||
def parse_keywords(keyword_json, space):
|
||||
def parse_keywords(keyword_json, request):
|
||||
keywords = []
|
||||
keyword_aliases = {}
|
||||
# retrieve keyword automation cache if it exists, otherwise build from database
|
||||
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{space.pk}'
|
||||
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
|
||||
keyword_aliases = c
|
||||
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
keyword_aliases[a.param_1] = a.param_2
|
||||
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
|
||||
automation_engine = AutomationEngine(request)
|
||||
|
||||
# keywords as list
|
||||
for kw in keyword_json:
|
||||
@@ -422,12 +415,8 @@ def parse_keywords(keyword_json, space):
|
||||
# if alias exists use that instead
|
||||
|
||||
if len(kw) != 0:
|
||||
if keyword_aliases:
|
||||
try:
|
||||
kw = keyword_aliases[kw]
|
||||
except KeyError:
|
||||
pass
|
||||
if k := Keyword.objects.filter(name=kw, space=space).first():
|
||||
automation_engine.apply_keyword_automation(kw)
|
||||
if k := Keyword.objects.filter(name=kw, space=request.space).first():
|
||||
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
|
||||
else:
|
||||
keywords.append({'label': kw, 'name': kw})
|
||||
@@ -438,15 +427,15 @@ def parse_keywords(keyword_json, space):
|
||||
def listify_keywords(keyword_list):
|
||||
# keywords as string
|
||||
try:
|
||||
if type(keyword_list[0]) == dict:
|
||||
if isinstance(keyword_list[0], dict):
|
||||
return keyword_list
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
if type(keyword_list) == str:
|
||||
if isinstance(keyword_list, str):
|
||||
keyword_list = keyword_list.split(',')
|
||||
|
||||
# keywords as string in list
|
||||
if (type(keyword_list) == list and len(keyword_list) == 1 and ',' in keyword_list[0]):
|
||||
if (isinstance(keyword_list, list) and len(keyword_list) == 1 and ',' in keyword_list[0]):
|
||||
keyword_list = keyword_list[0].split(',')
|
||||
return [x.strip() for x in keyword_list]
|
||||
|
||||
@@ -500,13 +489,13 @@ def get_images_from_soup(soup, url):
|
||||
|
||||
|
||||
def clean_dict(input_dict, key):
|
||||
if type(input_dict) == dict:
|
||||
if isinstance(input_dict, dict):
|
||||
for x in list(input_dict):
|
||||
if x == key:
|
||||
del input_dict[x]
|
||||
elif type(input_dict[x]) == dict:
|
||||
elif isinstance(input_dict[x], dict):
|
||||
input_dict[x] = clean_dict(input_dict[x], key)
|
||||
elif type(input_dict[x]) == list:
|
||||
elif isinstance(input_dict[x], list):
|
||||
temp_list = []
|
||||
for e in input_dict[x]:
|
||||
temp_list.append(clean_dict(e, key))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from django.urls import reverse
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from cookbook.views import views
|
||||
@@ -50,7 +48,6 @@ class ScopeMiddleware:
|
||||
return views.no_groups(request)
|
||||
|
||||
request.space = user_space.space
|
||||
# with scopes_disabled():
|
||||
with scope(space=request.space):
|
||||
return self.get_response(request)
|
||||
else:
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||
SupermarketCategoryRelation)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
def shopping_helper(qs, request):
|
||||
@@ -47,7 +44,7 @@ class RecipeShoppingEditor():
|
||||
self.mealplan = self._kwargs.get('mealplan', None)
|
||||
if type(self.mealplan) in [int, float]:
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
||||
if type(self.mealplan) == dict:
|
||||
if isinstance(self.mealplan, dict):
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
|
||||
self.id = self._kwargs.get('id', None)
|
||||
|
||||
@@ -69,11 +66,12 @@ class RecipeShoppingEditor():
|
||||
|
||||
@property
|
||||
def _recipe_servings(self):
|
||||
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings', None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
|
||||
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings',
|
||||
None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
|
||||
|
||||
@property
|
||||
def _servings_factor(self):
|
||||
return Decimal(self.servings)/Decimal(self._recipe_servings)
|
||||
return Decimal(self.servings) / Decimal(self._recipe_servings)
|
||||
|
||||
@property
|
||||
def _shared_users(self):
|
||||
@@ -90,9 +88,10 @@ class RecipeShoppingEditor():
|
||||
|
||||
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
||||
if exclude_onhand:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(
|
||||
food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
else:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
|
||||
@property
|
||||
def _include_related(self):
|
||||
@@ -109,7 +108,7 @@ class RecipeShoppingEditor():
|
||||
self.servings = float(servings)
|
||||
|
||||
if mealplan := kwargs.get('mealplan', None):
|
||||
if type(mealplan) == dict:
|
||||
if isinstance(mealplan, dict):
|
||||
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
|
||||
else:
|
||||
self.mealplan = mealplan
|
||||
@@ -170,14 +169,14 @@ class RecipeShoppingEditor():
|
||||
try:
|
||||
self._shopping_list_recipe.delete()
|
||||
return True
|
||||
except:
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
def _add_ingredients(self, ingredients=None):
|
||||
if not ingredients:
|
||||
return
|
||||
elif type(ingredients) == list:
|
||||
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
||||
elif isinstance(ingredients, list):
|
||||
ingredients = Ingredient.objects.filter(id__in=ingredients, food__ignore_shopping=False)
|
||||
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
||||
add_ingredients = ingredients.exclude(id__in=existing)
|
||||
|
||||
@@ -199,120 +198,3 @@ class RecipeShoppingEditor():
|
||||
to_delete = self._shopping_list_recipe.entries.exclude(ingredient__in=ingredients)
|
||||
ShoppingListEntry.objects.filter(id__in=to_delete).delete()
|
||||
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
|
||||
|
||||
|
||||
# # TODO refactor as class
|
||||
# def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
|
||||
# """
|
||||
# Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
|
||||
# :param list_recipe: Modify an existing ShoppingListRecipe
|
||||
# :param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
|
||||
# :param mealplan: alternatively use a mealplan recipe as source of ingredients
|
||||
# :param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
|
||||
# :param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
||||
# :param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
||||
# """
|
||||
# r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
||||
# if not r:
|
||||
# raise ValueError(_("You must supply a recipe or mealplan"))
|
||||
|
||||
# created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
|
||||
# if not created_by:
|
||||
# raise ValueError(_("You must supply a created_by"))
|
||||
|
||||
# try:
|
||||
# servings = float(servings)
|
||||
# except (ValueError, TypeError):
|
||||
# servings = getattr(mealplan, 'servings', 1.0)
|
||||
|
||||
# servings_factor = servings / r.servings
|
||||
|
||||
# shared_users = list(created_by.get_shopping_share())
|
||||
# shared_users.append(created_by)
|
||||
# if list_recipe:
|
||||
# created = False
|
||||
# else:
|
||||
# list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
|
||||
# created = True
|
||||
|
||||
# related_step_ing = []
|
||||
# if servings == 0 and not created:
|
||||
# list_recipe.delete()
|
||||
# return []
|
||||
# elif ingredients:
|
||||
# ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
|
||||
# else:
|
||||
# ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
|
||||
|
||||
# if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
||||
# ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
|
||||
# if related := created_by.userpreference.mealplan_autoinclude_related:
|
||||
# # TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
|
||||
# related_recipes = r.get_related_recipes()
|
||||
|
||||
# for x in related_recipes:
|
||||
# # related recipe is a Step serving size is driven by recipe serving size
|
||||
# # TODO once/if Steps can have a serving size this needs to be refactored
|
||||
# if exclude_onhand:
|
||||
# # if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
|
||||
# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
|
||||
# else:
|
||||
# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
|
||||
|
||||
# x_ing = []
|
||||
# if ingredients.filter(food__recipe=x).exists():
|
||||
# for ing in ingredients.filter(food__recipe=x):
|
||||
# if exclude_onhand:
|
||||
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
# else:
|
||||
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
|
||||
# for i in [x for x in x_ing]:
|
||||
# ShoppingListEntry.objects.create(
|
||||
# list_recipe=list_recipe,
|
||||
# food=i.food,
|
||||
# unit=i.unit,
|
||||
# ingredient=i,
|
||||
# amount=i.amount * Decimal(servings_factor),
|
||||
# created_by=created_by,
|
||||
# space=space,
|
||||
# )
|
||||
# # dont' add food to the shopping list that are actually recipes that will be added as ingredients
|
||||
# ingredients = ingredients.exclude(food__recipe=x)
|
||||
|
||||
# add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
|
||||
# if not append:
|
||||
# existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
|
||||
# # delete shopping list entries not included in ingredients
|
||||
# existing_list.exclude(ingredient__in=ingredients).delete()
|
||||
# # add shopping list entries that did not previously exist
|
||||
# add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
||||
# add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
||||
|
||||
# # if servings have changed, update the ShoppingListRecipe and existing Entries
|
||||
# if servings <= 0:
|
||||
# servings = 1
|
||||
|
||||
# if not created and list_recipe.servings != servings:
|
||||
# update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
|
||||
# list_recipe.servings = servings
|
||||
# list_recipe.save()
|
||||
# for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
|
||||
# sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
||||
# sle.save()
|
||||
|
||||
# # add any missing Entries
|
||||
# for i in [x for x in add_ingredients if x.food]:
|
||||
|
||||
# ShoppingListEntry.objects.create(
|
||||
# list_recipe=list_recipe,
|
||||
# food=i.food,
|
||||
# unit=i.unit,
|
||||
# ingredient=i,
|
||||
# amount=i.amount * Decimal(servings_factor),
|
||||
# created_by=created_by,
|
||||
# space=space,
|
||||
# )
|
||||
|
||||
# # return all shopping list items
|
||||
# return list_recipe
|
||||
@@ -2,7 +2,6 @@ from gettext import gettext as _
|
||||
|
||||
import bleach
|
||||
import markdown as md
|
||||
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||
from jinja2 import Template, TemplateSyntaxError, UndefinedError
|
||||
from markdown.extensions.tables import TableExtension
|
||||
|
||||
@@ -53,9 +52,17 @@ class IngredientObject(object):
|
||||
def render_instructions(step): # TODO deduplicate markdown cleanup code
|
||||
instructions = step.instruction
|
||||
|
||||
tags = markdown_tags + [
|
||||
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead', 'img'
|
||||
]
|
||||
tags = {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"b", "i", "strong", "em", "tt",
|
||||
"p", "br",
|
||||
"span", "div", "blockquote", "code", "pre", "hr",
|
||||
"ul", "ol", "li", "dd", "dt",
|
||||
"img",
|
||||
"a",
|
||||
"sub", "sup",
|
||||
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead'
|
||||
}
|
||||
parsed_md = md.markdown(
|
||||
instructions,
|
||||
extensions=[
|
||||
@@ -63,7 +70,11 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
|
||||
UrlizeExtension(), MarkdownFormatExtension()
|
||||
]
|
||||
)
|
||||
markdown_attrs['*'] = markdown_attrs['*'] + ['class', 'width', 'height']
|
||||
markdown_attrs = {
|
||||
"*": ["id", "class", 'width', 'height'],
|
||||
"img": ["src", "alt", "title"],
|
||||
"a": ["href", "alt", "title"],
|
||||
}
|
||||
|
||||
instructions = bleach.clean(parsed_md, tags, markdown_attrs)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class ChefTap(Integration):
|
||||
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,)
|
||||
|
||||
if source_url != '':
|
||||
step.instruction += '\n' + source_url
|
||||
|
||||
@@ -4,6 +4,7 @@ from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
|
||||
@@ -19,6 +20,10 @@ class Chowdown(Integration):
|
||||
direction_mode = False
|
||||
description_mode = False
|
||||
|
||||
description = None
|
||||
prep_time = None
|
||||
serving = None
|
||||
|
||||
ingredients = []
|
||||
directions = []
|
||||
descriptions = []
|
||||
@@ -26,6 +31,12 @@ class Chowdown(Integration):
|
||||
line = fl.decode("utf-8")
|
||||
if 'title:' in line:
|
||||
title = line.replace('title:', '').replace('"', '').strip()
|
||||
if 'description:' in line:
|
||||
description = line.replace('description:', '').replace('"', '').strip()
|
||||
if 'prep_time:' in line:
|
||||
prep_time = line.replace('prep_time:', '').replace('"', '').strip()
|
||||
if 'yield:' in line:
|
||||
serving = line.replace('yield:', '').replace('"', '').strip()
|
||||
if 'image:' in line:
|
||||
image = line.replace('image:', '').strip()
|
||||
if 'tags:' in line:
|
||||
@@ -48,15 +59,43 @@ class Chowdown(Integration):
|
||||
descriptions.append(line)
|
||||
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
if description:
|
||||
recipe.description = description
|
||||
|
||||
for k in tags.split(','):
|
||||
print(f'adding keyword {k.strip()}')
|
||||
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
|
||||
)
|
||||
ingredients_added = False
|
||||
for direction in directions:
|
||||
if len(direction.strip()) > 0:
|
||||
step = Step.objects.create(
|
||||
instruction=direction, name='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
else:
|
||||
step = Step.objects.create(
|
||||
instruction=direction, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in ingredients:
|
||||
if len(ingredient.strip()) > 0:
|
||||
amount, unit, food, note = ingredient_parser.parse(ingredient)
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
if serving:
|
||||
recipe.servings = parse_servings(serving)
|
||||
recipe.servings_text = 'servings'
|
||||
|
||||
if prep_time:
|
||||
recipe.working_time = parse_time(prep_time)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in ingredients:
|
||||
@@ -76,6 +115,7 @@ class Chowdown(Integration):
|
||||
if re.match(f'^images/{image}$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
|
||||
|
||||
recipe.save()
|
||||
return recipe
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import base64
|
||||
import gzip
|
||||
import json
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
import validators
|
||||
import yaml
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import (get_from_scraper, get_images_from_soup,
|
||||
iso_duration_to_minutes)
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
from cookbook.models import Ingredient, Recipe, Step
|
||||
|
||||
|
||||
class CookBookApp(Integration):
|
||||
@@ -25,7 +20,6 @@ class CookBookApp(Integration):
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe_html = file.getvalue().decode("utf-8")
|
||||
|
||||
# recipe_json, recipe_tree, html_data, images = get_recipe_from_source(recipe_html, 'CookBookApp', self.request)
|
||||
scrape = text_scraper(text=recipe_html)
|
||||
recipe_json = get_from_scraper(scrape, self.request)
|
||||
images = list(dict.fromkeys(get_images_from_soup(scrape.soup, None)))
|
||||
@@ -37,7 +31,7 @@ class CookBookApp(Integration):
|
||||
|
||||
try:
|
||||
recipe.servings = re.findall('([0-9])+', recipe_json['recipeYield'])[0]
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -47,7 +41,8 @@ class CookBookApp(Integration):
|
||||
pass
|
||||
|
||||
# assuming import files only contain single step
|
||||
step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space, )
|
||||
step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space,
|
||||
show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
|
||||
if 'nutrition' in recipe_json:
|
||||
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']
|
||||
@@ -62,7 +57,7 @@ class CookBookApp(Integration):
|
||||
if unit := ingredient.get('unit', None):
|
||||
u = ingredient_parser.get_unit(unit.get('name', None))
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
|
||||
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
|
||||
))
|
||||
|
||||
if len(images) > 0:
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import base64
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
from gettext import gettext as _
|
||||
|
||||
import requests
|
||||
import validators
|
||||
from lxml import etree
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
from cookbook.models import Ingredient, Recipe, Step
|
||||
|
||||
|
||||
class Cookmate(Integration):
|
||||
@@ -50,7 +45,7 @@ class Cookmate(Integration):
|
||||
for step in recipe_text.getchildren():
|
||||
if step.text:
|
||||
step = Step.objects.create(
|
||||
instruction=step.text.strip(), space=self.request.space,
|
||||
instruction=step.text.strip(), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
@@ -26,12 +25,13 @@ class CopyMeThat(Integration):
|
||||
except AttributeError:
|
||||
source = None
|
||||
|
||||
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip()[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(
|
||||
)[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
for category in file.find_all("span", {"class": "recipeCategory"}):
|
||||
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
|
||||
try:
|
||||
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
|
||||
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
|
||||
@@ -51,7 +51,7 @@ class CopyMeThat(Integration):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
step = Step.objects.create(instruction='', space=self.request.space, )
|
||||
step = Step.objects.create(instruction='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
@@ -61,7 +61,14 @@ class CopyMeThat(Integration):
|
||||
if not isinstance(ingredient, Tag) or not ingredient.text.strip() or "recipeIngredient_spacer" in ingredient['class']:
|
||||
continue
|
||||
if any(x in ingredient['class'] for x in ["recipeIngredient_subheader", "recipeIngredient_note"]):
|
||||
step.ingredients.add(Ingredient.objects.create(is_header=True, note=ingredient.text.strip()[:256], original_text=ingredient.text.strip(), space=self.request.space, ))
|
||||
step.ingredients.add(
|
||||
Ingredient.objects.create(
|
||||
is_header=True,
|
||||
note=ingredient.text.strip()[
|
||||
:256],
|
||||
original_text=ingredient.text.strip(),
|
||||
space=self.request.space,
|
||||
))
|
||||
else:
|
||||
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
@@ -78,7 +85,7 @@ class CopyMeThat(Integration):
|
||||
step.save()
|
||||
recipe.steps.add(step)
|
||||
step = Step.objects.create(instruction='', space=self.request.space, )
|
||||
|
||||
|
||||
step.name = instruction.text.strip()[:128]
|
||||
else:
|
||||
step.instruction += instruction.text.strip() + ' \n\n'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import traceback
|
||||
from io import BytesIO, StringIO
|
||||
from re import match
|
||||
from zipfile import ZipFile
|
||||
@@ -19,7 +20,10 @@ class Default(Integration):
|
||||
recipe = self.decode_recipe(recipe_string)
|
||||
images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist()))
|
||||
if images:
|
||||
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
|
||||
try:
|
||||
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
|
||||
except AttributeError:
|
||||
traceback.print_exc()
|
||||
return recipe
|
||||
|
||||
def decode_recipe(self, string):
|
||||
@@ -54,7 +58,7 @@ class Default(Integration):
|
||||
|
||||
try:
|
||||
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
|
||||
except ValueError:
|
||||
except (ValueError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
recipe_zip_obj.close()
|
||||
@@ -67,4 +71,4 @@ class Default(Integration):
|
||||
|
||||
export_zip_obj.close()
|
||||
|
||||
return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
|
||||
return [[self.get_export_file_name(), export_zip_stream.getvalue()]]
|
||||
|
||||
@@ -28,7 +28,7 @@ class Domestica(Integration):
|
||||
recipe.save()
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=file['directions'], space=self.request.space,
|
||||
instruction=file['directions'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
if file['source'] != '':
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import traceback
|
||||
import datetime
|
||||
import traceback
|
||||
import uuid
|
||||
@@ -18,8 +17,7 @@ from lxml import etree
|
||||
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.models import Keyword, Recipe
|
||||
from recipes.settings import DEBUG
|
||||
from recipes.settings import EXPORT_FILE_CACHE_DURATION
|
||||
from recipes.settings import DEBUG, EXPORT_FILE_CACHE_DURATION
|
||||
|
||||
|
||||
class Integration:
|
||||
@@ -39,7 +37,6 @@ class Integration:
|
||||
self.ignored_recipes = []
|
||||
|
||||
description = f'Imported by {request.user.get_user_display_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
|
||||
icon = '📥'
|
||||
|
||||
try:
|
||||
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
|
||||
@@ -52,23 +49,19 @@ class Integration:
|
||||
self.keyword = parent.add_child(
|
||||
name=name,
|
||||
description=description,
|
||||
icon=icon,
|
||||
space=request.space
|
||||
)
|
||||
except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
||||
self.keyword = parent.add_child(
|
||||
name=f'{name} {str(uuid.uuid4())[0:8]}',
|
||||
description=description,
|
||||
icon=icon,
|
||||
space=request.space
|
||||
)
|
||||
|
||||
|
||||
|
||||
def do_export(self, recipes, el):
|
||||
|
||||
with scope(space=self.request.space):
|
||||
el.total_recipes = len(recipes)
|
||||
el.total_recipes = len(recipes)
|
||||
el.cache_duration = EXPORT_FILE_CACHE_DURATION
|
||||
el.save()
|
||||
|
||||
@@ -80,7 +73,7 @@ class Integration:
|
||||
export_file = file
|
||||
|
||||
else:
|
||||
#zip the files if there is more then one file
|
||||
# zip the files if there is more then one file
|
||||
export_filename = self.get_export_file_name()
|
||||
export_stream = BytesIO()
|
||||
export_obj = ZipFile(export_stream, 'w')
|
||||
@@ -91,8 +84,7 @@ class Integration:
|
||||
export_obj.close()
|
||||
export_file = export_stream.getvalue()
|
||||
|
||||
|
||||
cache.set('export_file_'+str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
|
||||
cache.set('export_file_' + str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
|
||||
el.running = False
|
||||
el.save()
|
||||
|
||||
@@ -100,7 +92,6 @@ class Integration:
|
||||
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
||||
return response
|
||||
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
"""
|
||||
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
|
||||
@@ -164,7 +155,7 @@ class Integration:
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or type(z) == Tag:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
@@ -298,7 +289,6 @@ class Integration:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def get_export_file_name(self, format='zip'):
|
||||
return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
|
||||
|
||||
class Mealie(Integration):
|
||||
@@ -25,7 +25,7 @@ class Mealie(Integration):
|
||||
created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
for s in recipe_json['recipe_instructions']:
|
||||
step = Step.objects.create(instruction=s['text'], space=self.request.space, )
|
||||
step = Step.objects.create(instruction=s['text'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
recipe.steps.add(step)
|
||||
|
||||
step = recipe.steps.first()
|
||||
@@ -56,6 +56,12 @@ class Mealie(Integration):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if 'tags' in recipe_json and len(recipe_json['tags']) > 0:
|
||||
for k in recipe_json['tags']:
|
||||
if 'name' in k:
|
||||
keyword, created = Keyword.objects.get_or_create(name=k['name'].strip(), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
if 'notes' in recipe_json and len(recipe_json['notes']) > 0:
|
||||
notes_text = "#### Notes \n\n"
|
||||
for n in recipe_json['notes']:
|
||||
|
||||
@@ -39,7 +39,7 @@ class MealMaster(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
@@ -57,7 +57,7 @@ class MelaRecipes(Integration):
|
||||
recipe.source_url = recipe_json['link']
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=instruction, space=self.request.space,
|
||||
instruction=instruction, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
@@ -2,13 +2,14 @@ import json
|
||||
import re
|
||||
from io import BytesIO, StringIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step, NutritionInformation
|
||||
from cookbook.models import Ingredient, Keyword, NutritionInformation, Recipe, Step
|
||||
|
||||
|
||||
class NextcloudCookbook(Integration):
|
||||
@@ -51,14 +52,13 @@ class NextcloudCookbook(Integration):
|
||||
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipeInstructions']:
|
||||
instruction_text = ''
|
||||
if 'text' in s:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text'], name=s['name'], space=self.request.space,
|
||||
instruction=s['text'], name=s['name'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
else:
|
||||
step = Step.objects.create(
|
||||
instruction=s, space=self.request.space,
|
||||
instruction=s, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
if not ingredients_added:
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
@@ -91,7 +91,7 @@ class NextcloudCookbook(Integration):
|
||||
if nutrition != {}:
|
||||
recipe.nutrition = NutritionInformation.objects.create(**nutrition, space=self.request.space)
|
||||
recipe.save()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for f in self.files:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step, Keyword, Comment, CookLog
|
||||
from django.utils.translation import gettext as _
|
||||
from cookbook.models import Comment, CookLog, Ingredient, Keyword, Recipe, Step
|
||||
|
||||
|
||||
class OpenEats(Integration):
|
||||
|
||||
@@ -25,16 +27,16 @@ class OpenEats(Integration):
|
||||
if file["source"] != '':
|
||||
instructions += '\n' + _('Recipe source:') + f'[{file["source"]}]({file["source"]})'
|
||||
|
||||
cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
|
||||
cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
|
||||
if file["cuisine"] != '':
|
||||
keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
|
||||
keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
|
||||
if created:
|
||||
keyword.move(cuisine_keyword, pos="last-child")
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
|
||||
course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
|
||||
if file["course"] != '':
|
||||
keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
|
||||
keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
|
||||
if created:
|
||||
keyword.move(course_keyword, pos="last-child")
|
||||
recipe.keywords.add(keyword)
|
||||
@@ -51,7 +53,7 @@ class OpenEats(Integration):
|
||||
recipe.image = f'recipes/openeats-import/{file["photo"]}'
|
||||
recipe.save()
|
||||
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in file['ingredients']:
|
||||
|
||||
@@ -58,7 +58,7 @@ class Paprika(Integration):
|
||||
pass
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=instructions, space=self.request.space,
|
||||
instruction=instructions, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
if 'description' in recipe_json and len(recipe_json['description'].strip()) > 500:
|
||||
@@ -90,7 +90,7 @@ class Paprika(Integration):
|
||||
if validators.url(url, public=True):
|
||||
response = requests.get(url)
|
||||
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||
except:
|
||||
except Exception:
|
||||
if recipe_json.get("photo_data", None):
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
|
||||
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import json
|
||||
from io import BytesIO
|
||||
from re import match
|
||||
from zipfile import ZipFile
|
||||
import asyncio
|
||||
from pyppeteer import launch
|
||||
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.serializer import RecipeExportSerializer
|
||||
|
||||
from cookbook.models import ExportLog
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
import django.core.management.commands.runserver as runserver
|
||||
import logging
|
||||
from asgiref.sync import sync_to_async
|
||||
from pyppeteer import launch
|
||||
|
||||
from cookbook.integration.integration import Integration
|
||||
|
||||
|
||||
class PDFexport(Integration):
|
||||
|
||||
@@ -42,7 +32,6 @@ class PDFexport(Integration):
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
files = []
|
||||
for recipe in recipes:
|
||||
|
||||
@@ -50,20 +39,18 @@ class PDFexport(Integration):
|
||||
await page.emulateMedia('print')
|
||||
await page.setCookie(cookies)
|
||||
|
||||
await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'domcontentloaded'})
|
||||
await page.waitForSelector('#printReady');
|
||||
await page.goto('http://' + cmd.default_addr + ':' + cmd.default_port + '/view/recipe/' + str(recipe.id), {'waitUntil': 'domcontentloaded'})
|
||||
await page.waitForSelector('#printReady')
|
||||
|
||||
files.append([recipe.name + '.pdf', await page.pdf(options)])
|
||||
await page.close();
|
||||
await page.close()
|
||||
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(recipe)
|
||||
await sync_to_async(el.save, thread_sensitive=True)()
|
||||
|
||||
|
||||
await browser.close()
|
||||
return files
|
||||
|
||||
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
return asyncio.run(self.get_files_from_recipes_async(recipes, el, cookie))
|
||||
|
||||
@@ -35,7 +35,7 @@ class Pepperplate(Integration):
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
@@ -46,7 +46,7 @@ class Plantoeat(Integration):
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
if tags:
|
||||
|
||||
@@ -46,7 +46,7 @@ class RecetteTek(Integration):
|
||||
if not instructions:
|
||||
instructions = ''
|
||||
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,)
|
||||
|
||||
# Append the original import url to the step (if it exists)
|
||||
try:
|
||||
|
||||
@@ -41,7 +41,7 @@ class RecipeKeeper(Integration):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
step = Step.objects.create(instruction='', space=self.request.space, )
|
||||
step = Step.objects.create(instruction='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
|
||||
|
||||
@@ -39,7 +39,7 @@ class RecipeSage(Integration):
|
||||
ingredients_added = False
|
||||
for s in file['recipeInstructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text'], space=self.request.space,
|
||||
instruction=s['text'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
|
||||
@@ -2,12 +2,10 @@ import base64
|
||||
from io import BytesIO
|
||||
from xml import etree
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_time, parse_servings, parse_servings_text
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step, Keyword
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
|
||||
|
||||
class Rezeptsuitede(Integration):
|
||||
@@ -37,7 +35,7 @@ class Rezeptsuitede(Integration):
|
||||
try:
|
||||
if prep.find('step').text:
|
||||
step = Step.objects.create(
|
||||
instruction=prep.find('step').text.strip(), space=self.request.space,
|
||||
instruction=prep.find('step').text.strip(), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
recipe.steps.add(step)
|
||||
except Exception:
|
||||
@@ -61,14 +59,14 @@ class Rezeptsuitede(Integration):
|
||||
try:
|
||||
k, created = Keyword.objects.get_or_create(name=recipe_xml.find('head').find('cat').text.strip(), space=self.request.space)
|
||||
recipe.keywords.add(k)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
recipe.save()
|
||||
|
||||
try:
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_xml.find('head').find('picbin').text)), filetype='.jpeg')
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -38,7 +38,7 @@ class RezKonv(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=' \n'.join(directions) + '\n\n', space=self.request.space,
|
||||
instruction=' \n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
@@ -60,8 +60,8 @@ class RezKonv(Integration):
|
||||
def split_recipe_file(self, file):
|
||||
recipe_list = []
|
||||
current_recipe = ''
|
||||
encoding_list = ['windows-1250',
|
||||
'latin-1'] # TODO build algorithm to try trough encodings and fail if none work, use for all importers
|
||||
# TODO build algorithm to try trough encodings and fail if none work, use for all importers
|
||||
# encoding_list = ['windows-1250', 'latin-1']
|
||||
encoding = 'windows-1250'
|
||||
for fl in file.readlines():
|
||||
try:
|
||||
|
||||
@@ -43,7 +43,7 @@ class Saffron(Integration):
|
||||
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, )
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in ingredients:
|
||||
@@ -59,11 +59,11 @@ class Saffron(Integration):
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
|
||||
data = "Title: "+recipe.name if recipe.name else ""+"\n"
|
||||
data += "Description: "+recipe.description if recipe.description else ""+"\n"
|
||||
data = "Title: " + recipe.name if recipe.name else "" + "\n"
|
||||
data += "Description: " + recipe.description if recipe.description else "" + "\n"
|
||||
data += "Source: \n"
|
||||
data += "Original URL: \n"
|
||||
data += "Yield: "+str(recipe.servings)+"\n"
|
||||
data += "Yield: " + str(recipe.servings) + "\n"
|
||||
data += "Cookbook: \n"
|
||||
data += "Section: \n"
|
||||
data += "Image: \n"
|
||||
@@ -78,13 +78,13 @@ class Saffron(Integration):
|
||||
|
||||
data += "Ingredients: \n"
|
||||
for ingredient in recipeIngredient:
|
||||
data += ingredient+"\n"
|
||||
data += ingredient + "\n"
|
||||
|
||||
data += "Instructions: \n"
|
||||
for instruction in recipeInstructions:
|
||||
data += instruction+"\n"
|
||||
data += instruction + "\n"
|
||||
|
||||
return recipe.name+'.txt', data
|
||||
return recipe.name + '.txt', data
|
||||
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
files = []
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
|
||||
"PO-Revision-Date: 2022-07-06 14:32+0000\n"
|
||||
"Last-Translator: Nidhal Brniyah <n1a1b1@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-11-28 11:03+0000\n"
|
||||
"Last-Translator: Mahmoud Aljouhari <mapgohary@gmail.com>\n"
|
||||
"Language-Team: Arabic <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/ar/>\n"
|
||||
"Language: ar\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
|
||||
@@ -2578,7 +2578,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\views\views.py:262
|
||||
msgid "This feature is not available in the demo version!"
|
||||
msgstr ""
|
||||
msgstr "هذه الميزة غير موجودة في النسخة التجريبية!"
|
||||
|
||||
#: .\cookbook\views\views.py:322
|
||||
msgid "You must select at least one field to search!"
|
||||
|
||||
Binary file not shown.
@@ -13,8 +13,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"PO-Revision-Date: 2023-07-06 21:19+0000\n"
|
||||
"Last-Translator: Rubens <rubenixnagios@gmail.com>\n"
|
||||
"Language-Team: Catalan <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/ca/>\n"
|
||||
"Language: ca\n"
|
||||
@@ -421,7 +421,7 @@ msgstr "Compartir Llista de la Compra"
|
||||
|
||||
#: .\cookbook\forms.py:525
|
||||
msgid "Autosync"
|
||||
msgstr "Autosync"
|
||||
msgstr "Autosinc"
|
||||
|
||||
#: .\cookbook\forms.py:526
|
||||
msgid "Auto Add Meal Plan"
|
||||
@@ -477,7 +477,7 @@ msgstr "Mostra el recompte de receptes als filtres de cerca"
|
||||
|
||||
#: .\cookbook\forms.py:559
|
||||
msgid "Use the plural form for units and food inside this space."
|
||||
msgstr ""
|
||||
msgstr "Empra el plural d'aquestes unitats i menjars dins de l'espai."
|
||||
|
||||
#: .\cookbook\helper\AllAuthCustomAdapter.py:39
|
||||
msgid ""
|
||||
|
||||
Binary file not shown.
@@ -11,8 +11,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
||||
"PO-Revision-Date: 2023-03-25 11:32+0000\n"
|
||||
"Last-Translator: Matěj Kubla <matykubla@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-07-31 14:19+0000\n"
|
||||
"Last-Translator: Mára Štěpánek <stepanekm7@gmail.com>\n"
|
||||
"Language-Team: Czech <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/cs/>\n"
|
||||
"Language: cs\n"
|
||||
@@ -36,7 +36,7 @@ msgid ""
|
||||
"try them out!"
|
||||
msgstr ""
|
||||
"Barva horního navigačního menu. Některé barvy neladí se všemi tématy a je "
|
||||
"třeba je vyzkoušet."
|
||||
"třeba je vyzkoušet!"
|
||||
|
||||
#: .\cookbook\forms.py:45
|
||||
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
|
||||
@@ -50,7 +50,7 @@ msgid ""
|
||||
"to fractions automatically)"
|
||||
msgstr ""
|
||||
"Povolit podporu zlomků u množství ingrediencí (desetinná čísla budou "
|
||||
"automaticky převedena na zlomky)."
|
||||
"automaticky převedena na zlomky)"
|
||||
|
||||
#: .\cookbook\forms.py:47
|
||||
msgid ""
|
||||
|
||||
Binary file not shown.
@@ -15,8 +15,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-06-21 14:19+0000\n"
|
||||
"Last-Translator: Tobias Huppertz <tobias.huppertz@mail.de>\n"
|
||||
"PO-Revision-Date: 2023-11-22 18:19+0000\n"
|
||||
"Last-Translator: Spreez <tandoor@larsdev.de>\n"
|
||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/de/>\n"
|
||||
"Language: de\n"
|
||||
@@ -161,7 +161,7 @@ msgstr "Name"
|
||||
|
||||
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
|
||||
msgid "Keywords"
|
||||
msgstr "Stichwörter"
|
||||
msgstr "Schlüsselwörter"
|
||||
|
||||
#: .\cookbook\forms.py:125
|
||||
msgid "Preparation time in minutes"
|
||||
@@ -1436,11 +1436,11 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" <b>Password und Token</b> werden im <b>Klartext</b> in der Datenbank "
|
||||
" <b>Passwort und Token</b> werden im <b>Klartext</b> in der Datenbank "
|
||||
"gespeichert.\n"
|
||||
" Dies ist notwendig da Passwort oder Token benötigt werden, um API-"
|
||||
"Anfragen zu stellen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/"
|
||||
">\n"
|
||||
"Anfragen zu stellen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/>"
|
||||
"\n"
|
||||
" Um das Risiko zu minimieren sollten, wenn möglich, Tokens oder "
|
||||
"Accounts mit limitiertem Zugriff verwendet werden.\n"
|
||||
" "
|
||||
@@ -2600,7 +2600,7 @@ msgstr "Ungültiges URL Schema."
|
||||
|
||||
#: .\cookbook\views\api.py:1233
|
||||
msgid "No usable data could be found."
|
||||
msgstr "Es konnten keine nutzbaren Daten gefunden werden."
|
||||
msgstr "Es konnten keine passenden Daten gefunden werden."
|
||||
|
||||
#: .\cookbook\views\api.py:1326 .\cookbook\views\import_export.py:117
|
||||
msgid "Importing is not implemented for this provider"
|
||||
|
||||
Binary file not shown.
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"PO-Revision-Date: 2023-06-23 09:19+0000\n"
|
||||
"Last-Translator: sweeney <sweeneytodd91@protonmail.com>\n"
|
||||
"PO-Revision-Date: 2023-08-21 09:19+0000\n"
|
||||
"Last-Translator: Theodoros Grammenos <teogramm@outlook.com>\n"
|
||||
"Language-Team: Greek <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/el/>\n"
|
||||
"Language: el\n"
|
||||
@@ -22,7 +22,7 @@ msgstr ""
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\stats.html:28
|
||||
msgid "Ingredients"
|
||||
msgstr "Συστατικά"
|
||||
msgstr "Υλικά"
|
||||
|
||||
#: .\cookbook\forms.py:53
|
||||
msgid "Default unit"
|
||||
@@ -66,7 +66,7 @@ msgstr "Κοινοποίηση προγράμματος"
|
||||
|
||||
#: .\cookbook\forms.py:63
|
||||
msgid "Ingredient decimal places"
|
||||
msgstr ""
|
||||
msgstr "Δεκαδικά ψηφία υλικών"
|
||||
|
||||
#: .\cookbook\forms.py:64
|
||||
msgid "Shopping list auto sync period"
|
||||
@@ -262,6 +262,8 @@ msgid ""
|
||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
||||
"\">docs here</a>"
|
||||
msgstr ""
|
||||
"Μπορείτε να χρησιμοποιήσετε τη μορφοποίηση Markdown για να διαμορφώσετε αυτό "
|
||||
"το πεδίο. Δείτε τα <a href=\"/docs/markdown/\">έγγραφα εδώ</a>"
|
||||
|
||||
#: .\cookbook\forms.py:366
|
||||
msgid "Maximum number of users for this space reached."
|
||||
@@ -309,6 +311,8 @@ msgid ""
|
||||
"Use fuzzy matching on units, keywords and ingredients when editing and "
|
||||
"importing recipes."
|
||||
msgstr ""
|
||||
"Χρησιμοποιήστε ασαφείς (fuzzy) αντιστοιχίες σε μονάδες μέτρησης, λέξεις-"
|
||||
"κλειδιά και συστατικά κατά την επεξεργασία και εισαγωγή συνταγών."
|
||||
|
||||
#: .\cookbook\forms.py:451
|
||||
msgid ""
|
||||
@@ -323,6 +327,8 @@ msgid ""
|
||||
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
|
||||
"'pie' and 'piece' and 'soapie')"
|
||||
msgstr ""
|
||||
"Πεδία για αναζήτηση μερικών αντιστοιχιών. (π.χ. αναζήτηση για 'πίτα' τα "
|
||||
"'τυρόπιτα' και 'απιτα' θα βρίσκονται στα αποτελέσματα)"
|
||||
|
||||
#: .\cookbook\forms.py:455
|
||||
msgid ""
|
||||
@@ -420,7 +426,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:506
|
||||
msgid "Delimiter to use for CSV exports."
|
||||
msgstr ""
|
||||
msgstr "Το σημείο στίξης διαχωρισμού δεκαδικών για τις εξαγωγές σε αρχεία CSV."
|
||||
|
||||
#: .\cookbook\forms.py:507
|
||||
msgid "Prefix to add when copying list to the clipboard."
|
||||
@@ -454,7 +460,7 @@ msgstr "Προεπιλεγμένες ώρες καθυστέρησης"
|
||||
|
||||
#: .\cookbook\forms.py:517
|
||||
msgid "Filter to Supermarket"
|
||||
msgstr ""
|
||||
msgstr "Ταξινόμηση ανά Supermarket"
|
||||
|
||||
#: .\cookbook\forms.py:518
|
||||
msgid "Recent Days"
|
||||
@@ -462,7 +468,7 @@ msgstr "Πρόσφατες ημέρες"
|
||||
|
||||
#: .\cookbook\forms.py:519
|
||||
msgid "CSV Delimiter"
|
||||
msgstr ""
|
||||
msgstr "CSV σημείο στίξης διαχωρισμού δεκαδικών"
|
||||
|
||||
#: .\cookbook\forms.py:520
|
||||
msgid "List Prefix"
|
||||
@@ -474,7 +480,7 @@ msgstr "Αυτόματα διαθέσιμο"
|
||||
|
||||
#: .\cookbook\forms.py:531
|
||||
msgid "Reset Food Inheritance"
|
||||
msgstr ""
|
||||
msgstr "Επαναφορά κληρονομιάς φαγητών"
|
||||
|
||||
#: .\cookbook\forms.py:532
|
||||
msgid "Reset all food to inherit the fields configured."
|
||||
@@ -531,7 +537,7 @@ msgstr "Έχετε περισσότερους χρήστες από το επι
|
||||
|
||||
#: .\cookbook\helper\recipe_search.py:565
|
||||
msgid "One of queryset or hash_key must be provided"
|
||||
msgstr ""
|
||||
msgstr "Πρέπει να παρέχετε είτε το queryset είτε το hash_key"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:152
|
||||
msgid "You must supply a servings size"
|
||||
@@ -619,6 +625,8 @@ msgstr "Αναδόμηση πλήρους ευρετηρίου αναζήτησ
|
||||
#: .\cookbook\management\commands\rebuildindex.py:18
|
||||
msgid "Only Postgresql databases use full text search, no index to rebuild"
|
||||
msgstr ""
|
||||
"Μόνο οι βάσεις δεδομένων Postgresql χρησιμοποιούν αναζήτηση πλήρους "
|
||||
"κειμένου, δεν υπάρχει ανάγκη ανασύνθεσης των ευρετηρίων"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:29
|
||||
msgid "Recipe index rebuild complete."
|
||||
@@ -780,13 +788,15 @@ msgstr "Πρόσκληση στο Tandoor Recipes"
|
||||
|
||||
#: .\cookbook\serializer.py:1209
|
||||
msgid "Existing shopping list to update"
|
||||
msgstr ""
|
||||
msgstr "Υπάρχουσα λίστα αγορών για ενημέρωση"
|
||||
|
||||
#: .\cookbook\serializer.py:1211
|
||||
msgid ""
|
||||
"List of ingredient IDs from the recipe to add, if not provided all "
|
||||
"ingredients will be added."
|
||||
msgstr ""
|
||||
"Λίστα αναγνωριστικών συστατικών (ID) από τη συνταγή προς προσθήκη. Εάν δεν "
|
||||
"παρέχονται όλα τα συστατικά θα προστεθούν."
|
||||
|
||||
#: .\cookbook\serializer.py:1213
|
||||
msgid ""
|
||||
@@ -795,21 +805,23 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\serializer.py:1222
|
||||
msgid "Amount of food to add to the shopping list"
|
||||
msgstr ""
|
||||
msgstr "Ποσότητα του φαγητού που θα προστεθεί στη λίστα αγορών"
|
||||
|
||||
#: .\cookbook\serializer.py:1224
|
||||
msgid "ID of unit to use for the shopping list"
|
||||
msgstr ""
|
||||
msgstr "Το ID της μονάδας μέτρησης που θα χρησιμοποιείται στη λίστα αγορών"
|
||||
|
||||
#: .\cookbook\serializer.py:1226
|
||||
msgid "When set to true will delete all food from active shopping lists."
|
||||
msgstr ""
|
||||
"Όταν οριστεί σε true, θα διαγραφούν όλα τα τρόφιμα από τις ενεργές λίστες "
|
||||
"αγορών."
|
||||
|
||||
#: .\cookbook\tables.py:36 .\cookbook\templates\generic\edit_template.html:6
|
||||
#: .\cookbook\templates\generic\edit_template.html:14
|
||||
#: .\cookbook\templates\recipes_table.html:82
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Τροποποίηση"
|
||||
|
||||
#: .\cookbook\tables.py:116 .\cookbook\tables.py:131
|
||||
#: .\cookbook\templates\generic\delete_template.html:7
|
||||
@@ -817,28 +829,28 @@ msgstr ""
|
||||
#: .\cookbook\templates\generic\edit_template.html:28
|
||||
#: .\cookbook\templates\recipes_table.html:90
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
msgstr "Διαγραφή"
|
||||
|
||||
#: .\cookbook\templates\404.html:5
|
||||
msgid "404 Error"
|
||||
msgstr ""
|
||||
msgstr "404 Error"
|
||||
|
||||
#: .\cookbook\templates\404.html:18
|
||||
msgid "The page you are looking for could not be found."
|
||||
msgstr ""
|
||||
msgstr "Η σελίδα που αναζητάτε δεν μπορεί να βρεθεί."
|
||||
|
||||
#: .\cookbook\templates\404.html:33
|
||||
msgid "Take me Home"
|
||||
msgstr ""
|
||||
msgstr "Πήγαινε με στη αρχική σελίδα"
|
||||
|
||||
#: .\cookbook\templates\404.html:35
|
||||
msgid "Report a Bug"
|
||||
msgstr ""
|
||||
msgstr "Αναφορά σφάλματος"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:6
|
||||
#: .\cookbook\templates\account\email.html:17
|
||||
msgid "E-mail Addresses"
|
||||
msgstr ""
|
||||
msgstr "Διευθύνσεις e-mail"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:12
|
||||
#: .\cookbook\templates\account\password_change.html:11
|
||||
@@ -847,68 +859,71 @@ msgstr ""
|
||||
#: .\cookbook\templates\settings.html:17
|
||||
#: .\cookbook\templates\socialaccount\connections.html:10
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
msgstr "Ρυθμίσεις"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:13
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
msgstr "Email"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:19
|
||||
msgid "The following e-mail addresses are associated with your account:"
|
||||
msgstr ""
|
||||
msgstr "Οι παρακάτω διευθύνσεις e-mail συνδέονται με τον λογαριασμό σας:"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:36
|
||||
msgid "Verified"
|
||||
msgstr ""
|
||||
msgstr "Πιστοποιημένο"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:38
|
||||
msgid "Unverified"
|
||||
msgstr ""
|
||||
msgstr "Μη πιστοποιημένο"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:40
|
||||
msgid "Primary"
|
||||
msgstr ""
|
||||
msgstr "Κύριο"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:47
|
||||
msgid "Make Primary"
|
||||
msgstr ""
|
||||
msgstr "Μετατροπή σε κύριο"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:49
|
||||
msgid "Re-send Verification"
|
||||
msgstr ""
|
||||
msgstr "Επαναποστολή της επαλήθευσης"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:50
|
||||
#: .\cookbook\templates\generic\delete_template.html:57
|
||||
#: .\cookbook\templates\socialaccount\connections.html:44
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
msgstr "Αφαίρεση"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:58
|
||||
msgid "Warning:"
|
||||
msgstr ""
|
||||
msgstr "Προειδοποίηση:"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:58
|
||||
msgid ""
|
||||
"You currently do not have any e-mail address set up. You should really add "
|
||||
"an e-mail address so you can receive notifications, reset your password, etc."
|
||||
msgstr ""
|
||||
"Προς το παρόν, δεν έχετε καμία διεύθυνση e-mail καταχωρημένη. Θα πρέπει να "
|
||||
"προσθέσετε μια διεύθυνση ηλεκτρονικού ταχυδρομείου, ώστε να μπορείτε να "
|
||||
"λαμβάνετε ειδοποιήσεις, να επαναφέρετε τον κωδικό πρόσβασης, κ.λπ."
|
||||
|
||||
#: .\cookbook\templates\account\email.html:64
|
||||
msgid "Add E-mail Address"
|
||||
msgstr ""
|
||||
msgstr "Προσθήκη διεύθυνσης e-mail"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:69
|
||||
msgid "Add E-mail"
|
||||
msgstr ""
|
||||
msgstr "Προσθήκη e-mail"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:79
|
||||
msgid "Do you really want to remove the selected e-mail address?"
|
||||
msgstr ""
|
||||
msgstr "Θέλετε πραγματικά να αφαιρέσετε την επιλεγμένη διεύθυνση e-mail;"
|
||||
|
||||
#: .\cookbook\templates\account\email_confirm.html:6
|
||||
#: .\cookbook\templates\account\email_confirm.html:10
|
||||
msgid "Confirm E-mail Address"
|
||||
msgstr ""
|
||||
msgstr "Επιβεβαίωση διεύθυνσης e-mail"
|
||||
|
||||
#: .\cookbook\templates\account\email_confirm.html:16
|
||||
#, python-format
|
||||
@@ -918,11 +933,15 @@ msgid ""
|
||||
"for user %(user_display)s\n"
|
||||
" ."
|
||||
msgstr ""
|
||||
"Παρακαλώ επιβεβαιώστε ότι το\n"
|
||||
" <a href=\"mailto:%(email)s\">%(email)s</a> είναι μια διεύθυνση "
|
||||
"e-mail για τον χρήστη %(user_display)s\n"
|
||||
" ."
|
||||
|
||||
#: .\cookbook\templates\account\email_confirm.html:22
|
||||
#: .\cookbook\templates\generic\delete_template.html:72
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
msgstr "Επιβεβαίωση"
|
||||
|
||||
#: .\cookbook\templates\account\email_confirm.html:29
|
||||
#, python-format
|
||||
@@ -931,11 +950,15 @@ msgid ""
|
||||
" <a href=\"%(email_url)s\">issue a new e-mail confirmation "
|
||||
"request</a>."
|
||||
msgstr ""
|
||||
"Αυτός ο σύνδεσμος επιβεβαίωσης έχει λήξει είναι δεν είναι έγκυρος. Παρακαλώ "
|
||||
"\n"
|
||||
" <a href=\"%(email_url)s\">κάντε ένα νέο αίτημα για επιβεβαιωτικό "
|
||||
"e-mail</a>."
|
||||
|
||||
#: .\cookbook\templates\account\login.html:8
|
||||
#: .\cookbook\templates\base.html:340 .\cookbook\templates\openid\login.html:8
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
msgstr "Σύνδεση"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:15
|
||||
#: .\cookbook\templates\account\login.html:31
|
||||
@@ -945,41 +968,43 @@ msgstr ""
|
||||
#: .\cookbook\templates\openid\login.html:26
|
||||
#: .\cookbook\templates\socialaccount\authentication_error.html:15
|
||||
msgid "Sign In"
|
||||
msgstr ""
|
||||
msgstr "Σύνδεση"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:34
|
||||
#: .\cookbook\templates\socialaccount\signup.html:8
|
||||
#: .\cookbook\templates\socialaccount\signup.html:57
|
||||
msgid "Sign Up"
|
||||
msgstr ""
|
||||
msgstr "Εγγραφή"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:39
|
||||
#: .\cookbook\templates\account\login.html:41
|
||||
#: .\cookbook\templates\account\password_reset.html:29
|
||||
msgid "Reset My Password"
|
||||
msgstr ""
|
||||
msgstr "Επαναφορά κωδικού πρόσβασης"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:40
|
||||
msgid "Lost your password?"
|
||||
msgstr ""
|
||||
msgstr "Χασάτε τον κωδικό πρόσβασης;"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:52
|
||||
msgid "Social Login"
|
||||
msgstr ""
|
||||
msgstr "Σύνδεση με social media"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:53
|
||||
msgid "You can use any of the following providers to sign in."
|
||||
msgstr ""
|
||||
"Μπορείτε να χρησιμοποιήσετε οποιονδήποτε από τους παρακάτω παρόχους για να "
|
||||
"συνδεθείτε."
|
||||
|
||||
#: .\cookbook\templates\account\logout.html:5
|
||||
#: .\cookbook\templates\account\logout.html:9
|
||||
#: .\cookbook\templates\account\logout.html:18
|
||||
msgid "Sign Out"
|
||||
msgstr ""
|
||||
msgstr "Αποσύνδεση"
|
||||
|
||||
#: .\cookbook\templates\account\logout.html:11
|
||||
msgid "Are you sure you want to sign out?"
|
||||
msgstr ""
|
||||
msgstr "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;"
|
||||
|
||||
#: .\cookbook\templates\account\password_change.html:6
|
||||
#: .\cookbook\templates\account\password_change.html:16
|
||||
@@ -989,44 +1014,50 @@ msgstr ""
|
||||
#: .\cookbook\templates\account\password_reset_from_key_done.html:7
|
||||
#: .\cookbook\templates\account\password_reset_from_key_done.html:13
|
||||
msgid "Change Password"
|
||||
msgstr ""
|
||||
msgstr "Αλλαγή κωδικού πρόσβασης"
|
||||
|
||||
#: .\cookbook\templates\account\password_change.html:12
|
||||
#: .\cookbook\templates\account\password_set.html:12
|
||||
#: .\cookbook\templates\settings.html:76
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
msgstr "Κωδικός πρόσβασης"
|
||||
|
||||
#: .\cookbook\templates\account\password_change.html:22
|
||||
msgid "Forgot Password?"
|
||||
msgstr ""
|
||||
msgstr "Ξεχάσατε τον κωδικό πρόσβασης;"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset.html:7
|
||||
#: .\cookbook\templates\account\password_reset.html:13
|
||||
#: .\cookbook\templates\account\password_reset_done.html:7
|
||||
#: .\cookbook\templates\account\password_reset_done.html:10
|
||||
msgid "Password Reset"
|
||||
msgstr ""
|
||||
msgstr "Επαναφορά κωδικού πρόσβασης"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset.html:24
|
||||
msgid ""
|
||||
"Forgotten your password? Enter your e-mail address below, and we'll send you "
|
||||
"an e-mail allowing you to reset it."
|
||||
msgstr ""
|
||||
"Ξεχάσατε τον κωδικό πρόσβασης σας; Εισάγετε τη διεύθυνση ηλεκτρονικού "
|
||||
"ταχυδρομείου σας παρακάτω και θα σας στείλουμε ένα email που θα σας "
|
||||
"επιτρέψει να τον επαναφέρετε."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset.html:32
|
||||
msgid "Password reset is disabled on this instance."
|
||||
msgstr ""
|
||||
"Η επαναφορά κωδικού πρόσβασης είναι απενεργοποιημένη σε αυτήν την πλατφόρμα."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_done.html:16
|
||||
msgid ""
|
||||
"We have sent you an e-mail. Please contact us if you do not receive it "
|
||||
"within a few minutes."
|
||||
msgstr ""
|
||||
"Σας έχουμε στείλει ένα email. Παρακαλούμε επικοινωνήστε μαζί μας αν δεν το "
|
||||
"λάβετε εντός λίγων λεπτών."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:13
|
||||
msgid "Bad Token"
|
||||
msgstr ""
|
||||
msgstr "Μη έγκυρο token"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:25
|
||||
#, python-format
|
||||
@@ -1036,168 +1067,172 @@ msgid ""
|
||||
" Please request a <a href=\"%(passwd_reset_url)s\">new "
|
||||
"password reset</a>."
|
||||
msgstr ""
|
||||
"Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης ήταν άκυρος, πιθανώς επειδή έχει "
|
||||
"ήδη χρησιμοποιηθεί.\n"
|
||||
" Παρακαλώ ζητήστε έναν <a href=\"%(passwd_reset_url)s\""
|
||||
">νέο σύνδεσμο επαναφοράς κωδικού πρόσβασης</a>."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:33
|
||||
msgid "change password"
|
||||
msgstr ""
|
||||
msgstr "Αλλαγή κωδικού πρόσβασης"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:36
|
||||
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
|
||||
msgid "Your password is now changed."
|
||||
msgstr ""
|
||||
msgstr "Ο κωδικός πρόσβασης σας έχει αλλάξει."
|
||||
|
||||
#: .\cookbook\templates\account\password_set.html:6
|
||||
#: .\cookbook\templates\account\password_set.html:16
|
||||
#: .\cookbook\templates\account\password_set.html:21
|
||||
msgid "Set Password"
|
||||
msgstr ""
|
||||
msgstr "Ορισμός Κωδικού Πρόσβασης"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:6
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
msgstr "Εγγραφή"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:12
|
||||
msgid "Create an Account"
|
||||
msgstr ""
|
||||
msgstr "Δημιουργία λογαριασμού"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:42
|
||||
#: .\cookbook\templates\socialaccount\signup.html:33
|
||||
msgid "I accept the follwoing"
|
||||
msgstr ""
|
||||
msgstr "Αποδέχομαι τα παρακάτω"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:45
|
||||
#: .\cookbook\templates\socialaccount\signup.html:36
|
||||
msgid "Terms and Conditions"
|
||||
msgstr ""
|
||||
msgstr "Όροι και προϋποθέσεις"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:48
|
||||
#: .\cookbook\templates\socialaccount\signup.html:39
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
msgstr "και"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:52
|
||||
#: .\cookbook\templates\socialaccount\signup.html:43
|
||||
msgid "Privacy Policy"
|
||||
msgstr ""
|
||||
msgstr "Πολιτική απορρήτου"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:65
|
||||
msgid "Create User"
|
||||
msgstr ""
|
||||
msgstr "Δημιουργία χρήστη"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:69
|
||||
msgid "Already have an account?"
|
||||
msgstr ""
|
||||
msgstr "Έχετε ήδη λογαριασμό;"
|
||||
|
||||
#: .\cookbook\templates\account\signup_closed.html:5
|
||||
#: .\cookbook\templates\account\signup_closed.html:11
|
||||
msgid "Sign Up Closed"
|
||||
msgstr ""
|
||||
msgstr "Οι εγγραφές έκλεισαν"
|
||||
|
||||
#: .\cookbook\templates\account\signup_closed.html:13
|
||||
msgid "We are sorry, but the sign up is currently closed."
|
||||
msgstr ""
|
||||
msgstr "Λυπούμαστε, αλλά οι εγγραφές έχουν ήδη κλείσει."
|
||||
|
||||
#: .\cookbook\templates\api_info.html:5 .\cookbook\templates\base.html:330
|
||||
#: .\cookbook\templates\rest_framework\api.html:11
|
||||
msgid "API Documentation"
|
||||
msgstr ""
|
||||
msgstr "Τεκμηρίωση API"
|
||||
|
||||
#: .\cookbook\templates\base.html:103 .\cookbook\templates\index.html:87
|
||||
#: .\cookbook\templates\stats.html:22
|
||||
msgid "Recipes"
|
||||
msgstr ""
|
||||
msgstr "Συνταγές"
|
||||
|
||||
#: .\cookbook\templates\base.html:111
|
||||
msgid "Shopping"
|
||||
msgstr ""
|
||||
msgstr "Αγορές"
|
||||
|
||||
#: .\cookbook\templates\base.html:150 .\cookbook\views\lists.py:105
|
||||
msgid "Foods"
|
||||
msgstr ""
|
||||
msgstr "Φαγητά"
|
||||
|
||||
#: .\cookbook\templates\base.html:162
|
||||
#: .\cookbook\templates\forms\ingredients.html:24
|
||||
#: .\cookbook\templates\stats.html:26 .\cookbook\views\lists.py:122
|
||||
msgid "Units"
|
||||
msgstr ""
|
||||
msgstr "Μονάδες μέτρησης"
|
||||
|
||||
#: .\cookbook\templates\base.html:176 .\cookbook\templates\supermarket.html:7
|
||||
msgid "Supermarket"
|
||||
msgstr ""
|
||||
msgstr "Supermarket"
|
||||
|
||||
#: .\cookbook\templates\base.html:188
|
||||
msgid "Supermarket Category"
|
||||
msgstr ""
|
||||
msgstr "Κατηγορία Supermarket"
|
||||
|
||||
#: .\cookbook\templates\base.html:200 .\cookbook\views\lists.py:171
|
||||
msgid "Automations"
|
||||
msgstr ""
|
||||
msgstr "Αυτοματισμοί"
|
||||
|
||||
#: .\cookbook\templates\base.html:214 .\cookbook\views\lists.py:207
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
msgstr "Αρχεία"
|
||||
|
||||
#: .\cookbook\templates\base.html:226
|
||||
msgid "Batch Edit"
|
||||
msgstr ""
|
||||
msgstr "Μαζική Επεξεργασία"
|
||||
|
||||
#: .\cookbook\templates\base.html:238 .\cookbook\templates\history.html:6
|
||||
#: .\cookbook\templates\history.html:14
|
||||
msgid "History"
|
||||
msgstr ""
|
||||
msgstr "Ιστορικό"
|
||||
|
||||
#: .\cookbook\templates\base.html:252
|
||||
#: .\cookbook\templates\ingredient_editor.html:7
|
||||
#: .\cookbook\templates\ingredient_editor.html:13
|
||||
msgid "Ingredient Editor"
|
||||
msgstr ""
|
||||
msgstr "Επεξεργαστής Συστατικών"
|
||||
|
||||
#: .\cookbook\templates\base.html:264
|
||||
#: .\cookbook\templates\export_response.html:7
|
||||
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
msgstr "Εξαγωγή"
|
||||
|
||||
#: .\cookbook\templates\base.html:280 .\cookbook\templates\index.html:47
|
||||
msgid "Import Recipe"
|
||||
msgstr ""
|
||||
msgstr "Εισαγωγή συνταγής"
|
||||
|
||||
#: .\cookbook\templates\base.html:282
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
msgstr "Δημιουργία"
|
||||
|
||||
#: .\cookbook\templates\base.html:295
|
||||
#: .\cookbook\templates\generic\list_template.html:14
|
||||
#: .\cookbook\templates\stats.html:43
|
||||
msgid "External Recipes"
|
||||
msgstr ""
|
||||
msgstr "Εξωτερικές Συνταγές"
|
||||
|
||||
#: .\cookbook\templates\base.html:298
|
||||
#: .\cookbook\templates\space_manage.html:15
|
||||
msgid "Space Settings"
|
||||
msgstr ""
|
||||
msgstr "Ρυθμίσεις χώρου"
|
||||
|
||||
#: .\cookbook\templates\base.html:303 .\cookbook\templates\system.html:13
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
msgstr "Σύστημα"
|
||||
|
||||
#: .\cookbook\templates\base.html:305
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
msgstr "Διαχειριστής"
|
||||
|
||||
#: .\cookbook\templates\base.html:309
|
||||
#: .\cookbook\templates\space_overview.html:25
|
||||
msgid "Your Spaces"
|
||||
msgstr ""
|
||||
msgstr "Οι χώροι σας"
|
||||
|
||||
#: .\cookbook\templates\base.html:320
|
||||
#: .\cookbook\templates\space_overview.html:6
|
||||
msgid "Overview"
|
||||
msgstr ""
|
||||
msgstr "Σύνοψη"
|
||||
|
||||
#: .\cookbook\templates\base.html:324
|
||||
msgid "Markdown Guide"
|
||||
msgstr ""
|
||||
msgstr "Οδηγός χρήσης του Markdown"
|
||||
|
||||
#: .\cookbook\templates\base.html:326
|
||||
msgid "GitHub"
|
||||
@@ -1205,53 +1240,57 @@ msgstr "GitHub"
|
||||
|
||||
#: .\cookbook\templates\base.html:328
|
||||
msgid "Translate Tandoor"
|
||||
msgstr ""
|
||||
msgstr "Μεταφράστε το Tandoor"
|
||||
|
||||
#: .\cookbook\templates\base.html:332
|
||||
msgid "API Browser"
|
||||
msgstr ""
|
||||
msgstr "Περιηγητής API"
|
||||
|
||||
#: .\cookbook\templates\base.html:335
|
||||
msgid "Log out"
|
||||
msgstr ""
|
||||
msgstr "Αποσύνδεση"
|
||||
|
||||
#: .\cookbook\templates\base.html:357
|
||||
msgid "You are using the free version of Tandor"
|
||||
msgstr ""
|
||||
msgstr "Χρησιμοποιείται την δωρεάν έκδοση του Tandoor"
|
||||
|
||||
#: .\cookbook\templates\base.html:358
|
||||
msgid "Upgrade Now"
|
||||
msgstr ""
|
||||
msgstr "Αναβαθμιστείτε τώρα"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:6
|
||||
msgid "Batch edit Category"
|
||||
msgstr ""
|
||||
msgstr "Μαζική τροποποίηση κατηγοριών"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:15
|
||||
msgid "Batch edit Recipes"
|
||||
msgstr ""
|
||||
msgstr "Μαζική τροποποίηση Συνταγών"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:20
|
||||
msgid "Add the specified keywords to all recipes containing a word"
|
||||
msgstr ""
|
||||
"Προσθέστε τις καθορισμένες λέξεις-κλειδιά σε όλες τις συνταγές που περιέχουν "
|
||||
"μια λέξη"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:73
|
||||
msgid "Sync"
|
||||
msgstr ""
|
||||
msgstr "Συγχρονισμός"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:10
|
||||
msgid "Manage watched Folders"
|
||||
msgstr ""
|
||||
msgstr "Διαχείριση φακέλων που έχουν προβληθεί"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:14
|
||||
msgid ""
|
||||
"On this Page you can manage all storage folder locations that should be "
|
||||
"monitored and synced."
|
||||
msgstr ""
|
||||
"Σε αυτήν τη σελίδα μπορείτε να διαχειριστείτε όλες τις τοποθεσίες "
|
||||
"αποθήκευσης φακέλων που πρέπει να παρακολουθούνται και να συγχρονίζονται."
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:16
|
||||
msgid "The path must be in the following format"
|
||||
msgstr ""
|
||||
msgstr "Η διαδρομή (path) πρέπει να είναι στην ακόλουθη μορφή"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:20
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:14
|
||||
@@ -1263,55 +1302,57 @@ msgstr ""
|
||||
#: .\cookbook\templates\settings.html:202
|
||||
#: .\cookbook\templates\settings.html:213
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
msgstr "Αποθήκευση"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:21
|
||||
msgid "Manage External Storage"
|
||||
msgstr ""
|
||||
msgstr "Διαχείριση εξωτερικού χώρου αποθήκευσης"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:28
|
||||
msgid "Sync Now!"
|
||||
msgstr ""
|
||||
msgstr "Συγχρονισμός τώρα!"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:29
|
||||
msgid "Show Recipes"
|
||||
msgstr ""
|
||||
msgstr "Προβολή Συνταγών"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:30
|
||||
msgid "Show Log"
|
||||
msgstr ""
|
||||
msgstr "Προβολή αρχείων καταγραφής"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:4
|
||||
#: .\cookbook\templates\batch\waiting.html:10
|
||||
msgid "Importing Recipes"
|
||||
msgstr ""
|
||||
msgstr "Οι συνταγές εισάγονται"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:28
|
||||
msgid ""
|
||||
"This can take a few minutes, depending on the number of recipes in sync, "
|
||||
"please wait."
|
||||
msgstr ""
|
||||
"Αυτή η διαδικασία μπορεί να πάρει μερικά λεπτά, ανάλογα με τον αριθμό των "
|
||||
"συνταγών που πρέπει να συγχρονιστούν, παρακαλώ περιμένετε."
|
||||
|
||||
#: .\cookbook\templates\books.html:7
|
||||
msgid "Recipe Books"
|
||||
msgstr ""
|
||||
msgstr "Βιβλία Συνταγών"
|
||||
|
||||
#: .\cookbook\templates\export.html:8 .\cookbook\templates\test2.html:6
|
||||
msgid "Export Recipes"
|
||||
msgstr ""
|
||||
msgstr "Εξαγωγή Συνταγών"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:5
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:9
|
||||
msgid "Import new Recipe"
|
||||
msgstr ""
|
||||
msgstr "Εισαγωγή μια νέας συνταγή"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:7
|
||||
msgid "Edit Recipe"
|
||||
msgstr ""
|
||||
msgstr "Τροποποίηση συνταγής"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:15
|
||||
msgid "Edit Ingredients"
|
||||
msgstr ""
|
||||
msgstr "Τροποποίηση υλικών"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:16
|
||||
msgid ""
|
||||
@@ -1323,32 +1364,41 @@ msgid ""
|
||||
"them.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Την παρακάτω φόρμα μπορεί να χρησιμοποιηθεί στην περίπτωση που, κατά "
|
||||
"λάθος, δημιουργήθηκαν δύο (ή περισσότερες) μονάδες μέτρησης ή συστατικά που "
|
||||
"θα έπρεπε να είναι\n"
|
||||
" τα ίδια.\n"
|
||||
" Αυτή η φόρμα συγχωνεύει δύο μονάδες ή συστατικά και ενημερώνει όλες "
|
||||
"τις συνταγές που τα χρησιμοποιούν.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:26
|
||||
msgid "Are you sure that you want to merge these two units?"
|
||||
msgstr ""
|
||||
msgstr "Είστε βέβαιος ότι θέλετε να συγχωνεύσετε αυτές τις δύο μονάδες;"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:31
|
||||
#: .\cookbook\templates\forms\ingredients.html:40
|
||||
msgid "Merge"
|
||||
msgstr ""
|
||||
msgstr "Συγχώνευση"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:36
|
||||
msgid "Are you sure that you want to merge these two ingredients?"
|
||||
msgstr ""
|
||||
msgstr "Είστε βέβαιος ότι θέλετε να συγχωνεύσετε αυτά τα δύο υλικά;"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:21
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
|
||||
msgstr ""
|
||||
"Είστε σίγουροι ότι θέλετε να διαγράψετε τα %(title)s: <b>%(object)s</b> "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:22
|
||||
msgid "This cannot be undone!"
|
||||
msgstr ""
|
||||
msgstr "Αυτό δεν μπορεί να αναιρεθεί!"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:27
|
||||
msgid "Protected"
|
||||
msgstr ""
|
||||
msgstr "Προστατευμένο"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:42
|
||||
msgid "Cascade"
|
||||
@@ -1356,68 +1406,68 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:73
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
msgstr "Ακύρωση"
|
||||
|
||||
#: .\cookbook\templates\generic\edit_template.html:32
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
msgstr "Προβολή"
|
||||
|
||||
#: .\cookbook\templates\generic\edit_template.html:36
|
||||
msgid "Delete original file"
|
||||
msgstr ""
|
||||
msgstr "Διαγραφή πρωτότυπου αρχείου"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:6
|
||||
#: .\cookbook\templates\generic\list_template.html:22
|
||||
msgid "List"
|
||||
msgstr ""
|
||||
msgstr "Λίστα"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:36
|
||||
msgid "Filter"
|
||||
msgstr ""
|
||||
msgstr "Φίλτρο"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:41
|
||||
msgid "Import all"
|
||||
msgstr ""
|
||||
msgstr "Εισαγωγή όλων"
|
||||
|
||||
#: .\cookbook\templates\generic\table_template.html:76
|
||||
#: .\cookbook\templates\recipes_table.html:121
|
||||
msgid "previous"
|
||||
msgstr ""
|
||||
msgstr "προηγούμενο"
|
||||
|
||||
#: .\cookbook\templates\generic\table_template.html:98
|
||||
#: .\cookbook\templates\recipes_table.html:143
|
||||
msgid "next"
|
||||
msgstr ""
|
||||
msgstr "επόμενο"
|
||||
|
||||
#: .\cookbook\templates\history.html:20
|
||||
msgid "View Log"
|
||||
msgstr ""
|
||||
msgstr "Προβολή αρχείων καταγραφής"
|
||||
|
||||
#: .\cookbook\templates\history.html:24
|
||||
msgid "Cook Log"
|
||||
msgstr ""
|
||||
msgstr "Αρχείο καταγραφής μαγειρέματος"
|
||||
|
||||
#: .\cookbook\templates\import.html:6
|
||||
msgid "Import Recipes"
|
||||
msgstr ""
|
||||
msgstr "Εισαγωγή Συνταγών"
|
||||
|
||||
#: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20
|
||||
#: .\cookbook\templates\import_response.html:7 .\cookbook\views\delete.py:86
|
||||
#: .\cookbook\views\edit.py:191
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
msgstr "Εισαγωγή"
|
||||
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:18
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
msgstr "Κλείσιμο"
|
||||
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:32
|
||||
msgid "Open Recipe"
|
||||
msgstr ""
|
||||
msgstr "Άνοιγμα Συνταγής"
|
||||
|
||||
#: .\cookbook\templates\include\storage_backend_warning.html:4
|
||||
msgid "Security Warning"
|
||||
msgstr ""
|
||||
msgstr "Προειδοποίηση ασφαλείας"
|
||||
|
||||
#: .\cookbook\templates\include\storage_backend_warning.html:5
|
||||
msgid ""
|
||||
@@ -1434,32 +1484,32 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\index.html:29
|
||||
msgid "Search recipe ..."
|
||||
msgstr ""
|
||||
msgstr "Αναζήτηση συνταγής ..."
|
||||
|
||||
#: .\cookbook\templates\index.html:44
|
||||
msgid "New Recipe"
|
||||
msgstr ""
|
||||
msgstr "Νέα συνταγή"
|
||||
|
||||
#: .\cookbook\templates\index.html:53
|
||||
msgid "Advanced Search"
|
||||
msgstr ""
|
||||
msgstr "Αναζήτηση για προχωρημένους"
|
||||
|
||||
#: .\cookbook\templates\index.html:57
|
||||
msgid "Reset Search"
|
||||
msgstr ""
|
||||
msgstr "Επαναφορά αναζήτησης"
|
||||
|
||||
#: .\cookbook\templates\index.html:85
|
||||
msgid "Last viewed"
|
||||
msgstr ""
|
||||
msgstr "Τελευταίες προβολές"
|
||||
|
||||
#: .\cookbook\templates\index.html:94
|
||||
msgid "Log in to view recipes"
|
||||
msgstr ""
|
||||
msgstr "Συνδεθείτε για να δείτε τις συνταγές"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:5
|
||||
#: .\cookbook\templates\markdown_info.html:13
|
||||
msgid "Markdown Info"
|
||||
msgstr ""
|
||||
msgstr "Πληροφορίες για το Markdown"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:14
|
||||
msgid ""
|
||||
@@ -1479,31 +1529,33 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:25
|
||||
msgid "Headers"
|
||||
msgstr ""
|
||||
msgstr "Επικεφαλίδες"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:54
|
||||
msgid "Formatting"
|
||||
msgstr ""
|
||||
msgstr "Μορφοποίηση"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:56
|
||||
#: .\cookbook\templates\markdown_info.html:72
|
||||
msgid "Line breaks are inserted by adding two spaces after the end of a line"
|
||||
msgstr ""
|
||||
"Οι αλλαγές γραμμής εισάγονται προσθέτοντας δύο κενά μετά το τέλος μιας "
|
||||
"γραμμής"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:57
|
||||
#: .\cookbook\templates\markdown_info.html:73
|
||||
msgid "or by leaving a blank line in between."
|
||||
msgstr ""
|
||||
msgstr "ή αφήνοντας μια κενή γραμμή μεταξύ τους."
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:59
|
||||
#: .\cookbook\templates\markdown_info.html:74
|
||||
msgid "This text is bold"
|
||||
msgstr ""
|
||||
msgstr "Το κείμενο είναι έντονο (bold)"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:60
|
||||
#: .\cookbook\templates\markdown_info.html:75
|
||||
msgid "This text is italic"
|
||||
msgstr ""
|
||||
msgstr "Αυτό το κείμενο είναι πλάγιο (italic)"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:61
|
||||
#: .\cookbook\templates\markdown_info.html:77
|
||||
@@ -1512,7 +1564,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:84
|
||||
msgid "Lists"
|
||||
msgstr ""
|
||||
msgstr "Λίστες"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:85
|
||||
msgid ""
|
||||
@@ -1550,7 +1602,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:125
|
||||
msgid "Images & Links"
|
||||
msgstr ""
|
||||
msgstr "Φωτογραφίες και σύνδεσμοι"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:126
|
||||
msgid ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -14,8 +14,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-05-26 16:19+0000\n"
|
||||
"Last-Translator: Luis Cacho <luiscachog@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-09-25 09:59+0000\n"
|
||||
"Last-Translator: Matias Laporte <laportematias+weblate@gmail.com>\n"
|
||||
"Language-Team: Spanish <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -543,19 +543,19 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:268
|
||||
msgid "knead"
|
||||
msgstr ""
|
||||
msgstr "amasar"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:269
|
||||
msgid "thicken"
|
||||
msgstr ""
|
||||
msgstr "espesar"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:270
|
||||
msgid "warm up"
|
||||
msgstr ""
|
||||
msgstr "precalentar"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:271
|
||||
msgid "ferment"
|
||||
msgstr ""
|
||||
msgstr "fermentar"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:272
|
||||
msgid "sous-vide"
|
||||
@@ -573,11 +573,11 @@ msgstr ""
|
||||
#: .\cookbook\integration\copymethat.py:44
|
||||
#: .\cookbook\integration\melarecipes.py:37
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
msgstr "Favorito"
|
||||
|
||||
#: .\cookbook\integration\copymethat.py:50
|
||||
msgid "I made this"
|
||||
msgstr ""
|
||||
msgstr "Lo he preparado"
|
||||
|
||||
#: .\cookbook\integration\integration.py:218
|
||||
msgid ""
|
||||
@@ -604,7 +604,7 @@ msgstr "Se importaron %s recetas."
|
||||
|
||||
#: .\cookbook\integration\openeats.py:26
|
||||
msgid "Recipe source:"
|
||||
msgstr "Recipe source:"
|
||||
msgstr "Fuente de la receta:"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
msgid "Notes"
|
||||
@@ -645,19 +645,21 @@ msgstr "Sección"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:14
|
||||
msgid "Rebuilds full text search index on Recipe"
|
||||
msgstr ""
|
||||
msgstr "Reconstruye el índice de búsqueda por texto completo de la receta"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:18
|
||||
msgid "Only Postgresql databases use full text search, no index to rebuild"
|
||||
msgstr ""
|
||||
"Solo las bases de datos Postgresql utilizan la búsqueda por texto completo, "
|
||||
"no hay índice para reconstruir"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:29
|
||||
msgid "Recipe index rebuild complete."
|
||||
msgstr ""
|
||||
msgstr "Se reconstruyó el índice de la receta."
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:31
|
||||
msgid "Recipe index rebuild failed."
|
||||
msgstr ""
|
||||
msgstr "No fue posible reconstruir el índice de la receta."
|
||||
|
||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:14
|
||||
msgid "Breakfast"
|
||||
@@ -699,23 +701,23 @@ msgstr "Libros"
|
||||
|
||||
#: .\cookbook\models.py:580
|
||||
msgid " is part of a recipe step and cannot be deleted"
|
||||
msgstr ""
|
||||
msgstr " es parte del paso de una receta y no puede ser eliminado"
|
||||
|
||||
#: .\cookbook\models.py:1181 .\cookbook\templates\search_info.html:28
|
||||
msgid "Simple"
|
||||
msgstr ""
|
||||
msgstr "Simple"
|
||||
|
||||
#: .\cookbook\models.py:1182 .\cookbook\templates\search_info.html:33
|
||||
msgid "Phrase"
|
||||
msgstr ""
|
||||
msgstr "Frase"
|
||||
|
||||
#: .\cookbook\models.py:1183 .\cookbook\templates\search_info.html:38
|
||||
msgid "Web"
|
||||
msgstr ""
|
||||
msgstr "Web"
|
||||
|
||||
#: .\cookbook\models.py:1184 .\cookbook\templates\search_info.html:47
|
||||
msgid "Raw"
|
||||
msgstr ""
|
||||
msgstr "Crudo"
|
||||
|
||||
#: .\cookbook\models.py:1231
|
||||
msgid "Food Alias"
|
||||
@@ -762,49 +764,53 @@ msgstr "Palabra clave"
|
||||
|
||||
#: .\cookbook\serializer.py:198
|
||||
msgid "File uploads are not enabled for this Space."
|
||||
msgstr ""
|
||||
msgstr "Las cargas de archivo no están habilitadas para esta Instancia."
|
||||
|
||||
#: .\cookbook\serializer.py:209
|
||||
msgid "You have reached your file upload limit."
|
||||
msgstr ""
|
||||
msgstr "Has alcanzado el límite de cargas de archivo."
|
||||
|
||||
#: .\cookbook\serializer.py:291
|
||||
msgid "Cannot modify Space owner permission."
|
||||
msgstr ""
|
||||
msgstr "No puedes modificar los permisos del propietario de la Instancia."
|
||||
|
||||
#: .\cookbook\serializer.py:1093
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
msgstr "Hola"
|
||||
|
||||
#: .\cookbook\serializer.py:1093
|
||||
msgid "You have been invited by "
|
||||
msgstr ""
|
||||
msgstr "Has sido invitado por: "
|
||||
|
||||
#: .\cookbook\serializer.py:1094
|
||||
msgid " to join their Tandoor Recipes space "
|
||||
msgstr ""
|
||||
msgstr " para unirte a su instancia de Tandoor Recipes "
|
||||
|
||||
#: .\cookbook\serializer.py:1095
|
||||
msgid "Click the following link to activate your account: "
|
||||
msgstr ""
|
||||
msgstr "Haz click en el siguiente enlace para activar tu cuenta: "
|
||||
|
||||
#: .\cookbook\serializer.py:1096
|
||||
msgid ""
|
||||
"If the link does not work use the following code to manually join the space: "
|
||||
msgstr ""
|
||||
"Si el enlace no funciona, utiliza el siguiente código para unirte "
|
||||
"manualmente a la instancia: "
|
||||
|
||||
#: .\cookbook\serializer.py:1097
|
||||
msgid "The invitation is valid until "
|
||||
msgstr ""
|
||||
msgstr "La invitación es válida hasta "
|
||||
|
||||
#: .\cookbook\serializer.py:1098
|
||||
msgid ""
|
||||
"Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub "
|
||||
msgstr ""
|
||||
"Tandoor Recipes es un administrador de recetas Open Source. Dale una ojeada "
|
||||
"en GitHub "
|
||||
|
||||
#: .\cookbook\serializer.py:1101
|
||||
msgid "Tandoor Recipes Invite"
|
||||
msgstr ""
|
||||
msgstr "Invitación para Tandoor Recipes"
|
||||
|
||||
#: .\cookbook\serializer.py:1242
|
||||
msgid "Existing shopping list to update"
|
||||
|
||||
Binary file not shown.
@@ -14,10 +14,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/fr/>\n"
|
||||
"PO-Revision-Date: 2023-10-12 20:19+0000\n"
|
||||
"Last-Translator: pharok <pharok@free.fr>\n"
|
||||
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/fr/>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -310,7 +310,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Champs à rechercher en ignorant les accents. La sélection de cette option "
|
||||
"peut améliorer ou dégrader la qualité de la recherche en fonction de la "
|
||||
"langue."
|
||||
"langue"
|
||||
|
||||
#: .\cookbook\forms.py:466
|
||||
msgid ""
|
||||
@@ -326,8 +326,8 @@ msgid ""
|
||||
"will return 'salad' and 'sandwich')"
|
||||
msgstr ""
|
||||
"Champs permettant de rechercher les correspondances de début de mot (par "
|
||||
"exemple, si vous recherchez « sa », vous obtiendrez « salade » et "
|
||||
"« sandwich»)."
|
||||
"exemple, si vous recherchez « sa », vous obtiendrez « salade » et « "
|
||||
"sandwich»)"
|
||||
|
||||
#: .\cookbook\forms.py:470
|
||||
msgid ""
|
||||
@@ -546,10 +546,8 @@ msgid "One of queryset or hash_key must be provided"
|
||||
msgstr "Il est nécessaire de fournir soit le queryset, soit la clé de hachage"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:266
|
||||
#, fuzzy
|
||||
#| msgid "Use fractions"
|
||||
msgid "reverse rotation"
|
||||
msgstr "Utiliser les fractions"
|
||||
msgstr "sens inverse"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:267
|
||||
msgid "careful rotation"
|
||||
@@ -557,27 +555,27 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:268
|
||||
msgid "knead"
|
||||
msgstr ""
|
||||
msgstr "pétrir"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:269
|
||||
msgid "thicken"
|
||||
msgstr ""
|
||||
msgstr "épaissir"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:270
|
||||
msgid "warm up"
|
||||
msgstr ""
|
||||
msgstr "réchauffer"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:271
|
||||
msgid "ferment"
|
||||
msgstr ""
|
||||
msgstr "fermenter"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:272
|
||||
msgid "sous-vide"
|
||||
msgstr ""
|
||||
msgstr "sous-vide"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:157
|
||||
msgid "You must supply a servings size"
|
||||
msgstr "Vous devez fournir une information de portion"
|
||||
msgstr "Vous devez fournir un nombre de portions"
|
||||
|
||||
#: .\cookbook\helper\template_helper.py:79
|
||||
#: .\cookbook\helper\template_helper.py:81
|
||||
@@ -590,7 +588,6 @@ msgid "Favorite"
|
||||
msgstr "Favori"
|
||||
|
||||
#: .\cookbook\integration\copymethat.py:50
|
||||
#, fuzzy
|
||||
msgid "I made this"
|
||||
msgstr "J'ai fait ça"
|
||||
|
||||
@@ -620,10 +617,8 @@ msgid "Imported %s recipes."
|
||||
msgstr "%s recettes importées."
|
||||
|
||||
#: .\cookbook\integration\openeats.py:26
|
||||
#, fuzzy
|
||||
#| msgid "Recipe Home"
|
||||
msgid "Recipe source:"
|
||||
msgstr "Page d’accueil"
|
||||
msgstr "Source de la recette :"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
msgid "Notes"
|
||||
@@ -648,7 +643,7 @@ msgstr "Portions"
|
||||
|
||||
#: .\cookbook\integration\saffron.py:25
|
||||
msgid "Waiting time"
|
||||
msgstr "temps d’attente"
|
||||
msgstr "Temps d’attente"
|
||||
|
||||
#: .\cookbook\integration\saffron.py:27
|
||||
msgid "Preparation Time"
|
||||
@@ -851,7 +846,6 @@ msgid "ID of unit to use for the shopping list"
|
||||
msgstr "ID de l’unité à utiliser pour la liste de courses"
|
||||
|
||||
#: .\cookbook\serializer.py:1259
|
||||
#, fuzzy
|
||||
msgid "When set to true will delete all food from active shopping lists."
|
||||
msgstr ""
|
||||
"Lorsqu'il est défini sur \"true\", tous les aliments des listes de courses "
|
||||
@@ -967,8 +961,9 @@ msgid ""
|
||||
" ."
|
||||
msgstr ""
|
||||
"Confirmez SVP que\n"
|
||||
" <a href=\"mailto:%(email)s\"></a> est une adresse mail de "
|
||||
"l’utilisateur %(user_display)s."
|
||||
" <a href=\"mailto:%(email)s\"></a> est une adresse mail de l’"
|
||||
"utilisateur %(user_display)s\n"
|
||||
" ."
|
||||
|
||||
#: .\cookbook\templates\account\email_confirm.html:22
|
||||
#: .\cookbook\templates\generic\delete_template.html:72
|
||||
@@ -1371,9 +1366,8 @@ msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
|
||||
msgstr "Êtes-vous sûr(e) de vouloir supprimer %(title)s : <b>%(object)s</b> "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:22
|
||||
#, fuzzy
|
||||
msgid "This cannot be undone!"
|
||||
msgstr "Cela ne peut pas être annulé !"
|
||||
msgstr "L'opération ne peut pas être annulée !"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:27
|
||||
msgid "Protected"
|
||||
@@ -1456,12 +1450,12 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Les champs <b>Mot de passe et Token</b> sont stockés <b>en texte "
|
||||
"brut</b>dans la base de données.\n"
|
||||
" Les champs <b>Mot de passe et Token</b> sont stockés <b>en clair</"
|
||||
"b>dans la base de données.\n"
|
||||
" C'est nécessaire car ils sont utilisés pour faire des requêtes API, "
|
||||
"mais cela accroît le risque que quelqu'un les vole.<br/>\n"
|
||||
" Pour limiter les risques, des tokens ou comptes avec un accès limité "
|
||||
"devraient être utilisés.\n"
|
||||
" Pour limiter les risques, il est possible d'utiliser des tokens ou "
|
||||
"des comptes avec un accès limité.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\index.html:29
|
||||
@@ -1771,15 +1765,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:29
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Simple searches ignore punctuation and common words such as "
|
||||
#| "'the', 'a', 'and'. And will treat seperate words as required.\n"
|
||||
#| " Searching for 'apple or flour' will return any recipe that "
|
||||
#| "includes both 'apple' and 'flour' anywhere in the fields that have been "
|
||||
#| "selected for a full text search.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Simple searches ignore punctuation and common words such as "
|
||||
@@ -1791,7 +1776,7 @@ msgid ""
|
||||
msgstr ""
|
||||
" \n"
|
||||
" Les recherches simples ignorent la ponctuation et les mots "
|
||||
"courants tels que \"le\", \"a\", \"et\", et traiteront les mots séparés "
|
||||
"courants tels que \"le\", \"et\", \"a\", et traiteront les mots séparés "
|
||||
"comme il se doit.\n"
|
||||
" Si vous recherchez \"pomme ou farine\", vous obtiendrez toutes "
|
||||
"les recettes qui contiennent à la fois \"pomme\" et \"farine\" dans les "
|
||||
@@ -2219,7 +2204,7 @@ msgstr "Gérer l’abonnement"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
|
||||
msgid "Space"
|
||||
msgstr "Groupe :"
|
||||
msgstr "Groupe"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:17
|
||||
msgid ""
|
||||
@@ -2659,7 +2644,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\views\api.py:1394
|
||||
msgid "Sync successful!"
|
||||
msgstr "Synchro réussie !"
|
||||
msgstr "Synchronisation réussie !"
|
||||
|
||||
#: .\cookbook\views\api.py:1399
|
||||
msgid "Error synchronizing with Storage"
|
||||
@@ -2732,6 +2717,8 @@ msgid ""
|
||||
"The PDF Exporter is not enabled on this instance as it is still in an "
|
||||
"experimental state."
|
||||
msgstr ""
|
||||
"L'export PDF n'est pas activé sur cette instance car il est toujours au "
|
||||
"statut expérimental."
|
||||
|
||||
#: .\cookbook\views\lists.py:24
|
||||
msgid "Import Log"
|
||||
|
||||
2529
cookbook/locale/he/LC_MESSAGES/django.po
Normal file
2529
cookbook/locale/he/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"PO-Revision-Date: 2023-10-20 14:05+0000\n"
|
||||
"Last-Translator: Ferenc <ugyes@freemail.hu>\n"
|
||||
"Language-Team: Hungarian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/hu/>\n"
|
||||
"Language: hu_HU\n"
|
||||
@@ -99,7 +99,7 @@ msgstr ""
|
||||
#: .\cookbook\forms.py:74
|
||||
msgid "Users with whom newly created meal plans should be shared by default."
|
||||
msgstr ""
|
||||
"Azok a felhasználók, akikkel az újonnan létrehozott étkezési terveket "
|
||||
"Azok a felhasználók, akikkel az újonnan létrehozott menüterveket "
|
||||
"alapértelmezés szerint meg kell osztani."
|
||||
|
||||
#: .\cookbook\forms.py:75
|
||||
@@ -135,8 +135,7 @@ msgstr "A navigációs sávot az oldal tetejére rögzíti."
|
||||
|
||||
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
|
||||
msgid "Automatically add meal plan ingredients to shopping list."
|
||||
msgstr ""
|
||||
"Automatikusan hozzáadja az étkezési terv hozzávalóit a bevásárlólistához."
|
||||
msgstr "Automatikusan hozzáadja a menüterv hozzávalóit a bevásárlólistához."
|
||||
|
||||
#: .\cookbook\forms.py:84
|
||||
msgid "Exclude ingredients that are on hand."
|
||||
@@ -360,10 +359,8 @@ msgid "Partial Match"
|
||||
msgstr "Részleges találat"
|
||||
|
||||
#: .\cookbook\forms.py:480
|
||||
#, fuzzy
|
||||
#| msgid "Starts Wtih"
|
||||
msgid "Starts With"
|
||||
msgstr "Kezdődik a következővel"
|
||||
msgstr "Ezzel kezdődik"
|
||||
|
||||
#: .\cookbook\forms.py:481
|
||||
msgid "Fuzzy Search"
|
||||
@@ -387,16 +384,16 @@ msgid ""
|
||||
"When adding a meal plan to the shopping list (manually or automatically), "
|
||||
"include all related recipes."
|
||||
msgstr ""
|
||||
"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
|
||||
"automatikusan), vegye fel az összes kapcsolódó receptet."
|
||||
"Amikor menütervet ad hozzá a bevásárlólistához (kézzel vagy automatikusan), "
|
||||
"vegye fel az összes kapcsolódó receptet."
|
||||
|
||||
#: .\cookbook\forms.py:514
|
||||
msgid ""
|
||||
"When adding a meal plan to the shopping list (manually or automatically), "
|
||||
"exclude ingredients that are on hand."
|
||||
msgstr ""
|
||||
"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
|
||||
"automatikusan), zárja ki a kéznél lévő összetevőket."
|
||||
"Amikor menütervet ad hozzá a bevásárlólistához (kézzel vagy automatikusan), "
|
||||
"zárja ki a kéznél lévő összetevőket."
|
||||
|
||||
#: .\cookbook\forms.py:515
|
||||
msgid "Default number of hours to delay a shopping list entry."
|
||||
@@ -436,7 +433,7 @@ msgstr "Automatikus szinkronizálás"
|
||||
|
||||
#: .\cookbook\forms.py:526
|
||||
msgid "Auto Add Meal Plan"
|
||||
msgstr "Automatikus étkezési terv hozzáadása"
|
||||
msgstr "Menüterv automatikus hozzáadása"
|
||||
|
||||
#: .\cookbook\forms.py:527
|
||||
msgid "Exclude On Hand"
|
||||
@@ -490,6 +487,7 @@ msgstr "A receptek számának megjelenítése a keresési szűrőkön"
|
||||
#: .\cookbook\forms.py:559
|
||||
msgid "Use the plural form for units and food inside this space."
|
||||
msgstr ""
|
||||
"Használja a többes számot az egységek és az ételek esetében ezen a helyen."
|
||||
|
||||
#: .\cookbook\helper\AllAuthCustomAdapter.py:39
|
||||
msgid ""
|
||||
@@ -549,29 +547,27 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:268
|
||||
msgid "knead"
|
||||
msgstr ""
|
||||
msgstr "dagasztás"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:269
|
||||
msgid "thicken"
|
||||
msgstr ""
|
||||
msgstr "sűrítés"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:270
|
||||
msgid "warm up"
|
||||
msgstr ""
|
||||
msgstr "melegítés"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:271
|
||||
msgid "ferment"
|
||||
msgstr ""
|
||||
msgstr "fermentálás"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:272
|
||||
msgid "sous-vide"
|
||||
msgstr ""
|
||||
msgstr "sous-vide"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:157
|
||||
#, fuzzy
|
||||
#| msgid "You must supply a created_by"
|
||||
msgid "You must supply a servings size"
|
||||
msgstr "Meg kell adnia egy created_by"
|
||||
msgstr "Meg kell adnia az adagok nagyságát"
|
||||
|
||||
#: .\cookbook\helper\template_helper.py:79
|
||||
#: .\cookbook\helper\template_helper.py:81
|
||||
@@ -581,11 +577,11 @@ msgstr "Nem sikerült elemezni a sablon kódját."
|
||||
#: .\cookbook\integration\copymethat.py:44
|
||||
#: .\cookbook\integration\melarecipes.py:37
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
msgstr "Kedvenc"
|
||||
|
||||
#: .\cookbook\integration\copymethat.py:50
|
||||
msgid "I made this"
|
||||
msgstr ""
|
||||
msgstr "Elkészítettem"
|
||||
|
||||
#: .\cookbook\integration\integration.py:218
|
||||
msgid ""
|
||||
@@ -613,10 +609,8 @@ msgid "Imported %s recipes."
|
||||
msgstr "Importálva %s recept."
|
||||
|
||||
#: .\cookbook\integration\openeats.py:26
|
||||
#, fuzzy
|
||||
#| msgid "Recipe Home"
|
||||
msgid "Recipe source:"
|
||||
msgstr "Recipe Home"
|
||||
msgstr "Recept forrása:"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
msgid "Notes"
|
||||
@@ -632,10 +626,8 @@ msgstr "Forrás"
|
||||
|
||||
#: .\cookbook\integration\recettetek.py:54
|
||||
#: .\cookbook\integration\recipekeeper.py:70
|
||||
#, fuzzy
|
||||
#| msgid "Import Log"
|
||||
msgid "Imported from"
|
||||
msgstr "Import napló"
|
||||
msgstr "Importálva a"
|
||||
|
||||
#: .\cookbook\integration\saffron.py:23
|
||||
msgid "Servings"
|
||||
@@ -662,12 +654,10 @@ msgid "Rebuilds full text search index on Recipe"
|
||||
msgstr "Újraépíti a teljes szöveges keresési indexet a Recept oldalon"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:18
|
||||
#, fuzzy
|
||||
#| msgid "Only Postgress databases use full text search, no index to rebuild"
|
||||
msgid "Only Postgresql databases use full text search, no index to rebuild"
|
||||
msgstr ""
|
||||
"Csak a Postgress adatbázisok használnak teljes szöveges keresést, nincs "
|
||||
"újjáépítendő index"
|
||||
"Csak a Postgresql adatbázisok használják a teljes szöveges keresést, nem "
|
||||
"kell indexet újjáépíteni"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:29
|
||||
msgid "Recipe index rebuild complete."
|
||||
@@ -711,7 +701,7 @@ msgstr "Keresés"
|
||||
#: .\cookbook\templates\meal_plan.html:7 .\cookbook\views\delete.py:178
|
||||
#: .\cookbook\views\edit.py:211 .\cookbook\views\new.py:179
|
||||
msgid "Meal-Plan"
|
||||
msgstr "Étkezési terv"
|
||||
msgstr "Menüterv"
|
||||
|
||||
#: .\cookbook\models.py:367 .\cookbook\templates\base.html:118
|
||||
msgid "Books"
|
||||
@@ -750,16 +740,12 @@ msgid "Keyword Alias"
|
||||
msgstr "Kulcsszó álneve"
|
||||
|
||||
#: .\cookbook\models.py:1232
|
||||
#, fuzzy
|
||||
#| msgid "Description"
|
||||
msgid "Description Replace"
|
||||
msgstr "Leírás"
|
||||
msgstr "Leírás csere"
|
||||
|
||||
#: .\cookbook\models.py:1232
|
||||
#, fuzzy
|
||||
#| msgid "Instructions"
|
||||
msgid "Instruction Replace"
|
||||
msgstr "Elkészítés"
|
||||
msgstr "Leírás cseréje"
|
||||
|
||||
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
|
||||
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
|
||||
@@ -767,10 +753,8 @@ msgid "Recipe"
|
||||
msgstr "Recept"
|
||||
|
||||
#: .\cookbook\models.py:1259
|
||||
#, fuzzy
|
||||
#| msgid "Foods"
|
||||
msgid "Food"
|
||||
msgstr "Ételek"
|
||||
msgstr "Étel"
|
||||
|
||||
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
|
||||
msgid "Keyword"
|
||||
@@ -1176,7 +1160,7 @@ msgstr "Ételek"
|
||||
|
||||
#: .\cookbook\templates\base.html:165 .\cookbook\views\lists.py:122
|
||||
msgid "Units"
|
||||
msgstr "Egységek"
|
||||
msgstr "Mértékegységek"
|
||||
|
||||
#: .\cookbook\templates\base.html:179 .\cookbook\templates\supermarket.html:7
|
||||
msgid "Supermarket"
|
||||
@@ -1206,10 +1190,8 @@ msgstr "Előzmények"
|
||||
#: .\cookbook\templates\base.html:255
|
||||
#: .\cookbook\templates\ingredient_editor.html:7
|
||||
#: .\cookbook\templates\ingredient_editor.html:13
|
||||
#, fuzzy
|
||||
#| msgid "Ingredients"
|
||||
msgid "Ingredient Editor"
|
||||
msgstr "Hozzávalók"
|
||||
msgstr "Hozzávaló szerkesztő"
|
||||
|
||||
#: .\cookbook\templates\base.html:267
|
||||
#: .\cookbook\templates\export_response.html:7
|
||||
@@ -1252,7 +1234,7 @@ msgstr "Nincs hely"
|
||||
#: .\cookbook\templates\base.html:323
|
||||
#: .\cookbook\templates\space_overview.html:6
|
||||
msgid "Overview"
|
||||
msgstr ""
|
||||
msgstr "Áttekintés"
|
||||
|
||||
#: .\cookbook\templates\base.html:327
|
||||
msgid "Markdown Guide"
|
||||
@@ -1276,11 +1258,11 @@ msgstr "Kijelentkezés"
|
||||
|
||||
#: .\cookbook\templates\base.html:360
|
||||
msgid "You are using the free version of Tandor"
|
||||
msgstr ""
|
||||
msgstr "Ön a Tandoor ingyenes verzióját használja"
|
||||
|
||||
#: .\cookbook\templates\base.html:361
|
||||
msgid "Upgrade Now"
|
||||
msgstr ""
|
||||
msgstr "Frissítés most"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:6
|
||||
msgid "Batch edit Category"
|
||||
@@ -1377,7 +1359,7 @@ msgstr "Biztos, hogy törölni akarod a %(title)s: <b>%(object)s</b> "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:22
|
||||
msgid "This cannot be undone!"
|
||||
msgstr ""
|
||||
msgstr "Ezt nem lehet visszafordítani!"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:27
|
||||
msgid "Protected"
|
||||
@@ -1541,8 +1523,6 @@ msgstr "A sortörés a sor vége után két szóköz hozzáadásával történik
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:57
|
||||
#: .\cookbook\templates\markdown_info.html:73
|
||||
#, fuzzy
|
||||
#| msgid "or by leaving a blank line inbetween."
|
||||
msgid "or by leaving a blank line in between."
|
||||
msgstr "vagy egy üres sort hagyva közöttük."
|
||||
|
||||
@@ -1566,10 +1546,6 @@ msgid "Lists"
|
||||
msgstr "Listák"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:85
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Lists can ordered or unorderd. It is <b>important to leave a blank line "
|
||||
#| "before the list!</b>"
|
||||
msgid ""
|
||||
"Lists can ordered or unordered. It is <b>important to leave a blank line "
|
||||
"before the list!</b>"
|
||||
@@ -1701,11 +1677,11 @@ msgstr ""
|
||||
#: .\cookbook\templates\openid\login.html:27
|
||||
#: .\cookbook\templates\socialaccount\authentication_error.html:27
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
msgstr "Vissza"
|
||||
|
||||
#: .\cookbook\templates\profile.html:7
|
||||
msgid "Profile"
|
||||
msgstr ""
|
||||
msgstr "Profil"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:41
|
||||
msgid "by"
|
||||
@@ -1718,7 +1694,7 @@ msgstr "Megjegyzés"
|
||||
|
||||
#: .\cookbook\templates\rest_framework\api.html:5
|
||||
msgid "Recipe Home"
|
||||
msgstr "Recipe Home"
|
||||
msgstr "Recept főoldal"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:5
|
||||
#: .\cookbook\templates\search_info.html:9
|
||||
@@ -2104,17 +2080,15 @@ msgstr "Szuperfelhasználói fiók létrehozása"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\authentication_error.html:7
|
||||
#: .\cookbook\templates\socialaccount\authentication_error.html:23
|
||||
#, fuzzy
|
||||
#| msgid "Social Login"
|
||||
msgid "Social Network Login Failure"
|
||||
msgstr "Közösségi bejelentkezés"
|
||||
msgstr "Közösségi hálózat bejelentkezési hiba"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\authentication_error.html:25
|
||||
#, fuzzy
|
||||
#| msgid "An error occurred attempting to move "
|
||||
msgid ""
|
||||
"An error occurred while attempting to login via your social network account."
|
||||
msgstr "Hiba történt az áthelyezés közben "
|
||||
msgstr ""
|
||||
"Hiba történt, miközben megpróbált bejelentkezni a közösségi hálózati fiókján "
|
||||
"keresztül."
|
||||
|
||||
#: .\cookbook\templates\socialaccount\connections.html:4
|
||||
#: .\cookbook\templates\socialaccount\connections.html:15
|
||||
@@ -2152,7 +2126,7 @@ msgstr "Regisztráció"
|
||||
#: .\cookbook\templates\socialaccount\login.html:9
|
||||
#, python-format
|
||||
msgid "Connect %(provider)s"
|
||||
msgstr ""
|
||||
msgstr "Csatlakozás %(provider)s"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\login.html:11
|
||||
#, python-format
|
||||
@@ -2162,7 +2136,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\socialaccount\login.html:13
|
||||
#, python-format
|
||||
msgid "Sign In Via %(provider)s"
|
||||
msgstr ""
|
||||
msgstr "Bejelentkezve %(provider)s keresztül"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\login.html:15
|
||||
#, python-format
|
||||
@@ -2171,7 +2145,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\socialaccount\login.html:20
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
msgstr "Folytatás"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\signup.html:10
|
||||
#, python-format
|
||||
@@ -2210,10 +2184,8 @@ msgid "Manage Subscription"
|
||||
msgstr "Feliratkozás kezelése"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
|
||||
#, fuzzy
|
||||
#| msgid "Space:"
|
||||
msgid "Space"
|
||||
msgstr "Tér:"
|
||||
msgstr "Tér"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:17
|
||||
msgid ""
|
||||
@@ -2230,13 +2202,11 @@ msgstr "Meghívást kaphatsz egy meglévő térbe, vagy létrehozhatod a sajáto
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:53
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
msgstr "Tulajdonos"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:57
|
||||
#, fuzzy
|
||||
#| msgid "Create Space"
|
||||
msgid "Leave Space"
|
||||
msgstr "Tér létrehozása"
|
||||
msgstr "Kilépés a Térből"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:78
|
||||
#: .\cookbook\templates\space_overview.html:88
|
||||
@@ -2485,87 +2455,111 @@ msgstr ""
|
||||
"teljes szöveges keresés is."
|
||||
|
||||
#: .\cookbook\views\api.py:733
|
||||
#, fuzzy
|
||||
#| msgid "ID of keyword a recipe should have. For multiple repeat parameter."
|
||||
msgid ""
|
||||
"ID of keyword a recipe should have. For multiple repeat parameter. "
|
||||
"Equivalent to keywords_or"
|
||||
msgstr ""
|
||||
"A recept kulcsszavának azonosítója. Többszörös ismétlődő paraméter esetén."
|
||||
"A recept kulcsszavának azonosítója. Többszörös ismétlődő paraméter esetén. "
|
||||
"Egyenértékű a keywords_or kulcsszavakkal"
|
||||
|
||||
#: .\cookbook\views\api.py:736
|
||||
msgid ""
|
||||
"Keyword IDs, repeat for multiple. Return recipes with any of the keywords"
|
||||
msgstr ""
|
||||
"Kulcsszó azonosítók. Többször is megadható. A megadott kulcsszavak "
|
||||
"mindegyikéhez tartozó receptek listázza"
|
||||
|
||||
#: .\cookbook\views\api.py:739
|
||||
msgid ""
|
||||
"Keyword IDs, repeat for multiple. Return recipes with all of the keywords."
|
||||
msgstr ""
|
||||
"Kulcsszó azonosítók. Többször is megadható. Az összes megadott kulcsszót "
|
||||
"tartalmazó receptek listázása."
|
||||
|
||||
#: .\cookbook\views\api.py:742
|
||||
msgid ""
|
||||
"Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords."
|
||||
msgstr ""
|
||||
"Kulcsszó azonosító. Többször is megadható. Kizárja a recepteket a megadott "
|
||||
"kulcsszavak egyikéből."
|
||||
|
||||
#: .\cookbook\views\api.py:745
|
||||
msgid ""
|
||||
"Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords."
|
||||
msgstr ""
|
||||
"Kulcsszó azonosítók. Többször is megadható. Kizárja az összes megadott "
|
||||
"kulcsszóval rendelkező receptet."
|
||||
|
||||
#: .\cookbook\views\api.py:747
|
||||
msgid "ID of food a recipe should have. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"Az ételek azonosítója egy receptnek tartalmaznia kell. Többszörös ismétlődő "
|
||||
"paraméter esetén."
|
||||
"Annak az összetevőnek az azonosítója, amelynek receptjeit fel kell sorolni. "
|
||||
"Többször is megadható."
|
||||
|
||||
#: .\cookbook\views\api.py:750
|
||||
msgid "Food IDs, repeat for multiple. Return recipes with any of the foods"
|
||||
msgstr ""
|
||||
"Összetevő azonosító. Többször is megadható. Legalább egy összetevő "
|
||||
"receptjeinek listája"
|
||||
|
||||
#: .\cookbook\views\api.py:752
|
||||
msgid "Food IDs, repeat for multiple. Return recipes with all of the foods."
|
||||
msgstr ""
|
||||
"Összetevő azonosító. Többször is megadható. Az összes megadott összetevőt "
|
||||
"tartalmazó receptek listája."
|
||||
|
||||
#: .\cookbook\views\api.py:754
|
||||
msgid "Food IDs, repeat for multiple. Exclude recipes with any of the foods."
|
||||
msgstr ""
|
||||
"Összetevő azonosító. Többször is megadható. Kizárja azokat a recepteket, "
|
||||
"amelyek a megadott összetevők bármelyikét tartalmazzák."
|
||||
|
||||
#: .\cookbook\views\api.py:756
|
||||
msgid "Food IDs, repeat for multiple. Exclude recipes with all of the foods."
|
||||
msgstr ""
|
||||
"Összetevő azonosító. Többször is megadható. Kizárja az összes megadott "
|
||||
"összetevőt tartalmazó recepteket."
|
||||
|
||||
#: .\cookbook\views\api.py:757
|
||||
msgid "ID of unit a recipe should have."
|
||||
msgstr "Az egység azonosítója, amellyel a receptnek rendelkeznie kell."
|
||||
msgstr "A recepthez tartozó mértékegység azonosítója."
|
||||
|
||||
#: .\cookbook\views\api.py:759
|
||||
msgid ""
|
||||
"Rating a recipe should have or greater. [0 - 5] Negative value filters "
|
||||
"rating less than."
|
||||
msgstr ""
|
||||
"Egy recept minimális értékelése (0-5). A negatív értékek a maximális "
|
||||
"értékelés szerint szűrnek."
|
||||
|
||||
#: .\cookbook\views\api.py:760
|
||||
msgid "ID of book a recipe should be in. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"A könyv azonosítója, amelyben a receptnek szerepelnie kell. Többszörös "
|
||||
"ismétlés esetén paraméter."
|
||||
"A könyv azonosítója, amelyben a recept található. Többször is megadható."
|
||||
|
||||
#: .\cookbook\views\api.py:762
|
||||
msgid "Book IDs, repeat for multiple. Return recipes with any of the books"
|
||||
msgstr ""
|
||||
"A könyv azonosítója. Többször is megadható. A megadott könyvek összes "
|
||||
"receptjének listája"
|
||||
|
||||
#: .\cookbook\views\api.py:764
|
||||
msgid "Book IDs, repeat for multiple. Return recipes with all of the books."
|
||||
msgstr ""
|
||||
"A könyv azonosítója. Többször is megadható. Az összes könyvben szereplő "
|
||||
"recept listája."
|
||||
|
||||
#: .\cookbook\views\api.py:766
|
||||
msgid "Book IDs, repeat for multiple. Exclude recipes with any of the books."
|
||||
msgstr ""
|
||||
"A könyv azonosítói. Többször is megadható. Kizárja a megadott könyvek "
|
||||
"receptjeit."
|
||||
|
||||
#: .\cookbook\views\api.py:768
|
||||
msgid "Book IDs, repeat for multiple. Exclude recipes with all of the books."
|
||||
msgstr ""
|
||||
"A könyv azonosítói. Többször is megadható. Kizárja az összes megadott "
|
||||
"könyvben szereplő receptet."
|
||||
|
||||
#: .\cookbook\views\api.py:770
|
||||
msgid "If only internal recipes should be returned. [true/<b>false</b>]"
|
||||
@@ -2587,36 +2581,50 @@ msgid ""
|
||||
"Filter recipes cooked X times or more. Negative values returns cooked less "
|
||||
"than X times"
|
||||
msgstr ""
|
||||
"X-szer vagy többször főzött receptek szűrése. A negatív értékek X "
|
||||
"alkalomnál kevesebbet főzött recepteket jelenítik meg"
|
||||
|
||||
#: .\cookbook\views\api.py:778
|
||||
msgid ""
|
||||
"Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on "
|
||||
"or before date."
|
||||
msgstr ""
|
||||
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
|
||||
"vagy később főztek meg utoljára. A - jelölve az adott dátumon vagy azt "
|
||||
"megelőzően elkészítettek kerülnek be a receptek listájába."
|
||||
|
||||
#: .\cookbook\views\api.py:780
|
||||
msgid ""
|
||||
"Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or "
|
||||
"before date."
|
||||
msgstr ""
|
||||
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
|
||||
"vagy később hoztak létre. A - jelölve az adott dátumon vagy azt megelőzően "
|
||||
"hozták létre."
|
||||
|
||||
#: .\cookbook\views\api.py:782
|
||||
msgid ""
|
||||
"Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or "
|
||||
"before date."
|
||||
msgstr ""
|
||||
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
|
||||
"vagy később frissültek. A - jelölve az adott dátumon vagy azt megelőzően "
|
||||
"frissültek."
|
||||
|
||||
#: .\cookbook\views\api.py:784
|
||||
msgid ""
|
||||
"Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on "
|
||||
"or before date."
|
||||
msgstr ""
|
||||
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
|
||||
"vagy később néztek meg utoljára. A - jelölve az adott dátumon vagy azt "
|
||||
"megelőzően néztek meg utoljára."
|
||||
|
||||
#: .\cookbook\views\api.py:786
|
||||
#, fuzzy
|
||||
#| msgid "If only internal recipes should be returned. [true/<b>false</b>]"
|
||||
msgid "Filter recipes that can be made with OnHand food. [true/<b>false</b>]"
|
||||
msgstr "Ha csak a belső recepteket kell visszaadni. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
"Felsorolja azokat a recepteket, amelyeket a rendelkezésre álló összetevőkből "
|
||||
"el lehet készíteni. [true/<b>false</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:946
|
||||
msgid ""
|
||||
@@ -2647,7 +2655,7 @@ msgstr "Semmi feladat."
|
||||
|
||||
#: .\cookbook\views\api.py:1198
|
||||
msgid "Invalid Url"
|
||||
msgstr ""
|
||||
msgstr "Érvénytelen URL"
|
||||
|
||||
#: .\cookbook\views\api.py:1205
|
||||
msgid "Connection Refused."
|
||||
@@ -2655,13 +2663,11 @@ msgstr "Kapcsolat megtagadva."
|
||||
|
||||
#: .\cookbook\views\api.py:1210
|
||||
msgid "Bad URL Schema."
|
||||
msgstr ""
|
||||
msgstr "Rossz URL séma."
|
||||
|
||||
#: .\cookbook\views\api.py:1233
|
||||
#, fuzzy
|
||||
#| msgid "No useable data could be found."
|
||||
msgid "No usable data could be found."
|
||||
msgstr "Nem találtam használható adatokat."
|
||||
msgstr "Nem sikerült használható adatokat találni."
|
||||
|
||||
#: .\cookbook\views\api.py:1326 .\cookbook\views\import_export.py:117
|
||||
msgid "Importing is not implemented for this provider"
|
||||
@@ -2774,10 +2780,8 @@ msgid "Shopping Categories"
|
||||
msgstr "Bevásárlási kategóriák"
|
||||
|
||||
#: .\cookbook\views\lists.py:187
|
||||
#, fuzzy
|
||||
#| msgid "Filter"
|
||||
msgid "Custom Filters"
|
||||
msgstr "Szűrő"
|
||||
msgstr "Egyedi szűrők"
|
||||
|
||||
#: .\cookbook\views\lists.py:224
|
||||
msgid "Steps"
|
||||
|
||||
Binary file not shown.
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"PO-Revision-Date: 2023-04-17 20:55+0000\n"
|
||||
"Last-Translator: Espen Sellevåg <buskmenn.drammer03@icloud.com>\n"
|
||||
"PO-Revision-Date: 2023-08-19 21:36+0000\n"
|
||||
"Last-Translator: NeoID <neoid@animenord.com>\n"
|
||||
"Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/"
|
||||
"tandoor/recipes-backend/nb_NO/>\n"
|
||||
"Language: nb_NO\n"
|
||||
@@ -31,6 +31,8 @@ msgid ""
|
||||
"Color of the top navigation bar. Not all colors work with all themes, just "
|
||||
"try them out!"
|
||||
msgstr ""
|
||||
"Farge på toppnavigasjonslinjen. Ikke alle farger fungerer med alle temaer, "
|
||||
"så bare prøv dem ut!"
|
||||
|
||||
#: .\cookbook\forms.py:46
|
||||
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
|
||||
@@ -79,13 +81,15 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:56
|
||||
msgid "Makes the navbar stick to the top of the page."
|
||||
msgstr ""
|
||||
msgstr "Fest navigasjonslinjen til toppen av siden."
|
||||
|
||||
#: .\cookbook\forms.py:72
|
||||
msgid ""
|
||||
"Both fields are optional. If none are given the username will be displayed "
|
||||
"instead"
|
||||
msgstr ""
|
||||
"Begge feltene er valgfrie. Hvis ingen blir oppgitt, vil brukernavnet vises i "
|
||||
"stedet"
|
||||
|
||||
#: .\cookbook\forms.py:93 .\cookbook\forms.py:315
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:45
|
||||
@@ -97,15 +101,15 @@ msgstr "Navn"
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:81
|
||||
#: .\cookbook\templates\stats.html:24 .\cookbook\templates\url_import.html:202
|
||||
msgid "Keywords"
|
||||
msgstr ""
|
||||
msgstr "Nøkkelord"
|
||||
|
||||
#: .\cookbook\forms.py:95
|
||||
msgid "Preparation time in minutes"
|
||||
msgstr ""
|
||||
msgstr "Forberedelsestid i minutter"
|
||||
|
||||
#: .\cookbook\forms.py:96
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr ""
|
||||
msgstr "Ventetid (til matlaging/baking) i minutter"
|
||||
|
||||
#: .\cookbook\forms.py:97 .\cookbook\forms.py:317
|
||||
msgid "Path"
|
||||
@@ -124,6 +128,8 @@ msgid ""
|
||||
"To prevent duplicates recipes with the same name as existing ones are "
|
||||
"ignored. Check this box to import everything."
|
||||
msgstr ""
|
||||
"For å unngå duplikater, blir oppskrifter med samme navn som eksisterende "
|
||||
"ignorert. Merk av denne boksen for å importere alt."
|
||||
|
||||
#: .\cookbook\forms.py:149
|
||||
msgid "New Unit"
|
||||
@@ -131,7 +137,7 @@ msgstr "Ny enhet"
|
||||
|
||||
#: .\cookbook\forms.py:150
|
||||
msgid "New unit that other gets replaced by."
|
||||
msgstr ""
|
||||
msgstr "Ny enhet som erstatter den gamle."
|
||||
|
||||
#: .\cookbook\forms.py:155
|
||||
msgid "Old Unit"
|
||||
@@ -143,19 +149,19 @@ msgstr "Enhet som skal erstattes."
|
||||
|
||||
#: .\cookbook\forms.py:172
|
||||
msgid "New Food"
|
||||
msgstr ""
|
||||
msgstr "Ny matvare"
|
||||
|
||||
#: .\cookbook\forms.py:173
|
||||
msgid "New food that other gets replaced by."
|
||||
msgstr ""
|
||||
msgstr "Ny matvare som erstatter den gamle."
|
||||
|
||||
#: .\cookbook\forms.py:178
|
||||
msgid "Old Food"
|
||||
msgstr ""
|
||||
msgstr "Gammel matvare"
|
||||
|
||||
#: .\cookbook\forms.py:179
|
||||
msgid "Food that should be replaced."
|
||||
msgstr ""
|
||||
msgstr "Matvare som bør erstattes."
|
||||
|
||||
#: .\cookbook\forms.py:197
|
||||
msgid "Add your comment: "
|
||||
@@ -163,17 +169,19 @@ msgstr "Legg til din kommentar: "
|
||||
|
||||
#: .\cookbook\forms.py:238
|
||||
msgid "Leave empty for dropbox and enter app password for nextcloud."
|
||||
msgstr ""
|
||||
msgstr "La det stå tomt for Dropbox og skriv inn app-passordet for Nextcloud."
|
||||
|
||||
#: .\cookbook\forms.py:245
|
||||
msgid "Leave empty for nextcloud and enter api token for dropbox."
|
||||
msgstr ""
|
||||
msgstr "La det stå tomt for Nextcloud og skriv inn API-tokenet for Dropbox."
|
||||
|
||||
#: .\cookbook\forms.py:253
|
||||
msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
msgstr ""
|
||||
"La det stå tomt for Dropbox, og skriv bare inn grunn-URLen for Nextcloud "
|
||||
"(<code>/remote.php/webdav/</code> blir lagt til automatisk)"
|
||||
|
||||
#: .\cookbook\forms.py:291
|
||||
msgid "Search String"
|
||||
@@ -185,11 +193,12 @@ msgstr "Fil-ID"
|
||||
|
||||
#: .\cookbook\forms.py:354
|
||||
msgid "You must provide at least a recipe or a title."
|
||||
msgstr ""
|
||||
msgstr "Du må oppgi minst en oppskrift eller en tittel."
|
||||
|
||||
#: .\cookbook\forms.py:367
|
||||
msgid "You can list default users to share recipes with in the settings."
|
||||
msgstr ""
|
||||
"Du kan liste opp standardbrukere for å dele oppskrifter innen innstillingene."
|
||||
|
||||
#: .\cookbook\forms.py:368
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
|
||||
@@ -197,10 +206,14 @@ msgid ""
|
||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
||||
"\">docs here</a>"
|
||||
msgstr ""
|
||||
"Du kan bruke Markdown for å formatere dette feltet. Se <a href=\"/docs/"
|
||||
"markdown/\">dokumentasjonen her</a>"
|
||||
|
||||
#: .\cookbook\forms.py:393
|
||||
msgid "A username is not required, if left blank the new user can choose one."
|
||||
msgstr ""
|
||||
"Et brukernavn er ikke påkrevd. Hvis det blir stående tomt, kan den nye "
|
||||
"brukeren velge ett selv."
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:123
|
||||
#: .\cookbook\helper\permission_helper.py:129
|
||||
@@ -222,26 +235,30 @@ msgstr "Du er ikke innlogget og kan derfor ikke vise siden!"
|
||||
#: .\cookbook\helper\permission_helper.py:167
|
||||
#: .\cookbook\helper\permission_helper.py:182
|
||||
msgid "You cannot interact with this object as it is not owned by you!"
|
||||
msgstr ""
|
||||
msgstr "Du kan ikke samhandle med dette objektet, da det ikke tilhører deg!"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:40 .\cookbook\views\api.py:549
|
||||
msgid "The requested site provided malformed data and cannot be read."
|
||||
msgstr ""
|
||||
"Nettstedet du har forespurt, har levert feilformatert data som ikke kan "
|
||||
"leses."
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:54
|
||||
msgid ""
|
||||
"The requested site does not provide any recognized data format to import the "
|
||||
"recipe from."
|
||||
msgstr ""
|
||||
"Det forespurte nettstedet gir ingen gjenkjennelig dataformat som kan "
|
||||
"importeres oppskriften fra."
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:160
|
||||
msgid "Imported from"
|
||||
msgstr ""
|
||||
msgstr "Importert fra"
|
||||
|
||||
#: .\cookbook\helper\template_helper.py:60
|
||||
#: .\cookbook\helper\template_helper.py:62
|
||||
msgid "Could not parse template code."
|
||||
msgstr ""
|
||||
msgstr "Kunne ikke analysere mal-koden."
|
||||
|
||||
#: .\cookbook\integration\integration.py:102
|
||||
#: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20
|
||||
@@ -250,50 +267,52 @@ msgstr ""
|
||||
#: .\cookbook\templates\url_import.html:233 .\cookbook\views\delete.py:60
|
||||
#: .\cookbook\views\edit.py:190
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
msgstr "Importér"
|
||||
|
||||
#: .\cookbook\integration\integration.py:131
|
||||
msgid ""
|
||||
"Importer expected a .zip file. Did you choose the correct importer type for "
|
||||
"your data ?"
|
||||
msgstr ""
|
||||
"Importøren forventet en .zip-fil. Har du valgt riktig type importør for "
|
||||
"dataene dine?"
|
||||
|
||||
#: .\cookbook\integration\integration.py:134
|
||||
msgid "The following recipes were ignored because they already existed:"
|
||||
msgstr ""
|
||||
msgstr "Følgende oppskrifter ble ignorert fordi de allerede eksisterte:"
|
||||
|
||||
#: .\cookbook\integration\integration.py:137
|
||||
#, python-format
|
||||
msgid "Imported %s recipes."
|
||||
msgstr ""
|
||||
msgstr "Importerte %s oppskrifter."
|
||||
|
||||
#: .\cookbook\integration\paprika.py:44
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
msgstr "Notater"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:47
|
||||
msgid "Nutritional Information"
|
||||
msgstr ""
|
||||
msgstr "Næringsinformasjon"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:50
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
msgstr "Kilde"
|
||||
|
||||
#: .\cookbook\integration\safron.py:23
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:75
|
||||
#: .\cookbook\templates\include\log_cooking.html:16
|
||||
#: .\cookbook\templates\url_import.html:84
|
||||
msgid "Servings"
|
||||
msgstr ""
|
||||
msgstr "Porsjoner"
|
||||
|
||||
#: .\cookbook\integration\safron.py:25
|
||||
msgid "Waiting time"
|
||||
msgstr ""
|
||||
msgstr "Ventetid"
|
||||
|
||||
#: .\cookbook\integration\safron.py:27
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:69
|
||||
msgid "Preparation Time"
|
||||
msgstr ""
|
||||
msgstr "Forberedelsestid"
|
||||
|
||||
#: .\cookbook\integration\safron.py:29 .\cookbook\templates\base.html:71
|
||||
#: .\cookbook\templates\forms\ingredients.html:7
|
||||
@@ -329,7 +348,7 @@ msgstr "Søk"
|
||||
#: .\cookbook\templates\meal_plan.html:5 .\cookbook\views\delete.py:152
|
||||
#: .\cookbook\views\edit.py:224 .\cookbook\views\new.py:188
|
||||
msgid "Meal-Plan"
|
||||
msgstr ""
|
||||
msgstr "Måltidsplan"
|
||||
|
||||
#: .\cookbook\models.py:112 .\cookbook\templates\base.html:82
|
||||
msgid "Books"
|
||||
@@ -337,11 +356,11 @@ msgstr "Bøker"
|
||||
|
||||
#: .\cookbook\models.py:119
|
||||
msgid "Small"
|
||||
msgstr ""
|
||||
msgstr "Liten"
|
||||
|
||||
#: .\cookbook\models.py:119
|
||||
msgid "Large"
|
||||
msgstr ""
|
||||
msgstr "Stor"
|
||||
|
||||
#: .\cookbook\models.py:327
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:198
|
||||
@@ -1109,22 +1128,24 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:125
|
||||
msgid "Images & Links"
|
||||
msgstr ""
|
||||
msgstr "Bilder og lenker"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:126
|
||||
msgid ""
|
||||
"Links can be formatted with Markdown. This application also allows to paste "
|
||||
"links directly into markdown fields without any formatting."
|
||||
msgstr ""
|
||||
"Lenker kan formateres med Markdown. Denne applikasjonen lar deg også lime "
|
||||
"inn lenker direkte i Markdown-felt uten noen formatering."
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:132
|
||||
#: .\cookbook\templates\markdown_info.html:145
|
||||
msgid "This will become an image"
|
||||
msgstr ""
|
||||
msgstr "Dette vil bli til et bilde"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:152
|
||||
msgid "Tables"
|
||||
msgstr ""
|
||||
msgstr "Tabeller"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:153
|
||||
msgid ""
|
||||
@@ -1132,124 +1153,130 @@ msgid ""
|
||||
"editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
|
||||
"\"noreferrer noopener\" target=\"_blank\">this one.</a>"
|
||||
msgstr ""
|
||||
"Markdown-tabeller er vanskelige å lage for hånd. Det anbefales å bruke en "
|
||||
"tabellredigerer som <a href=\"https://www.tablesgenerator.com/"
|
||||
"markdown_tables\" rel=\"noreferrer noopener\" target=\"_blank\">denne.</a>"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:155
|
||||
#: .\cookbook\templates\markdown_info.html:157
|
||||
#: .\cookbook\templates\markdown_info.html:171
|
||||
#: .\cookbook\templates\markdown_info.html:177
|
||||
msgid "Table"
|
||||
msgstr ""
|
||||
msgstr "Tabell"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:155
|
||||
#: .\cookbook\templates\markdown_info.html:172
|
||||
msgid "Header"
|
||||
msgstr ""
|
||||
msgstr "Overskrift"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:157
|
||||
#: .\cookbook\templates\markdown_info.html:178
|
||||
msgid "Cell"
|
||||
msgstr ""
|
||||
msgstr "Celle"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:101
|
||||
msgid "New Entry"
|
||||
msgstr ""
|
||||
msgstr "Ny oppføring"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:113
|
||||
#: .\cookbook\templates\shopping_list.html:52
|
||||
msgid "Search Recipe"
|
||||
msgstr ""
|
||||
msgstr "Søk oppskrift"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:139
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "Tittel"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:141
|
||||
msgid "Note (optional)"
|
||||
msgstr ""
|
||||
msgstr "Merknad (valgfritt)"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:143
|
||||
msgid ""
|
||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
||||
"\" target=\"_blank\" rel=\"noopener noreferrer\">docs here</a>"
|
||||
msgstr ""
|
||||
"Du kan bruke Markdown for å formatere dette feltet. Se <a href=\"/docs/"
|
||||
"markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">dokumentasjonen "
|
||||
"her</a>"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:147
|
||||
#: .\cookbook\templates\meal_plan.html:251
|
||||
msgid "Serving Count"
|
||||
msgstr ""
|
||||
msgstr "Antall porsjoner"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:153
|
||||
msgid "Create only note"
|
||||
msgstr ""
|
||||
msgstr "Opprett kun en merknad"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:168
|
||||
#: .\cookbook\templates\shopping_list.html:7
|
||||
#: .\cookbook\templates\shopping_list.html:29
|
||||
#: .\cookbook\templates\shopping_list.html:705
|
||||
msgid "Shopping List"
|
||||
msgstr ""
|
||||
msgstr "Handleliste"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:172
|
||||
msgid "Shopping list currently empty"
|
||||
msgstr ""
|
||||
msgstr "Handlelisten er for øyeblikket tom"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:175
|
||||
msgid "Open Shopping List"
|
||||
msgstr ""
|
||||
msgstr "Åpne handlelisten"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:189
|
||||
msgid "Plan"
|
||||
msgstr ""
|
||||
msgstr "Plan"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:196
|
||||
msgid "Number of Days"
|
||||
msgstr ""
|
||||
msgstr "Antall dager"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:206
|
||||
msgid "Weekday offset"
|
||||
msgstr ""
|
||||
msgstr "Ukedagsforskyvning"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:209
|
||||
msgid ""
|
||||
"Number of days starting from the first day of the week to offset the default "
|
||||
"view."
|
||||
msgstr ""
|
||||
msgstr "Antall dager fra den første dagen i uken for å endre standardvisningen."
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:217
|
||||
#: .\cookbook\templates\meal_plan.html:294
|
||||
msgid "Edit plan types"
|
||||
msgstr ""
|
||||
msgstr "Rediger plantyper"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:219
|
||||
msgid "Show help"
|
||||
msgstr ""
|
||||
msgstr "Vis hjelp"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:220
|
||||
msgid "Week iCal export"
|
||||
msgstr ""
|
||||
msgstr "Uke iCal-eksport"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:264
|
||||
#: .\cookbook\templates\meal_plan_entry.html:18
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
msgstr "Opprettet av"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:270
|
||||
#: .\cookbook\templates\meal_plan_entry.html:20
|
||||
#: .\cookbook\templates\shopping_list.html:250
|
||||
msgid "Shared with"
|
||||
msgstr ""
|
||||
msgstr "Delt med"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:280
|
||||
msgid "Add to Shopping"
|
||||
msgstr ""
|
||||
msgstr "Legg til i handlelisten"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:323
|
||||
msgid "New meal type"
|
||||
msgstr ""
|
||||
msgstr "Ny måltidstype"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:338
|
||||
msgid "Meal Plan Help"
|
||||
msgstr ""
|
||||
msgstr "Hjelp for måltidsplanen"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:344
|
||||
msgid ""
|
||||
@@ -1289,7 +1316,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\meal_plan_entry.html:6
|
||||
msgid "Meal Plan View"
|
||||
msgstr ""
|
||||
msgstr "Visning av måltidsplanen"
|
||||
|
||||
#: .\cookbook\templates\meal_plan_entry.html:50
|
||||
msgid "Never cooked before."
|
||||
@@ -1297,7 +1324,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\meal_plan_entry.html:76
|
||||
msgid "Other meals on this day"
|
||||
msgstr ""
|
||||
msgstr "Andre måltider denne dagen"
|
||||
|
||||
#: .\cookbook\templates\no_groups_info.html:5
|
||||
#: .\cookbook\templates\no_groups_info.html:12
|
||||
|
||||
Binary file not shown.
@@ -13,10 +13,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-02-27 13:55+0000\n"
|
||||
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/nl/>\n"
|
||||
"PO-Revision-Date: 2023-08-15 19:19+0000\n"
|
||||
"Last-Translator: Jochum van der Heide <jochum@famvanderheide.com>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/nl/>\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -522,34 +522,32 @@ msgid "One of queryset or hash_key must be provided"
|
||||
msgstr "Er moet een queryset of hash_key opgegeven worden"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:266
|
||||
#, fuzzy
|
||||
#| msgid "Use fractions"
|
||||
msgid "reverse rotation"
|
||||
msgstr "Gebruik fracties"
|
||||
msgstr "omgekeerde rotatie"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:267
|
||||
msgid "careful rotation"
|
||||
msgstr ""
|
||||
msgstr "voorzichtige rotatie"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:268
|
||||
msgid "knead"
|
||||
msgstr ""
|
||||
msgstr "kneden"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:269
|
||||
msgid "thicken"
|
||||
msgstr ""
|
||||
msgstr "verdikken"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:270
|
||||
msgid "warm up"
|
||||
msgstr ""
|
||||
msgstr "opwarmen"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:271
|
||||
msgid "ferment"
|
||||
msgstr ""
|
||||
msgstr "gisten"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:272
|
||||
msgid "sous-vide"
|
||||
msgstr ""
|
||||
msgstr "sous-vide"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:157
|
||||
msgid "You must supply a servings size"
|
||||
@@ -594,10 +592,8 @@ msgid "Imported %s recipes."
|
||||
msgstr "%s recepten geïmporteerd."
|
||||
|
||||
#: .\cookbook\integration\openeats.py:26
|
||||
#, fuzzy
|
||||
#| msgid "Recipe Home"
|
||||
msgid "Recipe source:"
|
||||
msgstr "Recept thuis"
|
||||
msgstr "Bron van het recept:"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
msgid "Notes"
|
||||
|
||||
@@ -12,8 +12,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
|
||||
"PO-Revision-Date: 2023-01-08 17:55+0000\n"
|
||||
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
|
||||
"PO-Revision-Date: 2023-10-07 18:02+0000\n"
|
||||
"Last-Translator: Guilherme Roda <glealroda@gmail.com>\n"
|
||||
"Language-Team: Portuguese <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/pt/>\n"
|
||||
"Language: pt\n"
|
||||
@@ -206,8 +206,8 @@ msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
msgstr ""
|
||||
"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud (<code>/"
|
||||
"remote.php/webdav/</code>é adicionado automaticamente). "
|
||||
"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud "
|
||||
"(<code>/remote.php/webdav/</code>é adicionado automaticamente)"
|
||||
|
||||
#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
|
||||
msgid "Storage"
|
||||
@@ -277,16 +277,12 @@ msgstr ""
|
||||
"ignorados)."
|
||||
|
||||
#: .\cookbook\forms.py:461
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Select type method of search. Click <a href=\"/docs/search/\">here</a> "
|
||||
#| "for full desciption of choices."
|
||||
msgid ""
|
||||
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
|
||||
"full description of choices."
|
||||
msgstr ""
|
||||
"Selecionar o método de pesquisa. Uma descrição completa das opções pode ser "
|
||||
"encontrada <a href=\"/docs/search/\">aqui</a>."
|
||||
"Selecionar o método de pesquisa. Uma descrição completa das opções pode "
|
||||
"ser encontrada <a href=\"/docs/search/\">aqui</a>."
|
||||
|
||||
#: .\cookbook\forms.py:462
|
||||
msgid ""
|
||||
@@ -329,10 +325,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:476
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search Method"
|
||||
msgstr "Procurar"
|
||||
msgstr "Método de Pesquisa"
|
||||
|
||||
#: .\cookbook\forms.py:477
|
||||
msgid "Fuzzy Lookups"
|
||||
@@ -351,16 +345,12 @@ msgid "Starts With"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:481
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Fuzzy Search"
|
||||
msgstr "Procurar"
|
||||
msgstr "Pesquisa Fuzzy"
|
||||
|
||||
#: .\cookbook\forms.py:482
|
||||
#, fuzzy
|
||||
#| msgid "Text"
|
||||
msgid "Full Text"
|
||||
msgstr "Texto"
|
||||
msgstr "Texto Completo"
|
||||
|
||||
#: .\cookbook\forms.py:507
|
||||
msgid ""
|
||||
@@ -405,10 +395,8 @@ msgid "Prefix to add when copying list to the clipboard."
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:524
|
||||
#, fuzzy
|
||||
#| msgid "Shopping"
|
||||
msgid "Share Shopping List"
|
||||
msgstr "Compras"
|
||||
msgstr "Compartilhar Lista de Compras"
|
||||
|
||||
#: .\cookbook\forms.py:525
|
||||
msgid "Autosync"
|
||||
@@ -459,10 +447,8 @@ msgid "Reset all food to inherit the fields configured."
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:557
|
||||
#, fuzzy
|
||||
#| msgid "Food that should be replaced."
|
||||
msgid "Fields on food that should be inherited by default."
|
||||
msgstr "Prato a ser alterado."
|
||||
msgstr "Campos do alimento que devem ser herdados por padrão."
|
||||
|
||||
#: .\cookbook\forms.py:558
|
||||
msgid "Show recipe counts on search filters"
|
||||
@@ -516,10 +502,8 @@ msgid "One of queryset or hash_key must be provided"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:266
|
||||
#, fuzzy
|
||||
#| msgid "Use fractions"
|
||||
msgid "reverse rotation"
|
||||
msgstr "Usar frações"
|
||||
msgstr "rotação reversa"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:267
|
||||
msgid "careful rotation"
|
||||
@@ -585,16 +569,12 @@ msgid "Imported %s recipes."
|
||||
msgstr "%s receitas importadas."
|
||||
|
||||
#: .\cookbook\integration\openeats.py:26
|
||||
#, fuzzy
|
||||
#| msgid "Recipes"
|
||||
msgid "Recipe source:"
|
||||
msgstr "Receitas"
|
||||
msgstr "Fonte da Receita:"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
#, fuzzy
|
||||
#| msgid "Note"
|
||||
msgid "Notes"
|
||||
msgstr "Nota"
|
||||
msgstr "Notas"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:52
|
||||
msgid "Nutritional Information"
|
||||
@@ -606,10 +586,8 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\integration\recettetek.py:54
|
||||
#: .\cookbook\integration\recipekeeper.py:70
|
||||
#, fuzzy
|
||||
#| msgid "Import"
|
||||
msgid "Imported from"
|
||||
msgstr "Importar"
|
||||
msgstr "Importado de"
|
||||
|
||||
#: .\cookbook\integration\saffron.py:23
|
||||
msgid "Servings"
|
||||
@@ -706,32 +684,24 @@ msgid "Raw"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\models.py:1231
|
||||
#, fuzzy
|
||||
#| msgid "New Food"
|
||||
msgid "Food Alias"
|
||||
msgstr "Novo Prato"
|
||||
msgstr "Apelido do Alimento"
|
||||
|
||||
#: .\cookbook\models.py:1231
|
||||
#, fuzzy
|
||||
#| msgid "Units"
|
||||
msgid "Unit Alias"
|
||||
msgstr "Unidades"
|
||||
msgstr "Apelido da Unidade"
|
||||
|
||||
#: .\cookbook\models.py:1231
|
||||
#, fuzzy
|
||||
#| msgid "Keywords"
|
||||
msgid "Keyword Alias"
|
||||
msgstr "Palavras-chave"
|
||||
msgstr "Apelido de Palavra-chave"
|
||||
|
||||
#: .\cookbook\models.py:1232
|
||||
msgid "Description Replace"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\models.py:1232
|
||||
#, fuzzy
|
||||
#| msgid "Instructions"
|
||||
msgid "Instruction Replace"
|
||||
msgstr "Instruções"
|
||||
msgstr "Substituir Instruções"
|
||||
|
||||
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
|
||||
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
|
||||
@@ -739,10 +709,8 @@ msgid "Recipe"
|
||||
msgstr "Receita"
|
||||
|
||||
#: .\cookbook\models.py:1259
|
||||
#, fuzzy
|
||||
#| msgid "New Food"
|
||||
msgid "Food"
|
||||
msgstr "Novo Prato"
|
||||
msgstr "Alimento"
|
||||
|
||||
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
|
||||
msgid "Keyword"
|
||||
@@ -880,10 +848,8 @@ msgid "Primary"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\account\email.html:47
|
||||
#, fuzzy
|
||||
#| msgid "Make Header"
|
||||
msgid "Make Primary"
|
||||
msgstr "Adicionar Cabeçalho"
|
||||
msgstr "Tornar Primeiro"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:49
|
||||
msgid "Re-send Verification"
|
||||
@@ -1004,10 +970,8 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\account\password_change.html:12
|
||||
#: .\cookbook\templates\account\password_set.html:12
|
||||
#, fuzzy
|
||||
#| msgid "Settings"
|
||||
msgid "Password"
|
||||
msgstr "Definições"
|
||||
msgstr "Senha"
|
||||
|
||||
#: .\cookbook\templates\account\password_change.html:22
|
||||
msgid "Forgot Password?"
|
||||
@@ -1050,10 +1014,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:33
|
||||
#, fuzzy
|
||||
#| msgid "Settings"
|
||||
msgid "change password"
|
||||
msgstr "Definições"
|
||||
msgstr "alterar senha"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:36
|
||||
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
|
||||
@@ -1125,10 +1087,8 @@ msgid "Shopping"
|
||||
msgstr "Compras"
|
||||
|
||||
#: .\cookbook\templates\base.html:153 .\cookbook\views\lists.py:105
|
||||
#, fuzzy
|
||||
#| msgid "New Food"
|
||||
msgid "Foods"
|
||||
msgstr "Novo Prato"
|
||||
msgstr "Alimentos"
|
||||
|
||||
#: .\cookbook\templates\base.html:165 .\cookbook\views\lists.py:122
|
||||
msgid "Units"
|
||||
@@ -1139,20 +1099,16 @@ msgid "Supermarket"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\base.html:191
|
||||
#, fuzzy
|
||||
#| msgid "Batch edit Category"
|
||||
msgid "Supermarket Category"
|
||||
msgstr "Editar Categorias em massa"
|
||||
msgstr "Categoria de Supermercado"
|
||||
|
||||
#: .\cookbook\templates\base.html:203 .\cookbook\views\lists.py:171
|
||||
msgid "Automations"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\base.html:217 .\cookbook\views\lists.py:207
|
||||
#, fuzzy
|
||||
#| msgid "File ID"
|
||||
msgid "Files"
|
||||
msgstr "ID the ficheiro"
|
||||
msgstr "Arquivos"
|
||||
|
||||
#: .\cookbook\templates\base.html:229
|
||||
msgid "Batch Edit"
|
||||
@@ -1166,10 +1122,8 @@ msgstr "Histórico"
|
||||
#: .\cookbook\templates\base.html:255
|
||||
#: .\cookbook\templates\ingredient_editor.html:7
|
||||
#: .\cookbook\templates\ingredient_editor.html:13
|
||||
#, fuzzy
|
||||
#| msgid "Ingredients"
|
||||
msgid "Ingredient Editor"
|
||||
msgstr "Ingredientes"
|
||||
msgstr "Editor de Ingrediente"
|
||||
|
||||
#: .\cookbook\templates\base.html:267
|
||||
#: .\cookbook\templates\export_response.html:7
|
||||
@@ -1191,10 +1145,8 @@ msgid "External Recipes"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\base.html:301 .\cookbook\templates\space_manage.html:15
|
||||
#, fuzzy
|
||||
#| msgid "Settings"
|
||||
msgid "Space Settings"
|
||||
msgstr "Definições"
|
||||
msgstr "Configurar Espaço"
|
||||
|
||||
#: .\cookbook\templates\base.html:306 .\cookbook\templates\system.html:13
|
||||
msgid "System"
|
||||
@@ -1206,10 +1158,8 @@ msgstr "Administração"
|
||||
|
||||
#: .\cookbook\templates\base.html:312
|
||||
#: .\cookbook\templates\space_overview.html:25
|
||||
#, fuzzy
|
||||
#| msgid "Create"
|
||||
msgid "Your Spaces"
|
||||
msgstr "Criar"
|
||||
msgstr "Seus Espaços"
|
||||
|
||||
#: .\cookbook\templates\base.html:323
|
||||
#: .\cookbook\templates\space_overview.html:6
|
||||
@@ -1288,19 +1238,15 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:28
|
||||
msgid "Sync Now!"
|
||||
msgstr "Sincronizar"
|
||||
msgstr "Sincronizar Agora!"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:29
|
||||
#, fuzzy
|
||||
#| msgid "Recipes"
|
||||
msgid "Show Recipes"
|
||||
msgstr "Receitas"
|
||||
msgstr "Mostrar Receitas"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:30
|
||||
#, fuzzy
|
||||
#| msgid "View Log"
|
||||
msgid "Show Log"
|
||||
msgstr "Ver Registro"
|
||||
msgstr "Mostrar Log"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:4
|
||||
#: .\cookbook\templates\batch\waiting.html:10
|
||||
@@ -1335,7 +1281,7 @@ msgstr "Editar Receita"
|
||||
#: .\cookbook\templates\generic\delete_template.html:21
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
|
||||
msgstr "Tem a certeza que quer apagar %(title)s: <b>%(object)s</b>"
|
||||
msgstr "Tem certeza que deseja apagar %(title)s: <b>%(object)s</b> "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:22
|
||||
msgid "This cannot be undone!"
|
||||
@@ -1369,7 +1315,7 @@ msgstr "Apagar ficheiro original"
|
||||
#: .\cookbook\templates\generic\list_template.html:6
|
||||
#: .\cookbook\templates\generic\list_template.html:22
|
||||
msgid "List"
|
||||
msgstr "Listar "
|
||||
msgstr "Listar"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:36
|
||||
msgid "Filter"
|
||||
@@ -1422,13 +1368,13 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Os </b>campos da senha e Token</b> são guardados dentro da base de "
|
||||
"dados como <b>texto simples.</b>\n"
|
||||
"Isto é necessário porque eles são usados para fazer pedidos á API, mas "
|
||||
"também aumenta o risco de\n"
|
||||
"de alguém os roubar. <br/>\n"
|
||||
"Para limitar os possíveis danos, tokens e contas com acesso limitado podem "
|
||||
"ser usadas.\n"
|
||||
" Os campos de <b>senha e Token</b> são armazenados na base de dados "
|
||||
"como <b>texto simples</b>.\n"
|
||||
" Isto é necessário porque eles são usados para fazer pedidos à API, "
|
||||
"mas também aumenta o risco\n"
|
||||
" de alguém os roubar.<br/>\n"
|
||||
" Para limitar os possíveis danos, tokens e contas com acesso limitado "
|
||||
"podem ser usadas.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\index.html:29
|
||||
@@ -1441,7 +1387,7 @@ msgstr "Nova Receita"
|
||||
|
||||
#: .\cookbook\templates\index.html:53
|
||||
msgid "Advanced Search"
|
||||
msgstr "Procura avançada "
|
||||
msgstr "Pesquisa avançada"
|
||||
|
||||
#: .\cookbook\templates\index.html:57
|
||||
msgid "Reset Search"
|
||||
@@ -1493,8 +1439,6 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:57
|
||||
#: .\cookbook\templates\markdown_info.html:73
|
||||
#, fuzzy
|
||||
#| msgid "or by leaving a blank line inbetween."
|
||||
msgid "or by leaving a blank line in between."
|
||||
msgstr "ou deixando uma linha em branco no meio."
|
||||
|
||||
@@ -1518,10 +1462,6 @@ msgid "Lists"
|
||||
msgstr "Listas"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:85
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Lists can ordered or unorderd. It is <b>important to leave a blank line "
|
||||
#| "before the list!</b>"
|
||||
msgid ""
|
||||
"Lists can ordered or unordered. It is <b>important to leave a blank line "
|
||||
"before the list!</b>"
|
||||
@@ -1598,7 +1538,7 @@ msgstr "Cabeçalho"
|
||||
#: .\cookbook\templates\markdown_info.html:157
|
||||
#: .\cookbook\templates\markdown_info.html:178
|
||||
msgid "Cell"
|
||||
msgstr "Célula "
|
||||
msgstr "Célula"
|
||||
|
||||
#: .\cookbook\templates\no_groups_info.html:5
|
||||
#: .\cookbook\templates\no_groups_info.html:12
|
||||
@@ -1666,10 +1606,8 @@ msgstr ""
|
||||
#: .\cookbook\templates\search_info.html:5
|
||||
#: .\cookbook\templates\search_info.html:9
|
||||
#: .\cookbook\templates\settings.html:24
|
||||
#, fuzzy
|
||||
#| msgid "Search String"
|
||||
msgid "Search Settings"
|
||||
msgstr "Procurar"
|
||||
msgstr "Configurações de Pesquisa"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:10
|
||||
msgid ""
|
||||
@@ -1684,10 +1622,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\search_info.html:19
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search Methods"
|
||||
msgstr "Procurar"
|
||||
msgstr "Métodos de Pesquisa"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:23
|
||||
msgid ""
|
||||
@@ -1769,10 +1705,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\search_info.html:69
|
||||
#, fuzzy
|
||||
#| msgid "Search Recipe"
|
||||
msgid "Search Fields"
|
||||
msgstr "Procure Receita"
|
||||
msgstr "Campos de Pesquisa"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:73
|
||||
msgid ""
|
||||
@@ -1810,10 +1744,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\search_info.html:95
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search Index"
|
||||
msgstr "Procurar"
|
||||
msgstr "Índice de Pesquisa"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:99
|
||||
msgid ""
|
||||
@@ -2012,10 +1944,8 @@ msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:57
|
||||
#, fuzzy
|
||||
#| msgid "Create"
|
||||
msgid "Leave Space"
|
||||
msgstr "Criar"
|
||||
msgstr "Sair do Espaço"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:78
|
||||
#: .\cookbook\templates\space_overview.html:88
|
||||
@@ -2034,10 +1964,8 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:96
|
||||
#: .\cookbook\templates\space_overview.html:105
|
||||
#, fuzzy
|
||||
#| msgid "Create"
|
||||
msgid "Create Space"
|
||||
msgstr "Criar"
|
||||
msgstr "Criar Espaço"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:99
|
||||
msgid "Create your own recipe space."
|
||||
@@ -2487,10 +2415,8 @@ msgid "Shopping Categories"
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\views\lists.py:187
|
||||
#, fuzzy
|
||||
#| msgid "Filter"
|
||||
msgid "Custom Filters"
|
||||
msgstr "Filtrar"
|
||||
msgstr "Filtros Customizados"
|
||||
|
||||
#: .\cookbook\views\lists.py:224
|
||||
msgid "Steps"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"PO-Revision-Date: 2023-08-13 08:19+0000\n"
|
||||
"Last-Translator: Miha Perpar <miha.perpar2@gmail.com>\n"
|
||||
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/sl/>\n"
|
||||
"Language: sl\n"
|
||||
@@ -964,7 +964,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\base.html:275
|
||||
msgid "GitHub"
|
||||
msgstr ""
|
||||
msgstr "GitHub"
|
||||
|
||||
#: .\cookbook\templates\base.html:277
|
||||
msgid "Translate Tandoor"
|
||||
@@ -1961,7 +1961,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space.html:106
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "uporabnik"
|
||||
|
||||
#: .\cookbook\templates\space.html:107
|
||||
msgid "guest"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.core.management.base import BaseCommand
|
||||
from django_scopes import scopes_disabled
|
||||
from django.utils import translation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import Recipe, Step
|
||||
@@ -14,7 +14,7 @@ class Command(BaseCommand):
|
||||
help = _('Rebuilds full text search index on Recipe')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql':
|
||||
self.stdout.write(self.style.WARNING(_('Only Postgresql databases use full text search, no index to rebuild')))
|
||||
|
||||
try:
|
||||
|
||||
@@ -34,35 +34,14 @@ class RecipeSearchManager(models.Manager):
|
||||
+ SearchVector(StringAgg('steps__ingredients__food__name__unaccent', delimiter=' '), weight='B', config=language)
|
||||
+ SearchVector(StringAgg('keywords__name__unaccent', delimiter=' '), weight='B', config=language))
|
||||
search_rank = SearchRank(search_vectors, search_query)
|
||||
# USING TRIGRAM BREAKS WEB SEARCH
|
||||
# ADDING MULTIPLE TRIGRAMS CREATES DUPLICATE RESULTS
|
||||
# DISTINCT NOT COMPAITBLE WITH ANNOTATE
|
||||
# trigram_name = (TrigramSimilarity('name', search_text))
|
||||
# trigram_description = (TrigramSimilarity('description', search_text))
|
||||
# trigram_food = (TrigramSimilarity('steps__ingredients__food__name', search_text))
|
||||
# trigram_keyword = (TrigramSimilarity('keywords__name', search_text))
|
||||
# adding additional trigrams created duplicates
|
||||
# + TrigramSimilarity('description', search_text)
|
||||
# + TrigramSimilarity('steps__ingredients__food__name', search_text)
|
||||
# + TrigramSimilarity('keywords__name', search_text)
|
||||
|
||||
return (
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
search=search_vectors,
|
||||
rank=search_rank,
|
||||
# trigram=trigram_name+trigram_description+trigram_food+trigram_keyword
|
||||
# trigram_name=trigram_name,
|
||||
# trigram_description=trigram_description,
|
||||
# trigram_food=trigram_food,
|
||||
# trigram_keyword=trigram_keyword
|
||||
)
|
||||
.filter(
|
||||
Q(search=search_query)
|
||||
# | Q(trigram_name__gt=0.1)
|
||||
# | Q(name__icontains=search_text)
|
||||
# | Q(trigram_name__gt=0.2)
|
||||
# | Q(trigram_description__gt=0.2)
|
||||
# | Q(trigram_food__gt=0.2)
|
||||
# | Q(trigram_keyword__gt=0.2)
|
||||
)
|
||||
.order_by('-rank'))
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.utils import translation
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import (Index, PermissionModelMixin, Recipe, Step, SearchFields)
|
||||
from cookbook.models import Index, PermissionModelMixin, Recipe, SearchFields, Step
|
||||
|
||||
|
||||
def allSearchFields():
|
||||
@@ -21,7 +21,7 @@ def nameSearchField():
|
||||
|
||||
|
||||
def set_default_search_vector(apps, schema_editor):
|
||||
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql':
|
||||
return
|
||||
language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.9 on 2023-06-26 13:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0193_space_internal_note'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='food',
|
||||
name='properties_food_amount',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=100, max_digits=16),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.1.9 on 2023-06-30 20:34
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0194_alter_food_properties_food_amount'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invitelink',
|
||||
name='internal_note',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userspace',
|
||||
name='internal_note',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userspace',
|
||||
name='invite_link',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.invitelink'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='theme',
|
||||
field=models.CharField(choices=[('TANDOOR', 'Tandoor'), ('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero'), ('TANDOOR_DARK', 'Tandoor Dark (INCOMPLETE)')], default='TANDOOR', max_length=128),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0196_food_url.py
Normal file
18
cookbook/migrations/0196_food_url.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.10 on 2023-07-22 06:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0195_invitelink_internal_note_userspace_internal_note_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='url',
|
||||
field=models.CharField(blank=True, default='', max_length=1024, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.10 on 2023-08-24 08:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0196_food_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='step',
|
||||
name='show_ingredients_table',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='show_step_ingredients',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0198_propertytype_order.py
Normal file
18
cookbook/migrations/0198_propertytype_order.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.10 on 2023-08-24 09:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0197_step_show_ingredients_table_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='propertytype',
|
||||
name='order',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-01 17:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0198_propertytype_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='automation',
|
||||
name='type',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('FOOD_ALIAS', 'Food Alias'),
|
||||
('UNIT_ALIAS', 'Unit Alias'),
|
||||
('KEYWORD_ALIAS', 'Keyword Alias'),
|
||||
('DESCRIPTION_REPLACE', 'Description Replace'),
|
||||
('INSTRUCTION_REPLACE', 'Instruction Replace'),
|
||||
('NEVER_UNIT', 'Never Unit'),
|
||||
('TRANSPOSE_WORDS', 'Transpose Words'),
|
||||
('FOOD_REPLACE', 'Food Replace'),
|
||||
('UNIT_REPLACE', 'Unit Replace'),
|
||||
('NAME_REPLACE', 'Name Replace')],
|
||||
max_length=128),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 4.1.10 on 2023-08-29 11:59
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
def migrate_icons(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
MealType = apps.get_model('cookbook', 'MealType')
|
||||
Keyword = apps.get_model('cookbook', 'Keyword')
|
||||
PropertyType = apps.get_model('cookbook', 'PropertyType')
|
||||
RecipeBook = apps.get_model('cookbook', 'RecipeBook')
|
||||
|
||||
MealType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
|
||||
Keyword.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
|
||||
PropertyType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
|
||||
RecipeBook.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0199_alter_propertytype_options_alter_automation_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
migrations.RunPython(
|
||||
migrate_icons
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='propertytype',
|
||||
options={'ordering': ('order',)},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='keyword',
|
||||
name='icon',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='mealtype',
|
||||
name='icon',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='propertytype',
|
||||
name='icon',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='recipebook',
|
||||
name='icon',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-08 12:20
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
def apply_migration(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
MealPlan = apps.get_model('cookbook', 'MealPlan')
|
||||
MealPlan.objects.update(to_date=F('from_date'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0200_alter_propertytype_options_remove_keyword_icon_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='mealplan',
|
||||
old_name='date',
|
||||
new_name='from_date',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealplan',
|
||||
name='to_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(apply_migration),
|
||||
migrations.AlterField(
|
||||
model_name='mealplan',
|
||||
name='to_date',
|
||||
field=models.DateField(),
|
||||
),
|
||||
]
|
||||
17
cookbook/migrations/0202_remove_space_show_facet_count.py
Normal file
17
cookbook/migrations/0202_remove_space_show_facet_count.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-12 13:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0201_rename_date_mealplan_from_date_mealplan_to_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='space',
|
||||
name='show_facet_count',
|
||||
),
|
||||
]
|
||||
17
cookbook/migrations/0203_alter_unique_contstraints.py
Normal file
17
cookbook/migrations/0203_alter_unique_contstraints.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-14 12:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0202_remove_space_show_facet_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='mealtype',
|
||||
constraint=models.UniqueConstraint(fields=('space', 'name', 'created_by'), name='mt_unique_name_per_space'),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0204_propertytype_fdc_id.py
Normal file
18
cookbook/migrations/0204_propertytype_fdc_id.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-27 21:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0203_alter_unique_contstraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='propertytype',
|
||||
name='fdc_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
@@ -5,7 +5,6 @@ import uuid
|
||||
from datetime import date, timedelta
|
||||
|
||||
import oauth2_provider.models
|
||||
from PIL import Image
|
||||
from annoying.fields import AutoOneToOneField
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User
|
||||
@@ -14,13 +13,14 @@ from django.contrib.postgres.search import SearchVectorField
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import IntegrityError, models
|
||||
from django.db.models import Index, ProtectedError, Q, Avg, Max
|
||||
from django.db.models import Avg, Index, Max, ProtectedError, Q
|
||||
from django.db.models.fields.related import ManyToManyField
|
||||
from django.db.models.functions import Substr
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django_prometheus.models import ExportModelOperationsMixin
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from PIL import Image
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
|
||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT,
|
||||
@@ -116,10 +116,7 @@ class TreeModel(MP_Node):
|
||||
_full_name_separator = ' > '
|
||||
|
||||
def __str__(self):
|
||||
if self.icon:
|
||||
return f"{self.icon} {self.name}"
|
||||
else:
|
||||
return f"{self.name}"
|
||||
return f"{self.name}"
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
@@ -188,7 +185,6 @@ class TreeModel(MP_Node):
|
||||
:param filter: Filter (include) the descendants nodes with the provided Q filter
|
||||
"""
|
||||
descendants = Q()
|
||||
# TODO filter the queryset nodes to exclude descendants of objects in the queryset
|
||||
nodes = queryset.values('path', 'depth')
|
||||
for node in nodes:
|
||||
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
|
||||
@@ -268,7 +264,6 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
no_sharing_limit = models.BooleanField(default=False)
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
show_facet_count = models.BooleanField(default=False)
|
||||
|
||||
internal_note = models.TextField(blank=True, null=True)
|
||||
|
||||
@@ -331,6 +326,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
FLATLY = 'FLATLY'
|
||||
SUPERHERO = 'SUPERHERO'
|
||||
TANDOOR = 'TANDOOR'
|
||||
TANDOOR_DARK = 'TANDOOR_DARK'
|
||||
|
||||
THEMES = (
|
||||
(TANDOOR, 'Tandoor'),
|
||||
@@ -338,6 +334,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
(DARKLY, 'Darkly'),
|
||||
(FLATLY, 'Flatly'),
|
||||
(SUPERHERO, 'Superhero'),
|
||||
(TANDOOR_DARK, 'Tandoor Dark (INCOMPLETE)'),
|
||||
)
|
||||
|
||||
# Nav colors
|
||||
@@ -392,6 +389,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
shopping_add_onhand = models.BooleanField(default=False)
|
||||
filter_to_supermarket = models.BooleanField(default=False)
|
||||
left_handed = models.BooleanField(default=False)
|
||||
show_step_ingredients = models.BooleanField(default=True)
|
||||
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
||||
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||
csv_delim = models.CharField(max_length=2, default=",")
|
||||
@@ -413,6 +411,9 @@ class UserSpace(models.Model, PermissionModelMixin):
|
||||
# that having more than one active space should just break certain parts of the application and not leak any data
|
||||
active = models.BooleanField(default=False)
|
||||
|
||||
invite_link = models.ForeignKey("InviteLink", on_delete=models.PROTECT, null=True, blank=True)
|
||||
internal_note = models.TextField(blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -527,7 +528,6 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
|
||||
if SORT_TREE_BY_NAME:
|
||||
node_order_by = ['name']
|
||||
name = models.CharField(max_length=64)
|
||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
description = models.TextField(default="", blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True) # TODO deprecate
|
||||
updated_at = models.DateTimeField(auto_now=True) # TODO deprecate
|
||||
@@ -574,6 +574,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
|
||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
url = models.CharField(max_length=1024, blank=True, null=True, default='')
|
||||
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
|
||||
ignore_shopping = models.BooleanField(default=False) # inherited field
|
||||
onhand_users = models.ManyToManyField(User, blank=True)
|
||||
@@ -585,7 +586,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit')
|
||||
|
||||
properties = models.ManyToManyField("Property", blank=True, through='FoodProperty')
|
||||
properties_food_amount = models.IntegerField(default=100, blank=True)
|
||||
properties_food_amount = models.DecimalField(default=100, max_digits=16, decimal_places=2, blank=True)
|
||||
properties_food_unit = models.ForeignKey(Unit, on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
||||
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
|
||||
@@ -717,25 +718,6 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
food = ""
|
||||
unit = ""
|
||||
if self.always_use_plural_food and self.food.plural_name not in (None, "") and not self.no_amount:
|
||||
food = self.food.plural_name
|
||||
else:
|
||||
if self.amount > 1 and self.food.plural_name not in (None, "") and not self.no_amount:
|
||||
food = self.food.plural_name
|
||||
else:
|
||||
food = str(self.food)
|
||||
if self.always_use_plural_unit and self.unit.plural_name not in (None, "") and not self.no_amount:
|
||||
unit = self.unit.plural_name
|
||||
else:
|
||||
if self.amount > 1 and self.unit is not None and self.unit.plural_name not in (None, "") and not self.no_amount:
|
||||
unit = self.unit.plural_name
|
||||
else:
|
||||
unit = str(self.unit)
|
||||
return str(self.amount) + ' ' + str(unit) + ' ' + str(food)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order', 'pk']
|
||||
indexes = (
|
||||
@@ -751,6 +733,7 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
|
||||
order = models.IntegerField(default=0)
|
||||
file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
|
||||
show_as_header = models.BooleanField(default=True)
|
||||
show_ingredients_table = models.BooleanField(default=True)
|
||||
search_vector = SearchVectorField(null=True)
|
||||
step_recipe = models.ForeignKey('Recipe', default=None, blank=True, null=True, on_delete=models.PROTECT)
|
||||
|
||||
@@ -778,11 +761,13 @@ class PropertyType(models.Model, PermissionModelMixin):
|
||||
|
||||
name = models.CharField(max_length=128)
|
||||
unit = models.CharField(max_length=64, blank=True, null=True)
|
||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
order = models.IntegerField(default=0)
|
||||
description = models.CharField(max_length=512, blank=True, null=True)
|
||||
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')), (PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
|
||||
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')),
|
||||
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
|
||||
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
|
||||
|
||||
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
|
||||
# TODO show if empty property?
|
||||
# TODO formatting property?
|
||||
|
||||
@@ -797,6 +782,7 @@ class PropertyType(models.Model, PermissionModelMixin):
|
||||
models.UniqueConstraint(fields=['space', 'name'], name='property_type_unique_name_per_space'),
|
||||
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='property_type_unique_open_data_slug_per_space')
|
||||
]
|
||||
ordering = ('order',)
|
||||
|
||||
|
||||
class Property(models.Model, PermissionModelMixin):
|
||||
@@ -945,7 +931,6 @@ class RecipeImport(models.Model, PermissionModelMixin):
|
||||
class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
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)
|
||||
filter = models.ForeignKey('cookbook.CustomFilter', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
@@ -988,7 +973,6 @@ class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, Pe
|
||||
class MealType(models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
order = models.IntegerField(default=0)
|
||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
color = models.CharField(max_length=7, blank=True, null=True)
|
||||
default = models.BooleanField(default=False, blank=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
@@ -999,6 +983,11 @@ class MealType(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['space', 'name', 'created_by'], name='mt_unique_name_per_space'),
|
||||
]
|
||||
|
||||
|
||||
class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
|
||||
@@ -1008,7 +997,8 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
|
||||
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
|
||||
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
|
||||
note = models.TextField(blank=True)
|
||||
date = models.DateField()
|
||||
from_date = models.DateField()
|
||||
to_date = models.DateField()
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
@@ -1022,7 +1012,7 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
|
||||
return self.meal_type.name
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
|
||||
return f'{self.get_label()} - {self.from_date} - {self.meal_type.name}'
|
||||
|
||||
|
||||
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
|
||||
@@ -1142,6 +1132,8 @@ class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, Permis
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
internal_note = models.TextField(blank=True, null=True)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
@@ -1308,7 +1300,7 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
|
||||
|
||||
def is_image(self):
|
||||
try:
|
||||
img = Image.open(self.file.file.file)
|
||||
Image.open(self.file.file.file)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -1326,10 +1318,25 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
||||
KEYWORD_ALIAS = 'KEYWORD_ALIAS'
|
||||
DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE'
|
||||
INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE'
|
||||
NEVER_UNIT = 'NEVER_UNIT'
|
||||
TRANSPOSE_WORDS = 'TRANSPOSE_WORDS'
|
||||
FOOD_REPLACE = 'FOOD_REPLACE'
|
||||
UNIT_REPLACE = 'UNIT_REPLACE'
|
||||
NAME_REPLACE = 'NAME_REPLACE'
|
||||
|
||||
type = models.CharField(max_length=128,
|
||||
choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),
|
||||
(DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),))
|
||||
choices=(
|
||||
(FOOD_ALIAS, _('Food Alias')),
|
||||
(UNIT_ALIAS, _('Unit Alias')),
|
||||
(KEYWORD_ALIAS, _('Keyword Alias')),
|
||||
(DESCRIPTION_REPLACE, _('Description Replace')),
|
||||
(INSTRUCTION_REPLACE, _('Instruction Replace')),
|
||||
(NEVER_UNIT, _('Never Unit')),
|
||||
(TRANSPOSE_WORDS, _('Transpose Words')),
|
||||
(FOOD_REPLACE, _('Food Replace')),
|
||||
(UNIT_REPLACE, _('Unit Replace')),
|
||||
(NAME_REPLACE, _('Name Replace')),
|
||||
))
|
||||
name = models.CharField(max_length=128, default='')
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
|
||||
@@ -67,17 +67,3 @@ class FilterSchema(AutoSchema):
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
return parameters
|
||||
|
||||
|
||||
# class QueryOnlySchema(AutoSchema):
|
||||
# def get_path_parameters(self, path, method):
|
||||
# if not is_list_view(path, method, self.view):
|
||||
# return super(QueryOnlySchema, self).get_path_parameters(path, method)
|
||||
|
||||
# parameters = super().get_path_parameters(path, method)
|
||||
# parameters.append({
|
||||
# "name": 'query', "in": "query", "required": False,
|
||||
# "description": 'Query string matched (fuzzy) against object name.',
|
||||
# 'schema': {'type': 'string', },
|
||||
# })
|
||||
# return parameters
|
||||
|
||||
@@ -6,34 +6,34 @@ from gettext import gettext as _
|
||||
from html import escape
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.contrib.auth.models import Group, User, AnonymousUser
|
||||
from django.contrib.auth.models import AnonymousUser, Group, User
|
||||
from django.core.cache import caches
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Avg, Q, QuerySet, Sum
|
||||
from django.db.models import Q, QuerySet, Sum
|
||||
from django.http import BadHeaderError
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_scopes import scopes_disabled
|
||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||
from PIL import Image
|
||||
from oauth2_provider.models import AccessToken
|
||||
from PIL import Image
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.property_helper import FoodPropertyHelper
|
||||
from cookbook.helper.permission_helper import above_space_limit
|
||||
from cookbook.helper.property_helper import FoodPropertyHelper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
|
||||
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
|
||||
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
||||
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
|
||||
PropertyType, Property)
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Property,
|
||||
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
|
||||
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||
Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||
UserFile, UserPreference, UserSpace, ViewLog)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
from recipes.settings import AWS_ENABLED, MEDIA_URL
|
||||
|
||||
@@ -56,10 +56,9 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
try:
|
||||
if str2bool(
|
||||
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError) as e:
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
try:
|
||||
del fields['image']
|
||||
@@ -103,13 +102,13 @@ class CustomDecimalField(serializers.Field):
|
||||
return round(value, 2).normalize()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) == int or type(data) == float:
|
||||
if isinstance(data, int) or isinstance(data, float):
|
||||
return data
|
||||
elif type(data) == str:
|
||||
elif isinstance(data, str):
|
||||
if data == '':
|
||||
return 0
|
||||
try:
|
||||
return float(data.replace(',', ''))
|
||||
return float(data.replace(',', '.'))
|
||||
except ValueError:
|
||||
raise ValidationError('A valid number is required')
|
||||
|
||||
@@ -145,11 +144,11 @@ class SpaceFilterSerializer(serializers.ListSerializer):
|
||||
def to_representation(self, data):
|
||||
if self.context.get('request', None) is None:
|
||||
return
|
||||
if (type(data) == QuerySet and data.query.is_sliced):
|
||||
if (isinstance(data, QuerySet) and data.query.is_sliced):
|
||||
# if query is sliced it came from api request not nested serializer
|
||||
return super().to_representation(data)
|
||||
if self.child.Meta.model == User:
|
||||
if type(self.context['request'].user) == AnonymousUser:
|
||||
if isinstance(self.context['request'].user, AnonymousUser):
|
||||
data = []
|
||||
else:
|
||||
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
|
||||
@@ -209,7 +208,7 @@ class UserFileSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_preview_link(self, obj):
|
||||
try:
|
||||
img = Image.open(obj.file.file.file)
|
||||
Image.open(obj.file.file.file)
|
||||
return self.context['request'].build_absolute_uri(obj.file.url)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -257,7 +256,7 @@ class UserFileViewSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_preview_link(self, obj):
|
||||
try:
|
||||
img = Image.open(obj.file.file.file)
|
||||
Image.open(obj.file.file.file)
|
||||
return self.context['request'].build_absolute_uri(obj.file.url)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -301,7 +300,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
model = Space
|
||||
fields = (
|
||||
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
|
||||
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',
|
||||
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
|
||||
'image', 'use_plural',)
|
||||
read_only_fields = (
|
||||
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
|
||||
@@ -322,8 +321,8 @@ class UserSpaceSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserSpace
|
||||
fields = ('id', 'user', 'space', 'groups', 'active', 'created_at', 'updated_at',)
|
||||
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
|
||||
fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
|
||||
read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
|
||||
|
||||
|
||||
class SpacedModelSerializer(serializers.ModelSerializer):
|
||||
@@ -335,13 +334,16 @@ class SpacedModelSerializer(serializers.ModelSerializer):
|
||||
class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
obj, created = MealType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = SpaceFilterSerializer
|
||||
model = MealType
|
||||
fields = ('id', 'name', 'order', 'icon', 'color', 'default', 'created_by')
|
||||
fields = ('id', 'name', 'order', 'color', 'default', 'created_by')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@@ -375,7 +377,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
|
||||
'csv_delim', 'csv_prefix',
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients', 'food_children_exist'
|
||||
)
|
||||
|
||||
|
||||
@@ -448,7 +450,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||
class Meta:
|
||||
model = Keyword
|
||||
fields = (
|
||||
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
|
||||
'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
|
||||
'updated_at', 'full_name')
|
||||
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
|
||||
|
||||
@@ -457,17 +459,17 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin)
|
||||
recipe_filter = 'steps__ingredients__unit'
|
||||
|
||||
def create(self, validated_data):
|
||||
name = validated_data.pop('name').strip()
|
||||
# get_or_create drops any field that contains '__' when creating so values must be included in validated data
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
if x := validated_data.get('name', None):
|
||||
validated_data['name'] = x.strip()
|
||||
if x := validated_data.get('name', None):
|
||||
validated_data['plural_name'] = x.strip()
|
||||
|
||||
if plural_name := validated_data.pop('plural_name', None):
|
||||
plural_name = plural_name.strip()
|
||||
|
||||
if unit := Unit.objects.filter(Q(name=name) | Q(plural_name=name)).first():
|
||||
if unit := Unit.objects.filter(Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']), space=space).first():
|
||||
return unit
|
||||
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space,
|
||||
defaults=validated_data)
|
||||
obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -478,16 +480,16 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin)
|
||||
|
||||
class Meta:
|
||||
model = Unit
|
||||
fields = ('id', 'name', 'plural_name', 'description', 'numrecipe', 'image', 'open_data_slug')
|
||||
fields = ('id', 'name', 'plural_name', 'description', 'base_unit', 'numrecipe', 'image', 'open_data_slug')
|
||||
read_only_fields = ('id', 'numrecipe', 'image')
|
||||
|
||||
|
||||
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin):
|
||||
|
||||
def create(self, validated_data):
|
||||
name = validated_data.pop('name').strip()
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = SupermarketCategory.objects.get_or_create(name=name, space=space)
|
||||
obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -509,31 +511,35 @@ class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
|
||||
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin):
|
||||
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = Supermarket
|
||||
fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
|
||||
|
||||
|
||||
class PropertyTypeSerializer(OpenDataModelMixin):
|
||||
class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
|
||||
id = serializers.IntegerField(required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
|
||||
if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).first():
|
||||
return property_type
|
||||
|
||||
return super().create(validated_data)
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = PropertyType
|
||||
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
|
||||
fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug', 'fdc_id',)
|
||||
|
||||
|
||||
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
property_type = PropertyTypeSerializer()
|
||||
property_amount = CustomDecimalField()
|
||||
|
||||
# TODO prevent updates
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
@@ -541,7 +547,6 @@ class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = Property
|
||||
fields = ('id', 'property_amount', 'property_type')
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class RecipeSimpleSerializer(WritableNestedModelSerializer):
|
||||
@@ -572,7 +577,6 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
|
||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
|
||||
# shopping = serializers.SerializerMethodField('get_shopping_status')
|
||||
shopping = serializers.ReadOnlyField(source='shopping_status')
|
||||
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||
child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||
@@ -582,6 +586,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
|
||||
properties = PropertySerializer(many=True, allow_null=True, required=False)
|
||||
properties_food_unit = UnitSerializer(allow_null=True, required=False)
|
||||
properties_food_amount = CustomDecimalField(required=False)
|
||||
|
||||
recipe_filter = 'steps__ingredients__food'
|
||||
images = ['recipe__image']
|
||||
@@ -610,9 +615,6 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
|
||||
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
|
||||
|
||||
# def get_shopping_status(self, obj):
|
||||
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
|
||||
|
||||
def create(self, validated_data):
|
||||
name = validated_data['name'].strip()
|
||||
|
||||
@@ -635,7 +637,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
validated_data['recipe'] = Recipe.objects.get(**recipe)
|
||||
|
||||
# assuming if on hand for user also onhand for shopping_share users
|
||||
if not onhand is None:
|
||||
if onhand is not None:
|
||||
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||
if self.instance:
|
||||
onhand_users = self.instance.onhand_users.all()
|
||||
@@ -649,8 +651,15 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
if properties_food_unit := validated_data.pop('properties_food_unit', None):
|
||||
properties_food_unit = Unit.objects.filter(name=properties_food_unit['name']).first()
|
||||
|
||||
properties = validated_data.pop('properties', None)
|
||||
|
||||
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, properties_food_unit=properties_food_unit,
|
||||
defaults=validated_data)
|
||||
|
||||
if properties and len(properties) > 0:
|
||||
for p in properties:
|
||||
obj.properties.add(Property.objects.create(property_type_id=p['property_type']['id'], property_amount=p['property_amount'], space=space))
|
||||
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -661,7 +670,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
# assuming if on hand for user also onhand for shopping_share users
|
||||
onhand = validated_data.get('food_onhand', None)
|
||||
reset_inherit = self.initial_data.get('reset_inherit', False)
|
||||
if not onhand is None:
|
||||
if onhand is not None:
|
||||
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||
if onhand:
|
||||
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
|
||||
@@ -677,8 +686,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = (
|
||||
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe',
|
||||
'properties', 'properties_food_amount', 'properties_food_unit',
|
||||
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
|
||||
'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
|
||||
'food_onhand', 'supermarket_category',
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
||||
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
|
||||
@@ -756,14 +765,15 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
def get_step_recipe_data(self, obj):
|
||||
# check if root type is recipe to prevent infinite recursion
|
||||
# can be improved later to allow multi level embedding
|
||||
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
||||
if obj.step_recipe and isinstance(self.parent.root, RecipeSerializer):
|
||||
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = (
|
||||
'id', 'name', 'instruction', 'ingredients', 'ingredients_markdown',
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data', 'numrecipe'
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe',
|
||||
'step_recipe_data', 'numrecipe', 'show_ingredients_table'
|
||||
)
|
||||
|
||||
|
||||
@@ -792,9 +802,17 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
|
||||
return text + f' = {round(obj.converted_amount)} {obj.converted_unit}'
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
validated_data['space'] = validated_data.pop('space', self.context['request'].space)
|
||||
try:
|
||||
return UnitConversion.objects.get(
|
||||
food__name__iexact=validated_data.get('food', {}).get('name', None),
|
||||
base_unit__name__iexact=validated_data.get('base_unit', {}).get('name', None),
|
||||
converted_unit__name__iexact=validated_data.get('converted_unit', {}).get('name', None),
|
||||
space=validated_data['space']
|
||||
)
|
||||
except UnitConversion.DoesNotExist:
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = UnitConversion
|
||||
@@ -930,7 +948,7 @@ class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer)
|
||||
|
||||
class Meta:
|
||||
model = RecipeBook
|
||||
fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by', 'filter')
|
||||
fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@@ -947,8 +965,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
recipe = validated_data['recipe']
|
||||
if not book.get_owner() == self.context['request'].user and not self.context[
|
||||
'request'].user in book.get_shared():
|
||||
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
||||
raise NotFound(detail=None, code=None)
|
||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||
return obj
|
||||
@@ -986,12 +1003,22 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
model = MealPlan
|
||||
fields = (
|
||||
'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
|
||||
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
|
||||
'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name',
|
||||
'meal_type_name', 'shopping'
|
||||
)
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
class AutoMealPlanSerializer(serializers.Serializer):
|
||||
start_date = serializers.DateField()
|
||||
end_date = serializers.DateField()
|
||||
meal_type_id = serializers.IntegerField()
|
||||
keywords = KeywordSerializer(many=True)
|
||||
servings = CustomDecimalField()
|
||||
shared = UserSerializer(many=True, required=False, allow_null=True)
|
||||
addshopping = serializers.BooleanField()
|
||||
|
||||
|
||||
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
|
||||
recipe_name = serializers.ReadOnlyField(source='recipe.name')
|
||||
@@ -1238,7 +1265,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = InviteLink
|
||||
fields = (
|
||||
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'created_by', 'created_at',)
|
||||
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by', 'created_at',)
|
||||
read_only_fields = ('id', 'uuid', 'created_by', 'created_at',)
|
||||
|
||||
|
||||
@@ -1290,7 +1317,7 @@ class AccessTokenSerializer(serializers.ModelSerializer):
|
||||
class KeywordExportSerializer(KeywordSerializer):
|
||||
class Meta:
|
||||
model = Keyword
|
||||
fields = ('name', 'icon', 'description', 'created_at', 'updated_at')
|
||||
fields = ('name', 'description', 'created_at', 'updated_at')
|
||||
|
||||
|
||||
class NutritionInformationExportSerializer(NutritionInformationSerializer):
|
||||
@@ -1343,7 +1370,7 @@ class StepExportSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
|
||||
fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header', 'show_ingredients_table')
|
||||
|
||||
|
||||
class RecipeExportSerializer(WritableNestedModelSerializer):
|
||||
@@ -1355,7 +1382,7 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
|
||||
model = Recipe
|
||||
fields = (
|
||||
'name', 'description', 'keywords', 'steps', 'working_time',
|
||||
'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text',
|
||||
'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text', 'source_url',
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from decimal import Decimal
|
||||
from functools import wraps
|
||||
|
||||
from django.conf import settings
|
||||
@@ -13,12 +12,11 @@ from django_scopes import scope, scopes_disabled
|
||||
from cookbook.helper.cache_helper import CacheHelper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
|
||||
ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit, PropertyType)
|
||||
from cookbook.models import (Food, MealPlan, PropertyType, Recipe, SearchFields, SearchPreference,
|
||||
Step, Unit, UserPreference)
|
||||
|
||||
SQLITE = True
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']:
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
|
||||
SQLITE = False
|
||||
|
||||
|
||||
|
||||
@@ -51,13 +51,6 @@
|
||||
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 235.62851,202.1526 c -3.38906,-0.31992 -6.54323,1.7722 -7.40937,5.07666 l -3.10593,11.84344 c 39.34747,1.15551 65.965,27.49017 67.63017,66.72064 l 11.9414,-3.32129 c 3.29677,-0.91767 5.34573,-4.14216 4.95356,-7.55606 -4.37894,-38.04972 -35.85985,-69.14953 -74.00983,-72.76339 z m -12.26226,23.57322 -20.94044,79.87325 a 3.3995443,3.4118034 0 0 0 4.19438,4.15688 L 286.10367,287.635 c -0.8955,-36.81001 -25.8122,-61.48823 -62.73742,-61.90076 z m 5.78824,63.9529 a 6.7110067,6.7352075 0 1 1 6.711,-6.73521 6.7110067,6.7352075 0 0 1 -6.711,6.73521 z M 239.221,257.68648 a 6.7110067,6.7352075 0 1 1 6.71101,-6.7352 6.7110067,6.7352075 0 0 1 -6.71101,6.7352 z m 21.81077,21.88943 a 6.7110067,6.7352075 0 1 1 6.71101,-6.73521 6.7110067,6.7352075 0 0 1 -6.71101,6.73521 z"
|
||||
id="path2"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -7,8 +7,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="2000px"
|
||||
height="2000px"
|
||||
|
||||
viewBox="0 0 2000 2000"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
8
cookbook/static/themes/tandoor.min.css
vendored
8
cookbook/static/themes/tandoor.min.css
vendored
@@ -4634,13 +4634,15 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
.card-img-top {
|
||||
width: 100%;
|
||||
border-top-left-radius: calc(.25rem - 1px);
|
||||
border-top-right-radius: calc(.25rem - 1px)
|
||||
border-top-right-radius: calc(.25rem - 1px);
|
||||
background-color: #f5f5f6;
|
||||
}
|
||||
|
||||
.card-img-bottom {
|
||||
width: 100%;
|
||||
border-bottom-right-radius: calc(.25rem - 1px);
|
||||
border-bottom-left-radius: calc(.25rem - 1px)
|
||||
border-bottom-left-radius: calc(.25rem - 1px);
|
||||
background-color: #f5f5f6;
|
||||
}
|
||||
|
||||
.card-deck {
|
||||
@@ -10479,4 +10481,4 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([clas
|
||||
.ghost {
|
||||
opacity: 0.5 !important;
|
||||
background: #b98766 !important;
|
||||
}
|
||||
}
|
||||
|
||||
10564
cookbook/static/themes/tandoor_dark.min.css
vendored
Normal file
10564
cookbook/static/themes/tandoor_dark.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,7 @@ from django.utils.html import format_html
|
||||
from django.utils.translation import gettext as _
|
||||
from django_tables2.utils import A
|
||||
|
||||
from .models import (CookLog, InviteLink, Recipe, RecipeImport,
|
||||
Storage, Sync, SyncLog, ViewLog)
|
||||
from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog
|
||||
|
||||
|
||||
class StorageTable(tables.Table):
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<meta name="msapplication-TileColor" content="#161616">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||
|
||||
<!-- Bootstrap 4 -->
|
||||
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
|
||||
@@ -73,7 +74,7 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}"
|
||||
<nav class="navbar navbar-expand-lg {% nav_color request %}"
|
||||
id="id_main_nav"
|
||||
style="{% sticky_nav request %}">
|
||||
|
||||
@@ -81,7 +82,7 @@
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
|
||||
aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="Logo">
|
||||
<img class="brand-icon" src="{% logo_url request %}" alt="Logo">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -95,7 +96,7 @@
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
|
||||
aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="Logo">
|
||||
<img class="brand-icon" src="{% logo_url request %}" alt="Logo">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -135,10 +136,10 @@
|
||||
<i class="fas fa-fw fa-toolbox fa-lg"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-center dropdown-menu-center-large">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="row m-0 mt-1 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_keyword' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-tags fa-2x"></i>
|
||||
</div>
|
||||
@@ -150,7 +151,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_food' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-leaf fa-2x"></i>
|
||||
</div>
|
||||
@@ -162,7 +163,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_unit' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-balance-scale fa-2x"></i>
|
||||
</div>
|
||||
@@ -173,10 +174,10 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="row m-0 mt-1 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_supermarket' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-store-alt fa-2x"></i>
|
||||
</div>
|
||||
@@ -188,7 +189,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_supermarket_category' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-cubes fa-2x"></i>
|
||||
</div>
|
||||
@@ -200,7 +201,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_automation' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-robot fa-2x"></i>
|
||||
</div>
|
||||
@@ -211,10 +212,10 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="row m-0 mt-1 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_user_file' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file fa-2x"></i>
|
||||
</div>
|
||||
@@ -226,7 +227,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'data_batch_edit' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-edit fa-2x"></i>
|
||||
</div>
|
||||
@@ -238,7 +239,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_history' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-history fa-2x"></i>
|
||||
</div>
|
||||
@@ -249,10 +250,10 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="row m-0 mt-1 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_ingredient_editor' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-th-list fa-2x"></i>
|
||||
</div>
|
||||
@@ -264,7 +265,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_export' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file-export fa-2x"></i>
|
||||
</div>
|
||||
@@ -276,7 +277,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_property_type' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-database fa-2x"></i>
|
||||
</div>
|
||||
@@ -287,10 +288,10 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="row m-0 mt-1 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_unit_conversion' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card p-1 pt-2 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-exchange-alt fa-2x"></i>
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,14 @@
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
|
||||
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="app">
|
||||
<export-view></export-view>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
@@ -23,11 +20,13 @@
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.EXPORT_ID = {{pk}};
|
||||
{% if pk %}
|
||||
window.EXPORT_ID = {{ pk }}
|
||||
{% else %}
|
||||
window.EXPORT_ID = null
|
||||
{% endif %}
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}';
|
||||
</script>
|
||||
|
||||
{% render_bundle 'export_view' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
{% block title %}{% trans 'Export' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="app">
|
||||
<export-response-view></export-response-view>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
@@ -24,7 +21,11 @@
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.EXPORT_ID = {{pk}};
|
||||
{% if pk %}
|
||||
window.EXPORT_ID = {{ pk }}
|
||||
{% else %}
|
||||
window.EXPORT_ID = null
|
||||
{% endif %}
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
{
|
||||
"name": "Tandoor Recipes",
|
||||
"short_name": "Tandoor",
|
||||
"description": "Application to manage, tag and search recipes.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/assets/logo_color144.png",
|
||||
"type": "image/png",
|
||||
"sizes": "144x144"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/logo_color512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "./search",
|
||||
"background_color": "#ffcb76",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"theme_color": "#ffcb76",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Plan",
|
||||
"short_name": "Plan",
|
||||
"description": "View your meal Plan",
|
||||
"url": "./plan"
|
||||
},
|
||||
{
|
||||
"name": "Books",
|
||||
"short_name": "Cookbooks",
|
||||
"description": "View your cookbooks",
|
||||
"url": "./books"
|
||||
},
|
||||
{
|
||||
"name": "Shopping",
|
||||
"short_name": "Shopping",
|
||||
"description": "View your shopping lists",
|
||||
"url": "./list/shopping-list/"
|
||||
}
|
||||
]
|
||||
"name": "Tandoor Recipes",
|
||||
"short_name": "Tandoor",
|
||||
"description": "Application to manage, tag and search recipes.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/assets/logo_color144.png",
|
||||
"type": "image/png",
|
||||
"sizes": "144x144"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/logo_color512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "./search",
|
||||
"background_color": "#ffcb76",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"theme_color": "#ffcb76",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Plan",
|
||||
"short_name": "Plan",
|
||||
"description": "View your meal Plan",
|
||||
"url": "./plan"
|
||||
},
|
||||
{
|
||||
"name": "Books",
|
||||
"short_name": "Cookbooks",
|
||||
"description": "View your cookbooks",
|
||||
"url": "./books"
|
||||
},
|
||||
{
|
||||
"name": "Shopping",
|
||||
"short_name": "Shopping",
|
||||
"description": "View your shopping lists",
|
||||
"url": "./list/shopping-list/"
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/data/import/url",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"url": "url",
|
||||
"text": "text"
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,29 +11,39 @@
|
||||
{% block content %}
|
||||
|
||||
<h1>{% trans 'System' %}</h1>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h3>{% trans 'System Information' %}</h3>
|
||||
|
||||
{% blocktrans %}
|
||||
Django Recipes is an open source free software application. It can be found on
|
||||
<a href="https://github.com/vabene1111/recipes">GitHub</a>.
|
||||
Changelogs can be found <a href="https://github.com/vabene1111/recipes/releases">here</a>.
|
||||
{% endblocktrans %}
|
||||
<br/>
|
||||
<br/>
|
||||
Current Version: {% if version and version != '' %}
|
||||
<a href="https://github.com/vabene1111/recipes/releases/tag/{{ version }}">{{ version }}</a>{% else %}
|
||||
{{ version }}{% endif %}<br/>
|
||||
Ref: <a href="https://github.com/vabene1111/recipes/commit/{{ ref }}">{{ ref }}</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<h4>{% trans 'Media Serving' %} <span class="badge badge-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
|
||||
|
||||
<h3 class="mt-5">{% trans 'System Information' %}</h3>
|
||||
{% if version_info %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="list-group">
|
||||
{% for v in version_info %}
|
||||
<div class="list-group-item list-group-item-action">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ v.name }} ({{ v.branch }}) {% if v.tag %}- {{ v.tag }}{% endif %}</h5>
|
||||
</div>
|
||||
<pre class="card-text p-2" style="border: 1px solid lightgrey; border-radius: 5px" target="_blank">{{ v.version }}</pre>
|
||||
<a href="{{ v.website }}">Website</a>
|
||||
{% if v.commit_link %}
|
||||
- <a href="{{ v.commit_link }}" target="_blank">Commit</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% blocktrans %}
|
||||
You need to execute <code>version.py</code> in your update script to generate version information (done automatically in docker).
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
|
||||
<h4 class="mt-3">{% trans 'Media Serving' %} <span class="badge badge-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
|
||||
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
|
||||
{% if gunicorn_media %}
|
||||
{% blocktrans %}Serving media files directly using gunicorn/python is <b>not recommend</b>!
|
||||
@@ -44,10 +54,9 @@
|
||||
{% else %}
|
||||
{% trans 'Everything is fine!' %}
|
||||
{% endif %}
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h4>{% trans 'Secret Key' %} <span
|
||||
|
||||
<h4 class="mt-3">{% trans 'Secret Key' %} <span
|
||||
class="badge badge-{% if secret_key %}danger{% else %}success{% endif %}">{% if secret_key %}
|
||||
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
|
||||
{% if secret_key %}
|
||||
@@ -60,10 +69,8 @@
|
||||
{% else %}
|
||||
{% trans 'Everything is fine!' %}
|
||||
{% endif %}
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h4>{% trans 'Debug Mode' %} <span
|
||||
<h4 class="mt-3">{% trans 'Debug Mode' %} <span
|
||||
class="badge badge-{% if debug %}danger{% else %}success{% endif %}">{% if debug %}
|
||||
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
|
||||
{% if debug %}
|
||||
@@ -75,10 +82,8 @@
|
||||
{% else %}
|
||||
{% trans 'Everything is fine!' %}
|
||||
{% endif %}
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h4>{% trans 'Database' %} <span
|
||||
<h4 class="mt-3">{% trans 'Database' %} <span
|
||||
class="badge badge-{% if postgres %}warning{% else %}success{% endif %}">{% if postgres %}
|
||||
{% trans 'Info' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
|
||||
{% if postgres %}
|
||||
@@ -89,9 +94,8 @@
|
||||
{% else %}
|
||||
{% trans 'Everything is fine!' %}
|
||||
{% endif %}
|
||||
<br/>
|
||||
<br/>
|
||||
<h4>Debug</h4>
|
||||
|
||||
<h4 class="mt-3">Debug</h4>
|
||||
<textarea class="form-control" rows="20">
|
||||
Gunicorn Media: {{ gunicorn_media }}
|
||||
Sqlite: {{ postgres }}
|
||||
@@ -99,9 +103,9 @@ Debug: {{ debug }}
|
||||
|
||||
{% for key,value in request.META.items %}{% if key in 'SERVER_PORT,REMOTE_HOST,REMOTE_ADDR,SERVER_PROTOCOL' %}{{ key }}:{{ value }}
|
||||
{% endif %}{% endfor %}
|
||||
{% for key,value in request.META.items %}{% if 'HTTP_' in key %}{{ key }}:{{ value }}
|
||||
{% for key,value in request.META.items %}{% if 'HTTP_' in key %}{{ key }}:{{ value }}
|
||||
{% endif %}{% endfor %}
|
||||
{% for key,value in request.META.items %}{% if 'wsgi.' in key %}{{ key }}:{{ value }}
|
||||
{% for key,value in request.META.items %}{% if 'wsgi.' in key %}{{ key }}:{{ value }}
|
||||
{% endif %}{% endfor %}
|
||||
</textarea>
|
||||
<br/>
|
||||
|
||||
@@ -3,20 +3,19 @@ from gettext import gettext as _
|
||||
|
||||
import bleach
|
||||
import markdown as md
|
||||
from django_scopes import ScopeError
|
||||
from markdown.extensions.tables import TableExtension
|
||||
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||
from django import template
|
||||
from django.db.models import Avg
|
||||
from django.templatetags.static import static
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django_scopes import ScopeError
|
||||
from markdown.extensions.tables import TableExtension
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from cookbook.models import Space, get_model_name
|
||||
from cookbook.models import get_model_name
|
||||
from recipes import settings
|
||||
from recipes.settings import STATIC_URL, PLUGINS
|
||||
from recipes.settings import PLUGINS, STATIC_URL
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -46,9 +45,17 @@ def delete_url(model, pk):
|
||||
|
||||
@register.filter()
|
||||
def markdown(value):
|
||||
tags = markdown_tags + [
|
||||
tags = {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"b", "i", "strong", "em", "tt",
|
||||
"p", "br",
|
||||
"span", "div", "blockquote", "code", "pre", "hr",
|
||||
"ul", "ol", "li", "dd", "dt",
|
||||
"img",
|
||||
"a",
|
||||
"sub", "sup",
|
||||
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead'
|
||||
]
|
||||
}
|
||||
parsed_md = md.markdown(
|
||||
value,
|
||||
extensions=[
|
||||
@@ -56,9 +63,14 @@ def markdown(value):
|
||||
UrlizeExtension(), MarkdownFormatExtension()
|
||||
]
|
||||
)
|
||||
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
|
||||
parsed_md = parsed_md[3:] # remove outer paragraph
|
||||
parsed_md = parsed_md[:len(parsed_md)-4]
|
||||
markdown_attrs = {
|
||||
"*": ["id", "class"],
|
||||
"img": ["src", "alt", "title"],
|
||||
"a": ["href", "alt", "title"],
|
||||
}
|
||||
|
||||
parsed_md = parsed_md[3:] # remove outer paragraph
|
||||
parsed_md = parsed_md[:len(parsed_md) - 4]
|
||||
return bleach.clean(parsed_md, tags, markdown_attrs)
|
||||
|
||||
|
||||
@@ -132,6 +144,7 @@ def is_debug():
|
||||
def markdown_link():
|
||||
return f"{_('You can use markdown to format this field. See the ')}<a target='_blank' href='{reverse('docs_markdown')}'>{_('docs here')}</a>"
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def plugin_dropdown_nav_templates():
|
||||
templates = []
|
||||
@@ -140,6 +153,7 @@ def plugin_dropdown_nav_templates():
|
||||
templates.append(p['nav_dropdown'])
|
||||
return templates
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def plugin_main_nav_templates():
|
||||
templates = []
|
||||
@@ -187,7 +201,8 @@ def base_path(request, path_type):
|
||||
|
||||
@register.simple_tag
|
||||
def user_prefs(request):
|
||||
from cookbook.serializer import UserPreferenceSerializer # putting it with imports caused circular execution
|
||||
from cookbook.serializer import \
|
||||
UserPreferenceSerializer # putting it with imports caused circular execution
|
||||
try:
|
||||
return UserPreferenceSerializer(request.user.userpreference, context={'request': request}).data
|
||||
except AttributeError:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user