mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-26 19:59:15 -05:00
Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42d784aeb | ||
|
|
ce84b3b385 | ||
|
|
74fbcb03a1 | ||
|
|
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 | ||
|
|
4a390b5824 | ||
|
|
785dc15cd9 | ||
|
|
31f3425354 | ||
|
|
689eb426ea |
@@ -3,7 +3,6 @@ npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
|
||||
@@ -100,10 +100,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
|
||||
|
||||
10
.github/workflows/build-docker-open-data.yml
vendored
10
.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
|
||||
|
||||
18
.github/workflows/build-docker.yml
vendored
18
.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:
|
||||
|
||||
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"]
|
||||
@@ -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):
|
||||
@@ -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)
|
||||
|
||||
@@ -46,6 +46,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
fields = (
|
||||
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
||||
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
|
||||
'show_step_ingredients',
|
||||
)
|
||||
|
||||
labels = {
|
||||
@@ -60,7 +61,8 @@ 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 = {
|
||||
@@ -82,7 +84,8 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
'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 = {
|
||||
|
||||
@@ -3,8 +3,10 @@ import string
|
||||
import unicodedata
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Lower
|
||||
|
||||
from cookbook.models import Unit, Food, Automation, Ingredient
|
||||
from cookbook.models import Automation, Food, Ingredient, Unit
|
||||
|
||||
|
||||
class IngredientParser:
|
||||
@@ -12,6 +14,8 @@ class IngredientParser:
|
||||
ignore_rules = False
|
||||
food_aliases = {}
|
||||
unit_aliases = {}
|
||||
never_unit = {}
|
||||
transpose_words = {}
|
||||
|
||||
def __init__(self, request, cache_mode, ignore_automations=False):
|
||||
"""
|
||||
@@ -29,7 +33,7 @@ class IngredientParser:
|
||||
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
|
||||
self.food_aliases[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
||||
|
||||
UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
|
||||
@@ -38,11 +42,33 @@ class IngredientParser:
|
||||
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
|
||||
self.unit_aliases[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
||||
|
||||
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)
|
||||
|
||||
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():
|
||||
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.food_aliases = {}
|
||||
self.unit_aliases = {}
|
||||
self.never_unit = {}
|
||||
self.transpose_words = {}
|
||||
|
||||
def apply_food_automation(self, food):
|
||||
"""
|
||||
@@ -55,11 +81,11 @@ class IngredientParser:
|
||||
else:
|
||||
if self.food_aliases:
|
||||
try:
|
||||
return self.food_aliases[food]
|
||||
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=food, disabled=False).order_by('order').first():
|
||||
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 food
|
||||
|
||||
@@ -72,13 +98,13 @@ class IngredientParser:
|
||||
if self.ignore_rules:
|
||||
return unit
|
||||
else:
|
||||
if self.unit_aliases:
|
||||
if self.transpose_words:
|
||||
try:
|
||||
return self.unit_aliases[unit]
|
||||
return self.unit_aliases[unit.lower()]
|
||||
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():
|
||||
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 unit
|
||||
|
||||
@@ -133,10 +159,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 +186,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
|
||||
@@ -205,6 +232,67 @@ class IngredientParser:
|
||||
food, note = self.parse_food_with_comma(tokens)
|
||||
return food, note
|
||||
|
||||
def apply_never_unit_automations(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.ignore_rules:
|
||||
return tokens
|
||||
|
||||
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 automation := 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 = automation.param_2
|
||||
never_unit = True
|
||||
|
||||
if never_unit:
|
||||
tokens.insert(1, new_unit)
|
||||
|
||||
return tokens
|
||||
|
||||
def apply_transpose_words_automations(self, ingredient):
|
||||
"""
|
||||
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.ignore_rules:
|
||||
return ingredient
|
||||
|
||||
else:
|
||||
tokens = [x.lower() for x in ingredient.replace(',', ' ').split()]
|
||||
if self.transpose_words:
|
||||
filtered_rules = {}
|
||||
for key, value in self.transpose_words.items():
|
||||
if value[0] in tokens and value[1] in tokens:
|
||||
filtered_rules[key] = value
|
||||
for k, v in filtered_rules.items():
|
||||
ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, 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(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'):
|
||||
ingredient = re.sub(rf"\b({rule.param_1})\W*({rule.param_1})\b", r"\2 \1", ingredient, flags=re.IGNORECASE)
|
||||
return ingredient
|
||||
|
||||
def parse(self, ingredient):
|
||||
"""
|
||||
Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ...
|
||||
@@ -230,8 +318,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 +327,14 @@ 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)
|
||||
|
||||
ingredient = self.apply_transpose_words_automations(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 +347,7 @@ class IngredientParser:
|
||||
# three arguments if it already has a unit there can't be
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
tokens = self.apply_never_unit_automations(tokens)
|
||||
try:
|
||||
if unit is not None:
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
|
||||
@@ -322,7 +322,7 @@ 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 +332,7 @@ 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):
|
||||
@@ -434,3 +434,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
|
||||
|
||||
@@ -34,7 +34,7 @@ class FoodPropertyHelper:
|
||||
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
|
||||
|
||||
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, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'order': fpt.order, 'food_values': {}, 'total_value': 0, 'missing_value': False}
|
||||
|
||||
uch = UnitConversionHelper(self.space)
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ class RecipeSearch():
|
||||
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 +88,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:
|
||||
@@ -150,7 +153,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:
|
||||
@@ -434,22 +437,21 @@ class RecipeSearch():
|
||||
|
||||
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))
|
||||
|
||||
@@ -560,7 +562,7 @@ class RecipeSearch():
|
||||
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]
|
||||
|
||||
@@ -15,7 +15,6 @@ from recipe_scrapers._utils import get_host_name, get_minutes
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import Automation, Keyword, PropertyType
|
||||
|
||||
|
||||
# from unicodedata import decomposition
|
||||
|
||||
|
||||
@@ -51,7 +50,8 @@ def get_from_scraper(scrape, request):
|
||||
recipe_json['internal'] = True
|
||||
|
||||
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
|
||||
|
||||
@@ -147,7 +147,7 @@ def get_from_scraper(scrape, request):
|
||||
recipe_json['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:
|
||||
@@ -156,7 +156,14 @@ def get_from_scraper(scrape, request):
|
||||
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]
|
||||
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)
|
||||
@@ -206,7 +213,14 @@ def get_from_scraper(scrape, request):
|
||||
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]
|
||||
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']:
|
||||
@@ -272,7 +286,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 +330,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 +355,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 +372,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 +386,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 +408,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:
|
||||
@@ -413,7 +427,7 @@ def parse_keywords(keyword_json, space):
|
||||
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
|
||||
keyword_aliases[a.param_1.lower()] = a.param_2
|
||||
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
|
||||
|
||||
# keywords as list
|
||||
@@ -424,7 +438,7 @@ def parse_keywords(keyword_json, space):
|
||||
if len(kw) != 0:
|
||||
if keyword_aliases:
|
||||
try:
|
||||
kw = keyword_aliases[kw]
|
||||
kw = keyword_aliases[kw.lower()]
|
||||
except KeyError:
|
||||
pass
|
||||
if k := Keyword.objects.filter(name=kw, space=space).first():
|
||||
@@ -438,15 +452,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 +514,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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,7 +55,7 @@ class Chowdown(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
@@ -47,7 +47,7 @@ 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']
|
||||
|
||||
@@ -50,7 +50,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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 as e:
|
||||
traceback.print_exc()
|
||||
return recipe
|
||||
|
||||
def decode_recipe(self, string):
|
||||
|
||||
@@ -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'] != '':
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -67,7 +67,7 @@ class MelaRecipes(Integration):
|
||||
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,
|
||||
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ class NextcloudCookbook(Integration):
|
||||
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:
|
||||
|
||||
@@ -51,7 +51,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:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -49,7 +49,7 @@ class RecipeSage(Integration):
|
||||
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,
|
||||
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -37,7 +37,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
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-08-13 08:19+0000\n"
|
||||
"Last-Translator: Fabian Flodman <fabian@flodman.de>\n"
|
||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/de/>\n"
|
||||
"Language: de\n"
|
||||
@@ -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-08-27 11:19+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-08-16 21:19+0000\n"
|
||||
"Last-Translator: Alexandre Braure <alex@tkclab.ca>\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"
|
||||
@@ -549,7 +549,7 @@ msgstr "Il est nécessaire de fournir soit le queryset, soit la clé de hachage"
|
||||
#, fuzzy
|
||||
#| msgid "Use fractions"
|
||||
msgid "reverse rotation"
|
||||
msgstr "Utiliser les fractions"
|
||||
msgstr "sens inverse"
|
||||
|
||||
#: .\cookbook\helper\recipe_url_import.py:267
|
||||
msgid "careful rotation"
|
||||
@@ -620,10 +620,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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
@@ -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,34 @@
|
||||
# Generated by Django 4.1.10 on 2023-08-25 13:05
|
||||
|
||||
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')],
|
||||
max_length=128),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
@@ -331,6 +331,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
FLATLY = 'FLATLY'
|
||||
SUPERHERO = 'SUPERHERO'
|
||||
TANDOOR = 'TANDOOR'
|
||||
TANDOOR_DARK = 'TANDOOR_DARK'
|
||||
|
||||
THEMES = (
|
||||
(TANDOOR, 'Tandoor'),
|
||||
@@ -338,6 +339,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
(DARKLY, 'Darkly'),
|
||||
(FLATLY, 'Flatly'),
|
||||
(SUPERHERO, 'Superhero'),
|
||||
(TANDOOR_DARK, 'Tandoor Dark (INCOMPLETE)'),
|
||||
)
|
||||
|
||||
# Nav colors
|
||||
@@ -392,6 +394,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 +416,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)
|
||||
|
||||
@@ -574,6 +580,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 +592,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 +724,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 +739,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)
|
||||
|
||||
@@ -779,8 +768,10 @@ 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)
|
||||
|
||||
# TODO show if empty property?
|
||||
@@ -797,6 +788,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):
|
||||
@@ -1142,6 +1134,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')
|
||||
|
||||
@@ -1326,10 +1320,13 @@ 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'
|
||||
|
||||
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')),))
|
||||
(DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),
|
||||
(NEVER_UNIT, _('Never Unit')), (TRANSPOSE_WORDS, _('Transpose Words')),))
|
||||
name = models.CharField(max_length=128, default='')
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import random
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
@@ -109,7 +110,7 @@ class CustomDecimalField(serializers.Field):
|
||||
if data == '':
|
||||
return 0
|
||||
try:
|
||||
return float(data.replace(',', ''))
|
||||
return float(data.replace(',', '.'))
|
||||
except ValueError:
|
||||
raise ValidationError('A valid number is required')
|
||||
|
||||
@@ -322,8 +323,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):
|
||||
@@ -375,7 +376,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'
|
||||
)
|
||||
|
||||
|
||||
@@ -478,7 +479,7 @@ 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')
|
||||
|
||||
|
||||
@@ -514,26 +515,26 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataMo
|
||||
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():
|
||||
if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).filter(space=self.context['request'].space).first():
|
||||
return property_type
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = PropertyType
|
||||
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
|
||||
fields = ('id', 'name', 'icon', 'unit', 'description', 'order', 'open_data_slug')
|
||||
|
||||
|
||||
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 +542,6 @@ class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = Property
|
||||
fields = ('id', 'property_amount', 'property_type')
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class RecipeSimpleSerializer(WritableNestedModelSerializer):
|
||||
@@ -582,6 +582,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']
|
||||
@@ -649,8 +650,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):
|
||||
@@ -677,7 +685,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = (
|
||||
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe',
|
||||
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
|
||||
'properties', 'properties_food_amount', 'properties_food_unit',
|
||||
'food_onhand', 'supermarket_category',
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
||||
@@ -763,7 +771,8 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
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'
|
||||
)
|
||||
|
||||
|
||||
@@ -992,6 +1001,16 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
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 +1257,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',)
|
||||
|
||||
|
||||
@@ -1343,7 +1362,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 +1374,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):
|
||||
|
||||
@@ -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 |
10486
cookbook/static/themes/tandoor_dark.min.css
vendored
Normal file
10486
cookbook/static/themes/tandoor_dark.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 %}">
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -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,7 +63,12 @@ def markdown(value):
|
||||
UrlizeExtension(), MarkdownFormatExtension()
|
||||
]
|
||||
)
|
||||
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
|
||||
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)
|
||||
|
||||
@@ -16,6 +16,7 @@ def theme_url(request):
|
||||
UserPreference.DARKLY: 'themes/darkly.min.css',
|
||||
UserPreference.SUPERHERO: 'themes/superhero.min.css',
|
||||
UserPreference.TANDOOR: 'themes/tandoor.min.css',
|
||||
UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css',
|
||||
}
|
||||
if request.user.userpreference.theme in themes:
|
||||
return static(themes[request.user.userpreference.theme])
|
||||
@@ -26,8 +27,12 @@ def theme_url(request):
|
||||
@register.simple_tag
|
||||
def nav_color(request):
|
||||
if not request.user.is_authenticated:
|
||||
return 'primary'
|
||||
return request.user.userpreference.nav_color.lower()
|
||||
return 'navbar-light bg-primary'
|
||||
|
||||
if request.user.userpreference.nav_color.lower() in ['light', 'warning', 'info', 'success']:
|
||||
return f'navbar-light bg-{request.user.userpreference.nav_color.lower()}'
|
||||
else:
|
||||
return f'navbar-dark bg-{request.user.userpreference.nav_color.lower()}'
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
|
||||
@@ -475,6 +475,7 @@ def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
|
||||
with scope(space=obj_tree_1.space):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
obj_2 = Food.objects.get(id=obj_2.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
|
||||
# should return root objects in the space (obj_1, obj_2), ignoring query filters
|
||||
@@ -499,17 +500,16 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
|
||||
with scope(space=obj_tree_1.space):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
obj_2 = Food.objects.get(id=obj_2.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
obj_2.move(parent, node_location)
|
||||
obj_2 = Food.objects.get(id=obj_2.id)
|
||||
parent = Food.objects.get(id=parent.id)
|
||||
|
||||
# should return full tree starting at parent (obj_tree_1, obj_2), ignoring query filters
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
|
||||
# should return full tree starting at, but excluding parent (obj_tree_1, obj_2), ignoring query filters
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
|
||||
assert response['count'] == 4
|
||||
response = json.loads(u1_s1.get(
|
||||
f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
|
||||
# filtering is ignored - should return identical results as ?tree=x
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
|
||||
assert response['count'] == 4
|
||||
|
||||
|
||||
|
||||
@@ -81,10 +81,10 @@ def test_share_permission(recipe_1_s1, u1_s1, u1_s2, u2_s1, a_u):
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 200],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 200],
|
||||
['a1_s1', 200],
|
||||
['g1_s2', 404],
|
||||
['g1_s2', 403],
|
||||
['u1_s2', 404],
|
||||
['a1_s2', 404],
|
||||
])
|
||||
@@ -140,7 +140,7 @@ def test_update_private_recipe(u1_s1, u2_s1, recipe_1_s1):
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 201],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 201],
|
||||
['a1_s1', 201],
|
||||
])
|
||||
|
||||
@@ -48,7 +48,7 @@ def recipe(request, space_1, u1_s1):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['g1_s1', 204],
|
||||
['g1_s1', 403],
|
||||
['u1_s1', 204],
|
||||
['u1_s2', 404],
|
||||
['a1_s1', 204],
|
||||
|
||||
@@ -15,9 +15,9 @@ DETAIL_URL = 'api:space-detail'
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403, 0],
|
||||
['g1_s1', 403, 0],
|
||||
['u1_s1', 403, 0],
|
||||
['u1_s1', 200, 1],
|
||||
['a1_s1', 200, 1],
|
||||
['a2_s1', 200, 0],
|
||||
['a2_s1', 200, 1],
|
||||
])
|
||||
def test_list_permission(arg, request, space_1, a1_s1):
|
||||
space_1.created_by = auth.get_user(a1_s1)
|
||||
@@ -29,16 +29,6 @@ def test_list_permission(arg, request, space_1, a1_s1):
|
||||
assert len(json.loads(result.content)) == arg[2]
|
||||
|
||||
|
||||
def test_list_permission_owner(u1_s1, a1_s1, space_1):
|
||||
space_1.created_by = auth.get_user(a1_s1)
|
||||
space_1.save()
|
||||
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||
assert u1_s1.get(reverse(LIST_URL)).status_code == 403
|
||||
space_1.created_by = auth.get_user(u1_s1)
|
||||
space_1.save()
|
||||
assert u1_s1.get(reverse(LIST_URL)).status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 403],
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_list_filter(obj_1, obj_2, u1_s1):
|
||||
assert r.status_code == 200
|
||||
response = json.loads(r.content)
|
||||
assert len(response) == 2
|
||||
assert response[0]['name'] == obj_1.name
|
||||
# assert response[0]['name'] == obj_1.name # assuming an order when it's not always valid
|
||||
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||
assert len(response) == 1
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
|
||||
result = c.get(reverse(LIST_URL))
|
||||
assert result.status_code == arg[1]
|
||||
if arg[1] == 200:
|
||||
assert len(json.loads(result.content)) == arg[2]
|
||||
assert len(json.loads(result.content)['results']) == arg[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
|
||||
@@ -334,6 +334,8 @@ class StepFactory(factory.django.DjangoModelFactory):
|
||||
order = factory.Sequence(lambda x: x)
|
||||
# file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
|
||||
show_as_header = True
|
||||
# TODO: need to update to fetch from User's preferences
|
||||
show_ingredients_table = True
|
||||
step_recipe__has_recipe = False
|
||||
ingredients__food_recipe_count = 0
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@@ -7,7 +7,7 @@ from rest_framework.schemas import get_schema_view
|
||||
|
||||
from cookbook.helper import dal
|
||||
from recipes.settings import DEBUG, PLUGINS
|
||||
from recipes.version import VERSION_NUMBER
|
||||
from cookbook.version_info import TANDOOR_VERSION
|
||||
|
||||
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
|
||||
@@ -36,6 +36,7 @@ router.register(r'ingredient', api.IngredientViewSet)
|
||||
router.register(r'invite-link', api.InviteLinkViewSet)
|
||||
router.register(r'keyword', api.KeywordViewSet)
|
||||
router.register(r'meal-plan', api.MealPlanViewSet)
|
||||
router.register(r'auto-plan', api.AutoPlanViewSet, basename='auto-plan')
|
||||
router.register(r'meal-type', api.MealTypeViewSet)
|
||||
router.register(r'recipe', api.RecipeViewSet)
|
||||
router.register(r'recipe-book', api.RecipeBookViewSet)
|
||||
@@ -148,7 +149,7 @@ urlpatterns = [
|
||||
path('docs/search/', views.search_info, name='docs_search'),
|
||||
path('docs/api/', views.api_info, name='docs_api'),
|
||||
|
||||
path('openapi/', get_schema_view(title="Django Recipes", version=VERSION_NUMBER, public=True,
|
||||
path('openapi/', get_schema_view(title="Django Recipes", version=TANDOOR_VERSION, public=True,
|
||||
permission_classes=(permissions.AllowAny,)), name='openapi-schema'),
|
||||
|
||||
path('api/', include((router.urls, 'api'))),
|
||||
|
||||
3
cookbook/version_info.py
Normal file
3
cookbook/version_info.py
Normal file
@@ -0,0 +1,3 @@
|
||||
TANDOOR_VERSION = ""
|
||||
TANDOOR_REF = ""
|
||||
VERSION_INFO = []
|
||||
@@ -1,7 +1,9 @@
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import mimetypes
|
||||
import pathlib
|
||||
import random
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
@@ -25,6 +27,7 @@ from django.core.files import File
|
||||
from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.db.models.functions import Coalesce, Lower
|
||||
from django.db.models.signals import post_save
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
@@ -60,7 +63,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
|
||||
CustomIsSpaceOwner, CustomIsUser, group_required,
|
||||
is_space_owner, switch_user_active_space, above_space_limit,
|
||||
CustomRecipePermission, CustomUserPermission,
|
||||
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission)
|
||||
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission, IsReadOnlyDRF)
|
||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
|
||||
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
@@ -94,7 +97,8 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
|
||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
|
||||
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, PropertySerializer)
|
||||
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer,
|
||||
PropertySerializer, AutoMealPlanSerializer)
|
||||
from cookbook.views.import_export import get_integration
|
||||
from recipes import settings
|
||||
|
||||
@@ -402,11 +406,11 @@ class GroupViewSet(viewsets.ModelViewSet):
|
||||
class SpaceViewSet(viewsets.ModelViewSet):
|
||||
queryset = Space.objects
|
||||
serializer_class = SpaceSerializer
|
||||
permission_classes = [CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
permission_classes = [IsReadOnlyDRF & CustomIsUser | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
http_method_names = ['get', 'patch']
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(id=self.request.space.id, created_by=self.request.user)
|
||||
return self.queryset.filter(id=self.request.space.id)
|
||||
|
||||
|
||||
class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||
@@ -414,6 +418,7 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserSpaceSerializer
|
||||
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
|
||||
http_method_names = ['get', 'patch', 'delete']
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
if request.space.created_by == UserSpace.objects.get(pk=kwargs['pk']).user:
|
||||
@@ -421,6 +426,10 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
internal_note = self.request.query_params.get('internal_note', None)
|
||||
if internal_note is not None:
|
||||
self.queryset = self.queryset.filter(internal_note=internal_note)
|
||||
|
||||
if is_space_owner(self.request.user, self.request.space):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
else:
|
||||
@@ -661,6 +670,66 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class AutoPlanViewSet(viewsets.ViewSet):
|
||||
def create(self, request):
|
||||
serializer = AutoMealPlanSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
keywords = serializer.validated_data['keywords']
|
||||
start_date = serializer.validated_data['start_date']
|
||||
end_date = serializer.validated_data['end_date']
|
||||
servings = serializer.validated_data['servings']
|
||||
shared = serializer.get_initial().get('shared', None)
|
||||
shared_pks = list()
|
||||
if shared is not None:
|
||||
for i in range(len(shared)):
|
||||
shared_pks.append(shared[i]['id'])
|
||||
|
||||
days = min((end_date - start_date).days + 1, 14)
|
||||
|
||||
recipes = Recipe.objects.values('id', 'name')
|
||||
meal_plans = list()
|
||||
|
||||
for keyword in keywords:
|
||||
recipes = recipes.filter(keywords__name=keyword['name'])
|
||||
|
||||
if len(recipes) == 0:
|
||||
return Response(serializer.data)
|
||||
recipes = list(recipes.order_by('?')[:days])
|
||||
|
||||
for i in range(0, days):
|
||||
day = start_date + datetime.timedelta(i)
|
||||
recipe = recipes[i % len(recipes)]
|
||||
args = {'recipe_id': recipe['id'], 'servings': servings,
|
||||
'created_by': request.user,
|
||||
'meal_type_id': serializer.validated_data['meal_type_id'],
|
||||
'note': '', 'date': day, 'space': request.space}
|
||||
|
||||
m = MealPlan(**args)
|
||||
meal_plans.append(m)
|
||||
|
||||
MealPlan.objects.bulk_create(meal_plans)
|
||||
|
||||
for m in meal_plans:
|
||||
m.shared.set(shared_pks)
|
||||
|
||||
if request.data.get('addshopping', False):
|
||||
SLR = RecipeShoppingEditor(user=request.user, space=request.space)
|
||||
SLR.create(mealplan=m, servings=servings)
|
||||
|
||||
else:
|
||||
post_save.send(
|
||||
sender=m.__class__,
|
||||
instance=m,
|
||||
created=True,
|
||||
update_fields=None,
|
||||
)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
|
||||
class MealTypeViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
returns list of meal types created by the
|
||||
@@ -892,7 +961,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
url = serializer.validated_data['image_url']
|
||||
if validators.url(url, public=True):
|
||||
response = requests.get(url)
|
||||
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"})
|
||||
image = File(io.BytesIO(response.content))
|
||||
filetype = mimetypes.guess_extension(response.headers['content-type']) or filetype
|
||||
except UnidentifiedImageError as e:
|
||||
@@ -1047,6 +1116,21 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
Q(created_by=self.request.user)
|
||||
| Q(shoppinglist__shared=self.request.user)
|
||||
| Q(created_by__in=list(self.request.user.get_shopping_share()))
|
||||
).prefetch_related(
|
||||
'created_by',
|
||||
'food',
|
||||
'food__properties',
|
||||
'food__properties__property_type',
|
||||
'food__inherit_fields',
|
||||
'food__supermarket_category',
|
||||
'food__onhand_users',
|
||||
'food__substitute',
|
||||
'food__child_inherit_fields',
|
||||
|
||||
'unit',
|
||||
'list_recipe',
|
||||
'list_recipe__mealplan',
|
||||
'list_recipe__mealplan__recipe',
|
||||
).distinct().all()
|
||||
|
||||
if pk := self.request.query_params.getlist('id', []):
|
||||
@@ -1165,6 +1249,11 @@ class InviteLinkViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
permission_classes = [CustomIsSpaceOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
internal_note = self.request.query_params.get('internal_note', None)
|
||||
if internal_note is not None:
|
||||
self.queryset = self.queryset.filter(internal_note=internal_note)
|
||||
|
||||
if is_space_owner(self.request.user, self.request.space):
|
||||
self.queryset = self.queryset.filter(space=self.request.space).all()
|
||||
return super().get_queryset()
|
||||
@@ -1308,8 +1397,12 @@ def recipe_from_source(request):
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
try:
|
||||
json.loads(data)
|
||||
data = "<script type='application/ld+json'>" + data + "</script>"
|
||||
data_json = json.loads(data)
|
||||
if '@context' not in data_json:
|
||||
data_json['@context'] = 'https://schema.org'
|
||||
if '@type' not in data_json:
|
||||
data_json['@type'] = 'Recipe'
|
||||
data = "<script type='application/ld+json'>" + json.dumps(data_json) + "</script>"
|
||||
except JSONDecodeError:
|
||||
pass
|
||||
scrape = text_scraper(text=data, url=url)
|
||||
|
||||
@@ -37,7 +37,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||
obj.space = self.request.space
|
||||
obj.internal = True
|
||||
obj.save()
|
||||
obj.steps.add(Step.objects.create(space=self.request.space, show_as_header=False))
|
||||
obj.steps.add(Step.objects.create(space=self.request.space, show_as_header=False, show_ingredients_table=self.request.user.userpreference.show_step_ingredients))
|
||||
return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk}))
|
||||
|
||||
def get_success_url(self):
|
||||
|
||||
@@ -22,7 +22,8 @@ from cookbook.helper.permission_helper import group_required, has_group_permissi
|
||||
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink,
|
||||
Space, ViewLog, UserSpace)
|
||||
from cookbook.tables import (CookLogTable, ViewLogTable)
|
||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||
from cookbook.version_info import VERSION_INFO
|
||||
from recipes.settings import PLUGINS
|
||||
|
||||
|
||||
def index(request):
|
||||
@@ -320,8 +321,8 @@ def system(request):
|
||||
'gunicorn_media': settings.GUNICORN_MEDIA,
|
||||
'debug': settings.DEBUG,
|
||||
'postgres': postgres,
|
||||
'version': VERSION_NUMBER,
|
||||
'ref': BUILD_REF,
|
||||
'version_info': VERSION_INFO,
|
||||
'plugins': PLUGINS,
|
||||
'secret_key': secret_key
|
||||
})
|
||||
|
||||
@@ -370,7 +371,7 @@ def invite_link(request, token):
|
||||
link.used_by = request.user
|
||||
link.save()
|
||||
|
||||
user_space = UserSpace.objects.create(user=request.user, space=link.space, active=False)
|
||||
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
|
||||
|
||||
if request.user.userspace_set.count() == 1:
|
||||
user_space.active = True
|
||||
|
||||
@@ -100,15 +100,17 @@ AUTH_LDAP_START_TLS=1
|
||||
AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem
|
||||
```
|
||||
|
||||
## Reverse Proxy Authentication
|
||||
## External Authentication
|
||||
|
||||
!!! warning "Security Impact"
|
||||
If you just set `REMOTE_USER_AUTH=1` without any additional configuration, _anybody_ can authenticate with _any_ username!
|
||||
|
||||
!!! Info "Community Contributed Tutorial"
|
||||
This tutorial was provided by a community member. Since I do not use reverse proxy authentication, I cannot provide any
|
||||
assistance should you choose to use this authentication method.
|
||||
This tutorial was provided by a community member. We are not able to provide any support! Please only use, if you know what you are doing!
|
||||
|
||||
In order use proxy authentication you will need to:
|
||||
In order use external authentication (i.e. using a proxy auth like Authelia, Authentik, etc.) you will need to:
|
||||
|
||||
1. Set `REVERSE_PROXY_AUTH=1` in the `.env` file
|
||||
1. Set `REMOTE_USER_AUTH=1` in the `.env` file
|
||||
2. Update your nginx configuration file
|
||||
|
||||
Using any of the examples above will automatically generate a configuration file inside a docker volume.
|
||||
@@ -116,10 +118,10 @@ Use `docker volume inspect recipes_nginx` to find out where your volume is store
|
||||
|
||||
!!! warning "Configuration File Volume"
|
||||
The nginx config volume is generated when the container is first run. You can change the volume to a bind mount in the
|
||||
warning `docker-compose.yml`, but then you will need to manually create it. See section `Volumes vs Bind Mounts` below
|
||||
`docker-compose.yml`, but then you will need to manually create it. See section `Volumes vs Bind Mounts` below
|
||||
for more information.
|
||||
|
||||
The following example shows a configuration for Authelia:
|
||||
### Configuration Example for Authelia
|
||||
|
||||
```
|
||||
server {
|
||||
@@ -161,7 +163,7 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
Please refer to the appropriate documentation on how to setup the reverse proxy, authentication, and networks.
|
||||
Please refer to the appropriate documentation on how to set up the reverse proxy, authentication, and networks.
|
||||
|
||||
Ensure users have been configured for Authelia, and that the endpoint recipes is pointed to is protected but
|
||||
available.
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
!!! warning
|
||||
Automations are currently in a beta stage. They work pretty stable but if I encounter any
|
||||
issues while working on them, I might change how they work breaking existing automations.
|
||||
I will try to avoid this and am pretty confident it won't happen.
|
||||
Automations are currently in a beta stage. They work pretty stable but if I encounter any
|
||||
issues while working on them, I might change how they work breaking existing automations.
|
||||
I will try to avoid this and am pretty confident it won't happen.
|
||||
|
||||
|
||||
Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that
|
||||
Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that
|
||||
would otherwise have to be done manually. Currently, the following automations are supported.
|
||||
|
||||
## Unit, Food, Keyword Alias
|
||||
|
||||
Foods, Units and Keywords can have automations that automatically replace them with another object
|
||||
to allow aliasing them.
|
||||
to allow aliasing them.
|
||||
|
||||
This helps to add consistency to the naming of objects, for example to always use the singular form
|
||||
for the main name if a plural form is configured.
|
||||
for the main name if a plural form is configured.
|
||||
|
||||
These automations are best created by dragging and dropping Foods, Units or Keywords in their respective
|
||||
views and creating the automation there.
|
||||
These automations are best created by dragging and dropping Foods, Units or Keywords in their respective
|
||||
views and creating the automation there.
|
||||
|
||||
You can also create them manually by setting the following
|
||||
- **Parameter 1**: name of food/unit/keyword to match
|
||||
- **Parameter 2**: name of food/unit/keyword to replace matched food with
|
||||
|
||||
- **Parameter 1**: name of food/unit/keyword to match
|
||||
- **Parameter 2**: name of food/unit/keyword to replace matched food with
|
||||
|
||||
These rules are processed whenever you are importing recipes from websites or other apps
|
||||
and when using the simple ingredient input (shopping, recipe editor, ...).
|
||||
|
||||
## Description Replace
|
||||
This automation is a bit more complicated than the alis rules. It is run when importing a recipe
|
||||
|
||||
This automation is a bit more complicated than the alias rules. It is run when importing a recipe
|
||||
from a website.
|
||||
|
||||
It uses Regular Expressions (RegEx) to determine if a description should be altered, what exactly to remove
|
||||
and what to replace it with.
|
||||
and what to replace it with.
|
||||
|
||||
- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`)
|
||||
- **Parameter 2**: pattern of what to replace (e.g. `.*`)
|
||||
- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced.
|
||||
- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`)
|
||||
- **Parameter 2**: pattern of what to replace (e.g. `.*`)
|
||||
- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced.
|
||||
|
||||
To replace the description the python [re.sub](https://docs.python.org/2/library/re.html#re.sub) function is used
|
||||
like this `re.sub(<parameter 2>, <parameter 2>, <descriotion>, count=1)`
|
||||
@@ -41,24 +43,52 @@ like this `re.sub(<parameter 2>, <parameter 2>, <descriotion>, count=1)`
|
||||
To test out your patterns and learn about RegEx you can use [regexr.com](https://regexr.com/)
|
||||
|
||||
!!! info
|
||||
In order to prevent denial of service attacks on the RegEx engine the number of replace automations
|
||||
and the length of the inputs that are processed are limited. Those limits should never be reached
|
||||
during normal usage.
|
||||
In order to prevent denial of service attacks on the RegEx engine the number of replace automations
|
||||
and the length of the inputs that are processed are limited. Those limits should never be reached
|
||||
during normal usage.
|
||||
|
||||
## Instruction Replace
|
||||
|
||||
This works just like the Description Replace automation but runs against all instruction texts
|
||||
in all steps of a recipe during import.
|
||||
in all steps of a recipe during import.
|
||||
|
||||
Also instead of just replacing a single occurrence of the matched pattern it will replace all.
|
||||
|
||||
## Never Unit
|
||||
|
||||
Some ingredients have a pattern of AMOUNT and FOOD, if the food has multiple words (e.g. egg yolk) this can cause Tandoor
|
||||
to detect the word "egg" as a unit. This automation will detect the word 'egg' as something that should never be considered
|
||||
a unit.
|
||||
|
||||
You can also create them manually by setting the following
|
||||
|
||||
- **Parameter 1**: string to detect
|
||||
- **Parameter 2**: Optional: unit to insert into ingredient (e.g. 1 whole 'egg yolk' instead of 1 <empty> 'egg yolk')
|
||||
|
||||
These rules are processed whenever you are importing recipes from websites or other apps
|
||||
and when using the simple ingredient input (shopping, recipe editor, ...).
|
||||
|
||||
## Transpose Words
|
||||
|
||||
Some recipes list the food before the units for some foods (garlic cloves). This automation will transpose 2 words in an
|
||||
ingredient so "garlic cloves" will automatically become "cloves garlic"
|
||||
|
||||
- **Parameter 1**: first word to detect
|
||||
- **Parameter 2**: second word to detect
|
||||
|
||||
These rules are processed whenever you are importing recipes from websites or other apps
|
||||
and when using the simple ingredient input (shopping, recipe editor, ...).
|
||||
|
||||
# Order
|
||||
If the Automation type allows for more than one rule to be executed (for example description replace)
|
||||
the rules are processed in ascending order (ordered by the *order* property of the automation).
|
||||
The default order is always 1000 to make it easier to add automations before and after other automations.
|
||||
|
||||
If the Automation type allows for more than one rule to be executed (for example description replace)
|
||||
the rules are processed in ascending order (ordered by the _order_ property of the automation).
|
||||
The default order is always 1000 to make it easier to add automations before and after other automations.
|
||||
|
||||
Example:
|
||||
|
||||
1. Rule ABC (order 1000) replaces `everything` with `abc`
|
||||
2. Rule DEF (order 2000) replaces `everything` with `def`
|
||||
3. Rule XYZ (order 500) replaces `everything` with `xyz`
|
||||
|
||||
After processing rules XYZ, then ABC and then DEF the description will have the value `def`
|
||||
After processing rules XYZ, then ABC and then DEF the description will have the value `def`
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "2.4"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- ${POSTGRES_DATA_DIR:-./postgresql}:/var/lib/postgresql/data
|
||||
env_file:
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ${MEDIA_FILES_DIR:-./mediafiles}:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
@@ -41,6 +42,7 @@ services:
|
||||
depends_on:
|
||||
- web_recipes
|
||||
volumes:
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static:ro
|
||||
- ${MEDIA_FILES_DIR:-./mediafiles}:/media:ro
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
@@ -32,6 +33,7 @@ services:
|
||||
depends_on:
|
||||
- web_recipes
|
||||
volumes:
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static:ro
|
||||
- ./mediafiles:/media:ro
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
@@ -15,6 +15,7 @@ services:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
@@ -30,6 +31,7 @@ services:
|
||||
depends_on:
|
||||
- web_recipes
|
||||
volumes:
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static:ro
|
||||
- ./mediafiles:/media:ro
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
@@ -30,6 +31,7 @@ services:
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static:ro
|
||||
- ./mediafiles:/media:ro
|
||||
|
||||
@@ -69,7 +69,7 @@ services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
container_name: db_recipes
|
||||
image: postgres:11-alpine
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- ./recipes/db:/var/lib/postgresql/data
|
||||
env_file:
|
||||
|
||||
133
docs/install/truenas_portainer.md
Normal file
133
docs/install/truenas_portainer.md
Normal file
@@ -0,0 +1,133 @@
|
||||
!!! info "Community Contributed"
|
||||
This guide was contributed by the community and is neither officially supported, nor updated or tested.
|
||||
|
||||
This guide is to assist those installing Tandoor Recipes on Truenas Core using Docker and or Portainer
|
||||
|
||||
Docker install instructions adapted from [PhasedLogix IT Services's guide](https://getmethegeek.com/blog/2021-01-07-add-docker-capabilities-to-truenas-core/). Portainer install instructions adopted from the [Portainer Official Documentation](https://docs.portainer.io/start/install-ce/server/docker/linux). Tandoor installation on Portainer provided by users `Szeraax` and `TransatlanticFoe` on Discord (Thank you two!)
|
||||
|
||||
## **Instructions**
|
||||
|
||||
Basic guide to setup Docker and Portainer TrueNAS Core.
|
||||
|
||||
### 1. Login to TrueNAS through your browser
|
||||
- Go to the Virtual Machines Menu
|
||||

|
||||
- Click next to dedicate resources to the VM (see below image of authors setup, you may need to change resources to fit your needs)
|
||||

|
||||
- Hit next to go to disk setup
|
||||
-You want to create a new disk, here are the settings you should use
|
||||
-Disk Type: AHCI
|
||||
-Zvol location: tank/vm (Or wherever you have your VM memory located at)
|
||||
-Size: Atleast 30 gigs
|
||||

|
||||
-Hit next to go to network interface (The defaults are fine but make sure you select the right network adapter)
|
||||
-Hit next to go to installation
|
||||
-Navigate to your ubuntu ISO file (The original author and this author used Ubuntu Server. This OS uses less resources than some other OS's and can be ran Headless with either VNC or SSH access. You can use other OS's, but this guide was written with Ubuntu Server)
|
||||
-Hit next, then submit, you have made the virtual machine!
|
||||
-Open the virtual machine then hit VNC to open ubuntu
|
||||

|
||||
-Once its up choose your language and go through the installer
|
||||
-Once you are done with the setup we want to SSH into the ubuntu VM to setup docker
|
||||
-Open powershell and type SSH "user"@(ip) (replace "user" with the user you setup in the OS installation)
|
||||
-Enter your Password if requested
|
||||
-Close the VNC Console
|
||||
-Go back into the SSH console and get ready to type some commands. Type these commands in order:
|
||||
`sudo apt update`
|
||||
`sudo apt install apt-transport-https ca-certificates curl software-properties-common`
|
||||
`y` (If prompted with a question)
|
||||
`curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -`
|
||||
`sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"`
|
||||
`sudo apt update`
|
||||
`apt-cache policy docker-ce`
|
||||
-To make it so you don’t have to use sudo for every docker command run this command
|
||||
`sudo usermod -aG docker ${USER}`
|
||||
`su - ${USER}`
|
||||
|
||||
### 2. Install Portainer
|
||||
!!! Note: By default, Portainer Server will expose the UI over port 9443 and expose a TCP tunnel server over port 8000. The latter is optional and is only required if you plan to use the Edge compute features with Edge agents.
|
||||
|
||||
-First, create the volume that Portainer Server will use to store its database:
|
||||
`docker volume create portainer_data`
|
||||
-Then, download and install the Portainer Server container:
|
||||
`docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest`
|
||||
-Portainer Server has now been installed. You can check to see whether the Portainer Server container has started by running `docker ps`
|
||||
-Now that the installation is complete, you can log into your Portainer Server instance by opening a web browser and going to:
|
||||
`https://localhost:9443`
|
||||
-Replace `localhost` with the relevant IP address or FQDN if needed, and adjust the port if you changed it earlier.
|
||||
-You will be presented with the initial setup page for Portainer Server.
|
||||
-Create your first user
|
||||
-Your first user will be an administrator. The username defaults to admin but you can change it if you prefer. The password must be at least 12 characters long and meet the listed password requirements.
|
||||
-Connect Portainer to your environments.
|
||||
-Once the admin user has been created, the "Environment Wizard" will automatically launch. The wizard will help get you started with Portainer.
|
||||
-Select "Get Started" to use the Enviroment Portainer is running in
|
||||

|
||||
|
||||
### 3. Install Tandoor Recipies VIA Portainer Web Editor
|
||||
-From the menu select Stacks, click Add stack, give the stack a descriptive name then select Web editor.
|
||||

|
||||
-Use the below code and input it into the Web Editor:
|
||||
|
||||
`version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- stack.env
|
||||
|
||||
web_recipes:
|
||||
# image: vabene1111/recipes:latest
|
||||
image: vabene1111/recipes:beta
|
||||
env_file:
|
||||
- stack.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
- db_recipes
|
||||
|
||||
nginx_recipes:
|
||||
image: nginx:mainline-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- 12008:80
|
||||
env_file:
|
||||
- stack.env
|
||||
depends_on:
|
||||
- web_recipes
|
||||
volumes:
|
||||
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
|
||||
volumes:
|
||||
nginx_config:
|
||||
staticfiles:`
|
||||
|
||||
-Download the .env template from [HERE](https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template) and load this file by pressing the "Load Variables from .env File" button:
|
||||

|
||||
|
||||
-You will need to change the following variables:
|
||||
-`SECRET_KEY` needs to be replaced with a new key. This can be generated from websites like [Djecrety](https://djecrety.ir/)
|
||||
-`TIMEZONE` needs to be replaced with the appropriate code for your timezone. Accepted values can be found at [TimezoneDB](https://timezonedb.com/time-zones)
|
||||
-`POSTGRES_USER` and `POSTGRES_PASSWORD` needs to be replaced with your username and password from [PostgreSQL](https://www.postgresql.org/) !!!NOTE Do not sign in using social media. You need to sign up using Email and Password.
|
||||
-After those veriables are changed, you may press the `Deploy the Stack` button at the bottom of the page. This will create the needed containers to run Tandoor Recipes.
|
||||
|
||||
### 4. Login and Setup your new server!
|
||||
- You need to access your Tandoor Server through its Webpage: `https://localhost:xxxx` replacing `localhost` with the IP of the VM running Docker and `xxxx` with the port you chose in the Web Editor for `nginx_recipes` above. In this case, `12008`.
|
||||
!!! While the containers are starting and doing whatever they need to do, you might still get HTTP errors e.g. 500 or 502. Just be patient and try again in a moment
|
||||
-You will now need to set up the Tandoor Server through the WebGUI.
|
||||
@@ -33,6 +33,7 @@ nav:
|
||||
- Synology: install/synology.md
|
||||
- Kubernetes: install/kubernetes.md
|
||||
- KubeSail or PiBox: install/kubesail.md
|
||||
- TrueNAS Portainer: install/truenas_portainer.md
|
||||
- WSL: install/wsl.md
|
||||
- Manual: install/manual.md
|
||||
- Other setups: install/other.md
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-04-26 07:46+0200\n"
|
||||
"POT-Creation-Date: 2023-08-29 13:09+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -18,66 +18,120 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:436
|
||||
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:58
|
||||
msgid ""
|
||||
"You do not have the required permissions to view this page! You need to have "
|
||||
"the following modules licensed: "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:76
|
||||
msgid "You are not logged in and therefore cannot view this page!"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:79
|
||||
msgid "You do not have the required modules to view this page!"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:90
|
||||
msgid "You do not have the required module to view this page!"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:32
|
||||
msgid "Recipe"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:32
|
||||
msgid "Recipe Keyword"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:32
|
||||
msgid "Meal Plan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:32
|
||||
msgid "Shopping"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:32
|
||||
msgid "Book"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:37
|
||||
msgid "start"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:37
|
||||
msgid "center"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\plugins\enterprise_plugin\models.py:37
|
||||
msgid "end"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:455
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:437
|
||||
#: .\recipes\settings.py:456
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:438
|
||||
#: .\recipes\settings.py:457
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:439
|
||||
#: .\recipes\settings.py:458
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:440
|
||||
#: .\recipes\settings.py:459
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:441
|
||||
#: .\recipes\settings.py:460
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:442
|
||||
#: .\recipes\settings.py:461
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:443
|
||||
#: .\recipes\settings.py:462
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:444
|
||||
#: .\recipes\settings.py:463
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:445
|
||||
#: .\recipes\settings.py:464
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:446
|
||||
#: .\recipes\settings.py:465
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:447
|
||||
#: .\recipes\settings.py:466
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:448
|
||||
#: .\recipes\settings.py:467
|
||||
msgid "Norwegian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:468
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:449
|
||||
#: .\recipes\settings.py:469
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:450
|
||||
#: .\recipes\settings.py:470
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:451
|
||||
#: .\recipes\settings.py:471
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -46,7 +46,11 @@ INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
|
||||
# allow djangos wsgi server to server mediafiles
|
||||
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True)))
|
||||
|
||||
REVERSE_PROXY_AUTH = bool(int(os.getenv('REVERSE_PROXY_AUTH', False)))
|
||||
if os.getenv('REVERSE_PROXY_AUTH') is not None:
|
||||
print('DEPRECATION WARNING: Environment var "REVERSE_PROXY_AUTH" is deprecated. Please use "REMOTE_USER_AUTH".')
|
||||
REMOTE_USER_AUTH = bool(int(os.getenv('REVERSE_PROXY_AUTH', False)))
|
||||
else:
|
||||
REMOTE_USER_AUTH = bool(int(os.getenv('REMOTE_USER_AUTH', False)))
|
||||
|
||||
# default value for user preference 'comment'
|
||||
COMMENT_PREF_DEFAULT = bool(int(os.getenv('COMMENT_PREF_DEFAULT', True)))
|
||||
@@ -128,34 +132,43 @@ INSTALLED_APPS = [
|
||||
'treebeard',
|
||||
]
|
||||
|
||||
PLUGINS_DIRECTORY = os.path.join(BASE_DIR, 'recipes', 'plugins')
|
||||
PLUGINS = []
|
||||
try:
|
||||
for d in os.listdir(os.path.join(BASE_DIR, 'recipes', 'plugins')):
|
||||
if d != '__pycache__':
|
||||
try:
|
||||
apps_path = f'recipes.plugins.{d}.apps'
|
||||
__import__(apps_path)
|
||||
app_config_classname = dir(sys.modules[apps_path])[1]
|
||||
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
|
||||
if plugin_module not in INSTALLED_APPS:
|
||||
INSTALLED_APPS.append(plugin_module)
|
||||
plugin_class = getattr(
|
||||
sys.modules[apps_path], app_config_classname)
|
||||
plugin_config = {
|
||||
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
|
||||
'module': f'recipes.plugins.{d}',
|
||||
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
|
||||
'base_url': plugin_class.base_url,
|
||||
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
|
||||
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
|
||||
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
|
||||
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
|
||||
}
|
||||
PLUGINS.append(plugin_config)
|
||||
except Exception:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
print(f'ERROR failed to initialize plugin {d}')
|
||||
if os.path.isdir(PLUGINS_DIRECTORY):
|
||||
for d in os.listdir(PLUGINS_DIRECTORY):
|
||||
if d != '__pycache__':
|
||||
try:
|
||||
apps_path = f'recipes.plugins.{d}.apps'
|
||||
__import__(apps_path)
|
||||
app_config_classname = dir(sys.modules[apps_path])[1]
|
||||
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
|
||||
plugin_class = getattr(sys.modules[apps_path], app_config_classname)
|
||||
plugin_disabled = False
|
||||
if hasattr(plugin_class, 'disabled'):
|
||||
plugin_disabled = plugin_class.disabled
|
||||
if plugin_module not in INSTALLED_APPS and not plugin_disabled:
|
||||
INSTALLED_APPS.append(plugin_module)
|
||||
|
||||
plugin_config = {
|
||||
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
|
||||
'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown',
|
||||
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
|
||||
'github': plugin_class.github if hasattr(plugin_class, 'github') else '',
|
||||
'module': f'recipes.plugins.{d}',
|
||||
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
|
||||
'base_url': plugin_class.base_url,
|
||||
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
|
||||
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
|
||||
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
|
||||
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
|
||||
}
|
||||
PLUGINS.append(plugin_config)
|
||||
print(f'PLUGIN {d} loaded')
|
||||
except Exception:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
print(f'ERROR failed to initialize plugin {d}')
|
||||
except Exception:
|
||||
if DEBUG:
|
||||
print('ERROR failed to initialize plugins')
|
||||
@@ -264,7 +277,7 @@ SITE_ID = int(os.getenv('ALLAUTH_SITE_ID', 1))
|
||||
|
||||
ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
|
||||
|
||||
if REVERSE_PROXY_AUTH:
|
||||
if REMOTE_USER_AUTH:
|
||||
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
|
||||
AUTHENTICATION_BACKENDS.append(
|
||||
'django.contrib.auth.backends.RemoteUserBackend')
|
||||
@@ -522,4 +535,4 @@ DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv(
|
||||
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||
|
||||
mimetypes.add_type("text/javascript", ".js", True)
|
||||
mimetypes.add_type("text/javascript", ".js", True)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
VERSION_NUMBER = "0.0.0"
|
||||
BUILD_REF = ""
|
||||
@@ -1,18 +1,17 @@
|
||||
Django==4.1.9
|
||||
cryptography==41.0.0
|
||||
Django==4.1.10
|
||||
cryptography===41.0.3
|
||||
django-annoying==0.10.6
|
||||
django-autocomplete-light==3.9.4
|
||||
django-cleanup==7.0.0
|
||||
django-cleanup==8.0.0
|
||||
django-crispy-forms==1.14.0
|
||||
django-tables2==2.5.3
|
||||
djangorestframework==3.14.0
|
||||
drf-writable-nested==0.7.0
|
||||
django-oauth-toolkit==2.2.0
|
||||
django-debug-toolbar==3.8.1
|
||||
bleach==5.0.1
|
||||
bleach-allowlist==1.0.3
|
||||
bleach==6.0.0
|
||||
gunicorn==20.1.0
|
||||
lxml==4.9.2
|
||||
lxml==4.9.3
|
||||
Markdown==3.4.3
|
||||
Pillow==9.4.0
|
||||
psycopg2-binary==2.9.5
|
||||
@@ -20,9 +19,9 @@ python-dotenv==0.21.0
|
||||
requests==2.31.0
|
||||
six==1.16.0
|
||||
webdavclient3==3.14.6
|
||||
whitenoise==6.2.0
|
||||
whitenoise==6.5.0
|
||||
icalendar==5.0.4
|
||||
pyyaml==6.0
|
||||
pyyaml==6.0.1
|
||||
uritemplate==4.1.1
|
||||
beautifulsoup4==4.11.1
|
||||
microdata==0.8.0
|
||||
@@ -41,8 +40,8 @@ boto3==1.26.41
|
||||
django-prometheus==2.2.0
|
||||
django-hCaptcha==0.2.0
|
||||
python-ldap==3.4.3
|
||||
django-auth-ldap==4.2.0
|
||||
django-auth-ldap==4.4.0
|
||||
pytest-factoryboy==2.5.1
|
||||
pyppeteer==1.0.2
|
||||
validators==0.20.0
|
||||
pytube==12.1.0
|
||||
pytube==15.0.0
|
||||
|
||||
74
version.py
Normal file
74
version.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PLUGINS_DIRECTORY = os.path.join(BASE_DIR, 'recipes', 'plugins')
|
||||
|
||||
version_info = []
|
||||
tandoor_tag = ''
|
||||
tandoor_hash = ''
|
||||
try:
|
||||
print('getting tandoor version')
|
||||
r = subprocess.check_output(['git', 'show', '-s'], cwd=BASE_DIR).decode()
|
||||
tandoor_branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=BASE_DIR).decode()
|
||||
tandoor_hash = r.split('\n')[0].split(' ')[1]
|
||||
try:
|
||||
tandoor_tag = subprocess.check_output(['git', 'describe', '--exact-match', tandoor_hash], cwd=BASE_DIR).decode().replace('\n', '')
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
version_info.append({
|
||||
'name': 'Tandoor ',
|
||||
'version': re.sub(r'<.*>', '', r),
|
||||
'website': 'https://github.com/TandoorRecipes/recipes',
|
||||
'commit_link': 'https://github.com/TandoorRecipes/recipes/commit/' + r.split('\n')[0].split(' ')[1],
|
||||
'ref': tandoor_hash,
|
||||
'branch': tandoor_branch,
|
||||
'tag': tandoor_tag
|
||||
})
|
||||
|
||||
if os.path.isdir(PLUGINS_DIRECTORY):
|
||||
for d in os.listdir(PLUGINS_DIRECTORY):
|
||||
if d != '__pycache__':
|
||||
try:
|
||||
apps_path = f'recipes.plugins.{d}.apps'
|
||||
__import__(apps_path)
|
||||
app_config_classname = dir(sys.modules[apps_path])[1]
|
||||
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
|
||||
plugin_class = getattr(sys.modules[apps_path], app_config_classname)
|
||||
|
||||
print('getting plugin version for ', plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name)
|
||||
r = subprocess.check_output(['git', 'show', '-s'], cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode()
|
||||
branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode()
|
||||
commit_hash = r.split('\n')[0].split(' ')[1]
|
||||
try:
|
||||
print('running describe')
|
||||
tag = subprocess.check_output(['git', 'describe', '--exact-match', commit_hash], cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode().replace('\n', '')
|
||||
except:
|
||||
tag = ''
|
||||
|
||||
version_info.append({
|
||||
'name': 'Plugin: ' + plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
|
||||
'version': re.sub(r'<.*>', '', r),
|
||||
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
|
||||
'commit_link': plugin_class.github if hasattr(plugin_class, 'github') else '' + '/commit/' + commit_hash,
|
||||
'ref': commit_hash,
|
||||
'branch': branch,
|
||||
'tag': tag
|
||||
})
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
with open('cookbook/version_info.py', 'w+', encoding='UTF-8') as f:
|
||||
print(f"writing version info {version_info}")
|
||||
f.write(f'TANDOOR_VERSION = "{tandoor_tag}"\nTANDOOR_REF = "{tandoor_hash}"\nVERSION_INFO = {version_info}')
|
||||
@@ -59,9 +59,9 @@
|
||||
"@vue/compiler-sfc": "^3.2.45",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"typescript": "~4.9.3",
|
||||
"typescript": "~5.1.6",
|
||||
"vue-cli-plugin-i18n": "^2.3.2",
|
||||
"webpack-bundle-tracker": "1.8.1",
|
||||
"workbox-background-sync": "^6.5.4",
|
||||
|
||||
@@ -511,6 +511,10 @@ export default {
|
||||
this.website_url = urlParams.get('url')
|
||||
this.loadRecipe(this.website_url)
|
||||
}
|
||||
if (urlParams.has("text")) {
|
||||
this.website_url = urlParams.get('text')
|
||||
this.loadRecipe(this.website_url)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
@@ -675,12 +679,26 @@ export default {
|
||||
*/
|
||||
autoImport: function () {
|
||||
this.collapse_visible.import = true
|
||||
this.website_url_list.split('\n').forEach(r => {
|
||||
this.loadRecipe(r, true, undefined).then((recipe_json) => {
|
||||
let url_list = this.website_url_list.split('\n').filter(x => x.trim() !== '')
|
||||
if (url_list.length > 0) {
|
||||
let url = url_list.shift()
|
||||
this.website_url_list = url_list.join('\n')
|
||||
|
||||
|
||||
this.loadRecipe(url, true, undefined).then((recipe_json) => {
|
||||
this.importRecipe('multi_import', recipe_json, true)
|
||||
setTimeout(() => {
|
||||
this.autoImport()
|
||||
}, 2000)
|
||||
}).catch((err) => {
|
||||
|
||||
})
|
||||
})
|
||||
this.website_url_list = ''
|
||||
} else {
|
||||
this.import_loading = false
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
/**
|
||||
* Import recipes with uploaded files and app integration
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
|
||||
import draggable from "vuedraggable";
|
||||
import stringSimilarity from "string-similarity"
|
||||
import {getUserPreference} from "@/utils/utils"
|
||||
|
||||
export default {
|
||||
name: "ImportViewStepEditor",
|
||||
@@ -117,6 +118,7 @@ export default {
|
||||
recipe_json: undefined,
|
||||
current_edit_ingredient: null,
|
||||
current_edit_step: null,
|
||||
user_preferences: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -126,6 +128,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.recipe_json = this.recipe
|
||||
this.user_preferences = getUserPreference();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
@@ -138,7 +141,7 @@ export default {
|
||||
let steps = []
|
||||
step.instruction.split(split_character).forEach(part => {
|
||||
if (part.trim() !== '') {
|
||||
steps.push({'instruction': part, 'ingredients': []})
|
||||
steps.push({'instruction': part, 'ingredients': [], 'show_ingredients_table': this.user_preferences.show_step_ingredients})
|
||||
}
|
||||
})
|
||||
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
|
||||
</b-list-group-item>
|
||||
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id" >
|
||||
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id">
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<div>
|
||||
<b-img style="height: 50px; width: 50px; object-fit: cover"
|
||||
@@ -279,12 +279,19 @@
|
||||
:create_date="mealplan_default_date"
|
||||
@reload-meal-types="refreshMealTypes"
|
||||
></meal-plan-edit-modal>
|
||||
<auto-meal-plan-modal
|
||||
:modal_title="'Auto create meal plan'"
|
||||
:current_period="current_period"
|
||||
></auto-meal-plan-modal>
|
||||
|
||||
<div class="row d-none d-lg-block">
|
||||
<div class="col-12 float-right">
|
||||
<button class="btn btn-success shadow-none" @click="createEntryClick(new Date())"><i
|
||||
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
|
||||
</button>
|
||||
<button class="btn btn-primary shadow-none" @click="createAutoPlan(new Date())"><i
|
||||
class="fas fa-calendar-plus"></i> {{ $t("Auto_Planner") }}
|
||||
</button>
|
||||
<a class="btn btn-primary shadow-none" :href="iCalUrl"><i class="fas fa-download"></i>
|
||||
{{ $t("Export_To_ICal") }}
|
||||
</a>
|
||||
@@ -293,10 +300,11 @@
|
||||
|
||||
<bottom-navigation-bar :create_links="[{label:$t('Export_To_ICal'), url: iCalUrl, icon:'fas fa-download'}]">
|
||||
<template #custom_create_functions>
|
||||
<h6 class="dropdown-header">{{ $t('Meal_Plan')}}</h6>
|
||||
<h6 class="dropdown-header">{{ $t('Meal_Plan') }}</h6>
|
||||
<a class="dropdown-item" @click="createEntryClick(new Date())"><i
|
||||
class="fas fa-calendar-plus fa-fw"></i> {{ $t("Create") }}</a>
|
||||
</template>
|
||||
|
||||
</bottom-navigation-bar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -322,6 +330,8 @@ import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/component
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore";
|
||||
import axios from "axios";
|
||||
import AutoMealPlanModal from "@/components/AutoMealPlanModal";
|
||||
|
||||
const {makeToast} = require("@/utils/utils")
|
||||
|
||||
@@ -334,6 +344,7 @@ let SETTINGS_COOKIE_NAME = "mealplan_settings"
|
||||
export default {
|
||||
name: "MealPlanView",
|
||||
components: {
|
||||
AutoMealPlanModal,
|
||||
MealPlanEditModal,
|
||||
MealPlanCard,
|
||||
CalendarView,
|
||||
@@ -347,6 +358,16 @@ export default {
|
||||
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
|
||||
data: function () {
|
||||
return {
|
||||
AutoPlan: {
|
||||
meal_types: [],
|
||||
keywords: [[]],
|
||||
servings: 1,
|
||||
date: Date.now(),
|
||||
startDay: null,
|
||||
endDay: null,
|
||||
shared: [],
|
||||
addshopping: false
|
||||
},
|
||||
showDate: new Date(),
|
||||
plan_entries: [],
|
||||
recipe_viewed: {},
|
||||
@@ -656,7 +677,11 @@ export default {
|
||||
this.$bvModal.show(`id_meal_plan_edit_modal`)
|
||||
})
|
||||
|
||||
}
|
||||
},
|
||||
createAutoPlan() {
|
||||
this.$bvModal.show(`autoplan-modal`)
|
||||
},
|
||||
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user