mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-26 03:43:34 -05:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7046bc705 | ||
|
|
52946a8e4c | ||
|
|
dd6b77e029 | ||
|
|
396c1f3d5f | ||
|
|
379d5a5177 | ||
|
|
85a4d5d432 | ||
|
|
43eb10e488 | ||
|
|
d702c08a12 | ||
|
|
e78323d214 | ||
|
|
d2e866dd74 | ||
|
|
76687ad5df | ||
|
|
dab77e8e4f | ||
|
|
0b250c71aa | ||
|
|
571f670db0 | ||
|
|
4e9e628162 | ||
|
|
4f49b06704 | ||
|
|
8eb0c36665 | ||
|
|
6f69c09aca | ||
|
|
8e6f153882 | ||
|
|
07183fd40f | ||
|
|
04b7f0a398 | ||
|
|
1735fda48f | ||
|
|
1c9ea0eda7 | ||
|
|
83b5b6695c | ||
|
|
342fb3c96d | ||
|
|
b7a18466b5 | ||
|
|
0cdc4d51df | ||
|
|
e177669514 | ||
|
|
fd294dfcdd |
@@ -1,18 +1,18 @@
|
||||
name: Deploy Docker Image
|
||||
|
||||
name: publish dev image docker
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!master'
|
||||
jobs:
|
||||
build-push:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: AhnSeongHyun/action-tag-docker-build-push@v1.0.0
|
||||
uses: elgohr/Publish-Docker-Github-Action@2.13
|
||||
with:
|
||||
repo_name: vabene1111/recipes
|
||||
name: vabene1111/recipes
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
19
.github/workflows/docker-publish-latest.yml
vendored
Normal file
19
.github/workflows/docker-publish-latest.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: publish latest image docker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
tag: latest
|
||||
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
25
.github/workflows/docker-publish-release.yml
vendored
Normal file
25
.github/workflows/docker-publish-release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: publish tagged release docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build image job
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@master#
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
tag: ${{ steps.get_version.outputs.VERSION }}
|
||||
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
13
.github/workflows/docker-publish.yml
vendored
13
.github/workflows/docker-publish.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Publish Docker
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@2.13
|
||||
with:
|
||||
name: vabene1111/recipes
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
38
README.md
38
README.md
@@ -3,6 +3,8 @@ Recipes is a Django application to manage, tag and search recipes using either b
|
||||
|
||||

|
||||
|
||||
[More Screenshots](https://imgur.com/a/V01151p)
|
||||
|
||||
### Features
|
||||
|
||||
- :package: **Sync** files with Dropbox and Nextcloud (more can easily be added)
|
||||
@@ -12,20 +14,22 @@ Recipes is a Django application to manage, tag and search recipes using either b
|
||||
- :iphone: Optimized for use on **mobile** devices like phones and tablets
|
||||
- :shopping_cart: Generate **shopping** lists from recipes
|
||||
- :calendar: Create a **Plan** on what to eat when
|
||||
- :person_with_blond_hair: **Share** recipes with friends and comment on them to suggest or remember changes you made
|
||||
- :family: **Share** recipes with friends and comment on them to suggest or remember changes you made
|
||||
- :whale: Easy setup with **Docker**
|
||||
- :art: Customize your interface with **themes**
|
||||
- :envelope: Export and import recipes from other users
|
||||
- :heavy_plus_sign: Many more like recipe scaling, image compression, cookbooks, printing views, ...
|
||||
|
||||
This application is meant for people with a collection of recipes they want to share with family and friends or simply store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as a public page.
|
||||
|
||||
This application is meant for people with a collection of recipes they want to share with family and friends or simply
|
||||
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as a public page.
|
||||
Some Documentation can be found [here](https://github.com/vabene1111/recipes/wiki)
|
||||
# Installation
|
||||
|
||||
The docker image (`vabene1111/recipes`) simply exposes the application on port `8080`. You may choose any preferred installation method, the following are just examples to make it easier.
|
||||
|
||||
### Docker-Compose
|
||||
|
||||
2. Choose one of the included configurations [here](https://github.com/vabene1111/recipes/tree/develop/docs/docker).
|
||||
2. Choose one of the included configurations [here](docs/docker).
|
||||
2. Download the environment (config) file template and fill it out `wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env `
|
||||
3. Start the container `docker-compose up -d`
|
||||
4. Create a default user by running `docker-compose exec web_recipes createsuperuser`.
|
||||
@@ -38,7 +42,6 @@ Otherwise simply follow the instructions for any django based deployment
|
||||
(for example [this one](http://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html)).
|
||||
|
||||
## Updating
|
||||
|
||||
While intermediate updates can be skipped when updating please make sure to **read the release notes** in case some special action is required to update.
|
||||
|
||||
0. Before updating it is recommended to **create a backup!**
|
||||
@@ -46,30 +49,9 @@ While intermediate updates can be skipped when updating please make sure to **re
|
||||
2. Pull the latest image using `docker-compose pull`
|
||||
3. Start the container again using `docker-compose up -d`
|
||||
|
||||
# Documentation
|
||||
## Kubernetes
|
||||
|
||||
Most things should be straight forward but there are some more complicated things.
|
||||
##### Storage Backends
|
||||
A `Storage Backend` is a remote storage location where PDF files are read from. To add a new backend click on `Storage Data` and then on `Storage Backends`. There click the plus button.
|
||||
|
||||
Enter a name (just a display name for you to identify it) and an API access Token for the account you want to use.
|
||||
Dropboxes API tokens can be found on the [Dropboxes API explorer](https://dropbox.github.io/dropbox-api-v2-explorer/#auth_token/from_oauth1)
|
||||
with the button on the top right. For Nextcloud you can use a App apssword created in the settings.
|
||||
|
||||
##### Adding Synced Paths
|
||||
To add a new path from your Storage backend to the sync list, go to `Storage Data >> Configure Sync` and select the storage backend you want to use.
|
||||
Then enter the path you want to monitor starting at the storage root (e.g. `/Folder/RecipesFolder`) and save it.
|
||||
|
||||
##### Syncing Data
|
||||
To sync the recipes app with the storage backends press `Sync now` under `Storage Data >> Configure Sync`.
|
||||
##### Import Recipes
|
||||
All files found by the sync can be found under `Manage Data >> Import recipes`. There you can either import all at once without modifying them or import one by one, adding tags while importing.
|
||||
##### Batch Edit
|
||||
If you have many untagged recipes, you may want to edit them all at once. To do so, go to
|
||||
`Storage Data >> Batch Edit`. Enter a word which should be contained in the recipe name and select the tags you want to apply.
|
||||
When clicking submit, every recipe containing the word will be updated (tags are added).
|
||||
|
||||
> Currently the only option is word contains, maybe some more SQL like operators will be added later.
|
||||
You can find a basic kubernetes setup [here](docs/k8s/). Please see the README in the folder for more detail.
|
||||
|
||||
## Contributing
|
||||
Pull Requests and ideas are welcome, feel free to contribute in any way.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from dal import autocomplete
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -31,10 +30,11 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = ('theme', 'nav_color')
|
||||
fields = ('default_unit', 'theme', 'nav_color', 'default_page')
|
||||
|
||||
help_texts = {
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!')
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.')
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,25 @@ class ShoppingForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class ExportForm(forms.Form):
|
||||
recipe = forms.ModelChoiceField(
|
||||
queryset=Recipe.objects.filter(internal=True).all(),
|
||||
widget=SelectWidget
|
||||
)
|
||||
image = forms.BooleanField(
|
||||
help_text=_('Export Base64 encoded image?'),
|
||||
required=False
|
||||
)
|
||||
download = forms.BooleanField(
|
||||
help_text=_('Download export directly or show on page?'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
recipe = forms.CharField(widget=forms.Textarea, help_text=_('Simply paste a JSON export into this textarea and click import.'))
|
||||
|
||||
|
||||
class UnitMergeForm(forms.Form):
|
||||
prefix = 'unit'
|
||||
|
||||
|
||||
81
cookbook/helper/mdx_urlize.py
Normal file
81
cookbook/helper/mdx_urlize.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""A more liberal autolinker
|
||||
|
||||
Inspired by Django's urlize function.
|
||||
|
||||
Positive examples:
|
||||
|
||||
>>> import markdown
|
||||
>>> md = markdown.Markdown(extensions=['urlize'])
|
||||
|
||||
>>> md.convert('http://example.com/')
|
||||
u'<p><a href="http://example.com/">http://example.com/</a></p>'
|
||||
|
||||
>>> md.convert('go to http://example.com')
|
||||
u'<p>go to <a href="http://example.com">http://example.com</a></p>'
|
||||
|
||||
>>> md.convert('example.com')
|
||||
u'<p><a href="http://example.com">example.com</a></p>'
|
||||
|
||||
>>> md.convert('example.net')
|
||||
u'<p><a href="http://example.net">example.net</a></p>'
|
||||
|
||||
>>> md.convert('www.example.us')
|
||||
u'<p><a href="http://www.example.us">www.example.us</a></p>'
|
||||
|
||||
>>> md.convert('(www.example.us/path/?name=val)')
|
||||
u'<p>(<a href="http://www.example.us/path/?name=val">www.example.us/path/?name=val</a>)</p>'
|
||||
|
||||
>>> md.convert('go to <http://example.com> now!')
|
||||
u'<p>go to <a href="http://example.com">http://example.com</a> now!</p>'
|
||||
|
||||
Negative examples:
|
||||
|
||||
>>> md.convert('del.icio.us')
|
||||
u'<p>del.icio.us</p>'
|
||||
|
||||
"""
|
||||
|
||||
import markdown
|
||||
|
||||
# Global Vars
|
||||
URLIZE_RE = '(%s)' % '|'.join([
|
||||
r'<(?:f|ht)tps?://[^>]*>',
|
||||
r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
|
||||
r'\bwww\.[^)<>\s]+[^.,)<>\s]',
|
||||
r'[^(<\s]+\.(?:com|net|org)\b',
|
||||
])
|
||||
|
||||
class UrlizePattern(markdown.inlinepatterns.Pattern):
|
||||
""" Return a link Element given an autolink (`http://example/com`). """
|
||||
def handleMatch(self, m):
|
||||
url = m.group(2)
|
||||
|
||||
if url.startswith('<'):
|
||||
url = url[1:-1]
|
||||
|
||||
text = url
|
||||
|
||||
if not url.split('://')[0] in ('http','https','ftp'):
|
||||
if '@' in url and not '/' in url:
|
||||
url = 'mailto:' + url
|
||||
else:
|
||||
url = 'http://' + url
|
||||
|
||||
el = markdown.util.etree.Element("a")
|
||||
el.set('href', url)
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
return el
|
||||
|
||||
class UrlizeExtension(markdown.Extension):
|
||||
""" Urlize Extension for Python-Markdown. """
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
""" Replace autolink with UrlizePattern """
|
||||
md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
|
||||
|
||||
def makeExtension(*args, **kwargs):
|
||||
return UrlizeExtension(*args, **kwargs)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
Binary file not shown.
@@ -7,25 +7,25 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-03-18 12:13+0100\n"
|
||||
"PO-Revision-Date: 2020-03-18 12:19+0100\n"
|
||||
"POT-Creation-Date: 2020-04-25 23:31+0200\n"
|
||||
"PO-Revision-Date: 2020-04-25 23:31+0200\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
|
||||
#: .\cookbook\filters.py:15 .\cookbook\templates\base.html:98
|
||||
#: .\cookbook\filters.py:15 .\cookbook\templates\base.html:99
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:28
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\recipe_view.html:104 .\cookbook\views\lists.py:45
|
||||
#: .\cookbook\templates\recipe_view.html:110 .\cookbook\views\lists.py:45
|
||||
msgid "Ingredients"
|
||||
msgstr "Zutaten"
|
||||
|
||||
#: .\cookbook\forms.py:35
|
||||
#: .\cookbook\forms.py:36
|
||||
msgid ""
|
||||
"Color of the top navigation bar. Not all colors work with all themes, just "
|
||||
"try them out!"
|
||||
@@ -33,36 +33,48 @@ msgstr ""
|
||||
"Farbe der oberen Navigationsleiste. Nicht alle Farben passen, daher einfach "
|
||||
"mal ausprobieren!"
|
||||
|
||||
#: .\cookbook\forms.py:49 .\cookbook\forms.py:67 .\cookbook\forms.py:196
|
||||
#: .\cookbook\forms.py:37
|
||||
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
|
||||
msgstr "Standard Einheit für neue Zutaten."
|
||||
|
||||
#: .\cookbook\forms.py:49
|
||||
msgid ""
|
||||
"Both fields are optional. If none are given the username will be displayed "
|
||||
"instead"
|
||||
msgstr ""
|
||||
"Beide Felder sind optional, wenn keins von beiden gegeben ist wird der "
|
||||
"Nutzername angezeigt"
|
||||
|
||||
#: .\cookbook\forms.py:63 .\cookbook\forms.py:81 .\cookbook\forms.py:229
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: .\cookbook\forms.py:50 .\cookbook\forms.py:68 .\cookbook\forms.py:197
|
||||
#: .\cookbook\forms.py:64 .\cookbook\forms.py:82 .\cookbook\forms.py:230
|
||||
#: .\cookbook\templates\stats.html:22
|
||||
msgid "Keywords"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
#: .\cookbook\forms.py:51 .\cookbook\forms.py:70
|
||||
#: .\cookbook\forms.py:65 .\cookbook\forms.py:84
|
||||
msgid "Preparation time in minutes"
|
||||
msgstr "Zubereitungszeit in Minuten"
|
||||
|
||||
#: .\cookbook\forms.py:52 .\cookbook\forms.py:71
|
||||
#: .\cookbook\forms.py:66 .\cookbook\forms.py:85
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr "Wartezeit (kochen/backen) in Minuten"
|
||||
|
||||
#: .\cookbook\forms.py:53 .\cookbook\forms.py:198
|
||||
#: .\cookbook\forms.py:67 .\cookbook\forms.py:231
|
||||
msgid "Path"
|
||||
msgstr "Pfad"
|
||||
|
||||
#: .\cookbook\forms.py:54
|
||||
#: .\cookbook\forms.py:68
|
||||
msgid "Storage UID"
|
||||
msgstr "Speicher ID"
|
||||
|
||||
#: .\cookbook\forms.py:69
|
||||
#: .\cookbook\forms.py:83
|
||||
msgid "Instructions"
|
||||
msgstr "Anleitung"
|
||||
|
||||
#: .\cookbook\forms.py:82
|
||||
#: .\cookbook\forms.py:96
|
||||
msgid ""
|
||||
"Include <code>- [ ]</code> in list for easier usage in markdown based "
|
||||
"documents."
|
||||
@@ -70,51 +82,63 @@ msgstr ""
|
||||
"Füge <code>- [ ]</code> vor den Zutaten ein um sie besser in einem Markdown "
|
||||
"Dokument zu verwenden."
|
||||
|
||||
#: .\cookbook\forms.py:94
|
||||
#: .\cookbook\forms.py:108
|
||||
msgid "Export Base64 encoded image?"
|
||||
msgstr "Base64 kodiertes Bild exportieren ?"
|
||||
|
||||
#: .\cookbook\forms.py:112
|
||||
msgid "Download export directly or show on page?"
|
||||
msgstr "Direkter Download oder anzeige auf Seite ?"
|
||||
|
||||
#: .\cookbook\forms.py:118
|
||||
msgid "Simply paste a JSON export into this textarea and click import."
|
||||
msgstr "Einfach JSON in die Textbox einfügen und importieren klicken."
|
||||
|
||||
#: .\cookbook\forms.py:127
|
||||
msgid "New Unit"
|
||||
msgstr "Neue Einheit"
|
||||
|
||||
#: .\cookbook\forms.py:95
|
||||
#: .\cookbook\forms.py:128
|
||||
msgid "New unit that other gets replaced by."
|
||||
msgstr "Neue Einheit die die alte ersetzt."
|
||||
|
||||
#: .\cookbook\forms.py:100
|
||||
#: .\cookbook\forms.py:133
|
||||
msgid "Old Unit"
|
||||
msgstr "Alte Einheit"
|
||||
|
||||
#: .\cookbook\forms.py:101
|
||||
#: .\cookbook\forms.py:134
|
||||
msgid "Unit that should be replaced."
|
||||
msgstr "Einheit die ersetzt werden soll."
|
||||
|
||||
#: .\cookbook\forms.py:111
|
||||
#: .\cookbook\forms.py:144
|
||||
msgid "New Ingredient"
|
||||
msgstr "Neue Zutat"
|
||||
|
||||
#: .\cookbook\forms.py:112
|
||||
#: .\cookbook\forms.py:145
|
||||
msgid "New ingredient that other gets replaced by."
|
||||
msgstr "Neue Zutat die die alte ersetzt."
|
||||
|
||||
#: .\cookbook\forms.py:117
|
||||
#: .\cookbook\forms.py:150
|
||||
msgid "Old Ingredient"
|
||||
msgstr "Alte Zutat"
|
||||
|
||||
#: .\cookbook\forms.py:118
|
||||
#: .\cookbook\forms.py:151
|
||||
msgid "Ingredient that should be replaced."
|
||||
msgstr "Zutat die ersetzt werden soll."
|
||||
|
||||
#: .\cookbook\forms.py:130
|
||||
#: .\cookbook\forms.py:163
|
||||
msgid "Add your comment: "
|
||||
msgstr "Schreibe einen Kommentar:"
|
||||
msgstr "Schreibe einen Kommentar: "
|
||||
|
||||
#: .\cookbook\forms.py:155
|
||||
#: .\cookbook\forms.py:188
|
||||
msgid "Leave empty for dropbox and enter app password for nextcloud."
|
||||
msgstr "Für Dropbox leer lassen, bei Nextcloud App-Passwort eingeben."
|
||||
|
||||
#: .\cookbook\forms.py:158
|
||||
#: .\cookbook\forms.py:191
|
||||
msgid "Leave empty for nextcloud and enter api token for dropbox."
|
||||
msgstr "Bei Nextcloud leer lassen, bei Dropbox API Token eingeben."
|
||||
|
||||
#: .\cookbook\forms.py:166
|
||||
#: .\cookbook\forms.py:199
|
||||
msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
@@ -122,119 +146,128 @@ msgstr ""
|
||||
"Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote.php/"
|
||||
"webdav/</code> wird automatisch hinzugefügt)"
|
||||
|
||||
#: .\cookbook\forms.py:185
|
||||
#: .\cookbook\forms.py:218
|
||||
msgid "Search String"
|
||||
msgstr "Such Wort"
|
||||
|
||||
#: .\cookbook\forms.py:199
|
||||
#: .\cookbook\forms.py:232
|
||||
msgid "File ID"
|
||||
msgstr "Datei ID"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
#: .\cookbook\models.py:49
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
#: .\cookbook\models.py:49 .\cookbook\templates\base.html:93
|
||||
#: .\cookbook\templates\meal_plan.html:4 .\cookbook\templates\meal_plan.html:32
|
||||
#: .\cookbook\views\delete.py:136 .\cookbook\views\edit.py:286
|
||||
#: .\cookbook\views\new.py:138
|
||||
msgid "Meal-Plan"
|
||||
msgstr "Plan"
|
||||
|
||||
#: .\cookbook\models.py:49 .\cookbook\templates\base.html:90
|
||||
msgid "Books"
|
||||
msgstr "Bücher"
|
||||
|
||||
#: .\cookbook\models.py:210
|
||||
msgid "Breakfast"
|
||||
msgstr "Frühstück"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
#: .\cookbook\models.py:210
|
||||
msgid "Lunch"
|
||||
msgstr "Mittagessen"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
#: .\cookbook\models.py:210
|
||||
msgid "Dinner"
|
||||
msgstr "Abendessen"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
#: .\cookbook\models.py:210
|
||||
msgid "Other"
|
||||
msgstr "Andere"
|
||||
|
||||
#: .\cookbook\tables.py:83
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:49
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:160
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:50
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:161
|
||||
#: .\cookbook\templates\generic\delete_template.html:5
|
||||
#: .\cookbook\templates\generic\delete_template.html:13
|
||||
#: .\cookbook\templates\generic\edit_template.html:25
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: .\cookbook\templates\base.html:70 .\cookbook\templates\base.html:78
|
||||
#: .\cookbook\templates\base.html:70 .\cookbook\templates\base.html:79
|
||||
#: .\cookbook\templates\forms\ingredients.html:7
|
||||
#: .\cookbook\templates\index.html:7 .\cookbook\templates\shopping_list.html:7
|
||||
msgid "Cookbook"
|
||||
msgstr "Kochbuch"
|
||||
|
||||
#: .\cookbook\templates\base.html:85
|
||||
#: .\cookbook\templates\base.html:86
|
||||
msgid "Utensils"
|
||||
msgstr "Utensilien"
|
||||
|
||||
#: .\cookbook\templates\base.html:89
|
||||
msgid "Books"
|
||||
msgstr "Bücher"
|
||||
|
||||
#: .\cookbook\templates\base.html:92 .\cookbook\templates\meal_plan.html:4
|
||||
#: .\cookbook\templates\meal_plan.html:13 .\cookbook\views\delete.py:136
|
||||
#: .\cookbook\views\edit.py:283 .\cookbook\views\new.py:130
|
||||
msgid "Meal-Plan"
|
||||
msgstr "Plan"
|
||||
|
||||
#: .\cookbook\templates\base.html:95
|
||||
#: .\cookbook\templates\base.html:96
|
||||
msgid "Shopping"
|
||||
msgstr "Einkaufsliste"
|
||||
|
||||
#: .\cookbook\templates\base.html:105
|
||||
#: .\cookbook\templates\base.html:106
|
||||
msgid "Tags"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
#: .\cookbook\templates\base.html:109 .\cookbook\views\delete.py:70
|
||||
#: .\cookbook\views\edit.py:159 .\cookbook\views\lists.py:18
|
||||
#: .\cookbook\views\new.py:46
|
||||
#: .\cookbook\templates\base.html:110 .\cookbook\views\delete.py:70
|
||||
#: .\cookbook\views\edit.py:162 .\cookbook\views\lists.py:18
|
||||
#: .\cookbook\views\new.py:47
|
||||
msgid "Keyword"
|
||||
msgstr "Schlagwort"
|
||||
|
||||
#: .\cookbook\templates\base.html:111
|
||||
#: .\cookbook\templates\base.html:112
|
||||
msgid "Batch Edit"
|
||||
msgstr "Massenbearbeitung"
|
||||
|
||||
#: .\cookbook\templates\base.html:116
|
||||
#: .\cookbook\templates\base.html:117
|
||||
msgid "Storage Data"
|
||||
msgstr "Datenquellen"
|
||||
|
||||
#: .\cookbook\templates\base.html:120
|
||||
#: .\cookbook\templates\base.html:121
|
||||
msgid "Storage Backends"
|
||||
msgstr "Speicher Quellen"
|
||||
|
||||
#: .\cookbook\templates\base.html:122
|
||||
#: .\cookbook\templates\base.html:123
|
||||
msgid "Configure Sync"
|
||||
msgstr "Sync Einstellen"
|
||||
|
||||
#: .\cookbook\templates\base.html:124
|
||||
msgid "Import Recipes"
|
||||
msgstr "Importierte Rezepte"
|
||||
#: .\cookbook\templates\base.html:125
|
||||
msgid "Discovered Recipes"
|
||||
msgstr "Entdeckte Rezepte"
|
||||
|
||||
#: .\cookbook\templates\base.html:126 .\cookbook\views\lists.py:26
|
||||
msgid "Import Log"
|
||||
msgstr "Import Log"
|
||||
#: .\cookbook\templates\base.html:127
|
||||
msgid "Discovery Log"
|
||||
msgstr "Entdeckungs Log"
|
||||
|
||||
#: .\cookbook\templates\base.html:128 .\cookbook\templates\stats.html:10
|
||||
#: .\cookbook\templates\base.html:129 .\cookbook\templates\stats.html:10
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiken"
|
||||
|
||||
#: .\cookbook\templates\base.html:130
|
||||
#: .\cookbook\templates\base.html:131
|
||||
msgid "Units & Ingredients"
|
||||
msgstr "Einheiten & Zutaten"
|
||||
|
||||
#: .\cookbook\templates\base.html:145 .\cookbook\templates\settings.html:6
|
||||
#: .\cookbook\templates\base.html:133
|
||||
msgid "Import Recipe"
|
||||
msgstr "Importier Rezept"
|
||||
|
||||
#: .\cookbook\templates\base.html:149 .\cookbook\templates\settings.html:6
|
||||
#: .\cookbook\templates\settings.html:11
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: .\cookbook\templates\base.html:148
|
||||
#: .\cookbook\templates\base.html:152
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: .\cookbook\templates\base.html:152
|
||||
#: .\cookbook\templates\base.html:156
|
||||
msgid "Logout"
|
||||
msgstr "Ausloggen"
|
||||
|
||||
#: .\cookbook\templates\base.html:157
|
||||
#: .\cookbook\templates\base.html:161
|
||||
#: .\cookbook\templates\registration\login.html:44
|
||||
msgid "Login"
|
||||
msgstr "Einloggen"
|
||||
@@ -253,7 +286,7 @@ msgstr ""
|
||||
"Ausgewählte Schlagwörter zu allen Rezepten die das Suchwort enthalten "
|
||||
"hinzufügen"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:143
|
||||
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:146
|
||||
msgid "Sync"
|
||||
msgstr "Synchronisieren"
|
||||
|
||||
@@ -302,17 +335,45 @@ msgstr "Neues Buch"
|
||||
msgid "There are no recipes in this book yet."
|
||||
msgstr "In diesem Buch sind bisher keine Rezepte."
|
||||
|
||||
#: .\cookbook\templates\export.html:6
|
||||
msgid "Export Recipes"
|
||||
msgstr "Exportier Rezepte"
|
||||
|
||||
#: .\cookbook\templates\export.html:19
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
|
||||
#: .\cookbook\templates\export.html:31
|
||||
msgid "Exported Recipe"
|
||||
msgstr "Exportierte Rezepte"
|
||||
|
||||
#: .\cookbook\templates\export.html:42
|
||||
msgid "Copy to clipboard"
|
||||
msgstr "In Zwischenablage kopieren"
|
||||
|
||||
#: .\cookbook\templates\export.html:54
|
||||
#: .\cookbook\templates\shopping_list.html:48
|
||||
msgid "Copied!"
|
||||
msgstr "Kopiert!"
|
||||
|
||||
#: .\cookbook\templates\export.html:61
|
||||
#: .\cookbook\templates\shopping_list.html:37
|
||||
#: .\cookbook\templates\shopping_list.html:55
|
||||
msgid "Copy list to clipboard"
|
||||
msgstr "Kopiere Liste in Zwischenablage"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:5
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:9
|
||||
msgid "Import new Recipe"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:14
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:47
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:48
|
||||
#: .\cookbook\templates\generic\edit_template.html:23
|
||||
#: .\cookbook\templates\generic\new_template.html:23
|
||||
#: .\cookbook\templates\recipe_view.html:340
|
||||
#: .\cookbook\templates\settings.html:33 .\cookbook\templates\settings.html:47
|
||||
#: .\cookbook\templates\recipe_view.html:357
|
||||
#: .\cookbook\templates\settings.html:22 .\cookbook\templates\settings.html:28
|
||||
#: .\cookbook\templates\settings.html:50 .\cookbook\templates\settings.html:64
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
@@ -321,7 +382,7 @@ msgstr "Speichern"
|
||||
msgid "Edit Recipe"
|
||||
msgstr "Rezept bearbeiten"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:37
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:38
|
||||
msgid ""
|
||||
"Use <b>Ctrl</b>+<b>Space</b> to insert new Ingredient!<br/>You can also save "
|
||||
"the recipe using <b>Ctrl</b>+<b>Shift</b>+<b>S</b>."
|
||||
@@ -329,36 +390,35 @@ msgstr ""
|
||||
"Benutze <b>Strg</b>+<b>Leertaste</b> um eine neue Zutat einzufügen!<br/"
|
||||
">Rezepte können mit<b>Strg</b>+<b>Shift</b>+<b>S</b> gespeichert werden."
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:51
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:52
|
||||
#: .\cookbook\templates\generic\edit_template.html:27
|
||||
#: .\cookbook\templates\recipe_view.html:7
|
||||
msgid "View"
|
||||
msgstr "Angucken"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:55
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:56
|
||||
#: .\cookbook\templates\generic\edit_template.html:30
|
||||
msgid "Delete original file"
|
||||
msgstr "Original löschen"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:142
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:189
|
||||
#: .\cookbook\views\delete.py:81 .\cookbook\views\edit.py:175
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:143
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:190
|
||||
#: .\cookbook\views\delete.py:81 .\cookbook\views\edit.py:178
|
||||
msgid "Ingredient"
|
||||
msgstr "Zutat"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:147
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:148
|
||||
msgid "Amount"
|
||||
msgstr "Menge"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:149
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:150
|
||||
msgid "Unit"
|
||||
msgstr "Einheit"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:154
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:155
|
||||
msgid "Note"
|
||||
msgstr "Notiz "
|
||||
msgstr "Notiz"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:163
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:164
|
||||
msgid "Are you sure that you want to delete this ingredient?"
|
||||
msgstr "Bist du sicher das du diese Zutat löschen willst?"
|
||||
|
||||
@@ -404,7 +464,7 @@ msgstr "Bist du sicher diese beiden Zutaten zusammengeführt werden sollen ?"
|
||||
#: .\cookbook\templates\generic\delete_template.html:18
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
|
||||
msgstr "Bist du sicher das %(title)s: <b>%(object)s</b> gelöscht werden soll"
|
||||
msgstr "Bist du sicher das %(title)s: <b>%(object)s</b> gelöscht werden soll "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:21
|
||||
msgid "Confirm"
|
||||
@@ -441,9 +501,18 @@ msgstr "vorherige"
|
||||
msgid "next"
|
||||
msgstr "nächste"
|
||||
|
||||
#: .\cookbook\templates\import.html:6
|
||||
msgid "Import Recipes"
|
||||
msgstr "Importierte Rezepte"
|
||||
|
||||
#: .\cookbook\templates\import.html:14 .\cookbook\views\delete.py:48
|
||||
#: .\cookbook\views\edit.py:254
|
||||
msgid "Import"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:28
|
||||
#: .\cookbook\views\delete.py:21 .\cookbook\views\edit.py:315
|
||||
#: .\cookbook\views\new.py:34
|
||||
#: .\cookbook\views\delete.py:21 .\cookbook\views\edit.py:318
|
||||
#: .\cookbook\views\new.py:35
|
||||
msgid "Recipe"
|
||||
msgstr "Rezept"
|
||||
|
||||
@@ -501,47 +570,47 @@ msgstr "Suche zurücksetzen"
|
||||
msgid "Log in to view Recipies"
|
||||
msgstr "Bitte einloggen um Rezepte zu sehen"
|
||||
|
||||
#: .\cookbook\templates\meal_plan.html:20
|
||||
#: .\cookbook\templates\meal_plan.html:39
|
||||
msgid "Week"
|
||||
msgstr "Woche"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:67
|
||||
#: .\cookbook\templates\recipe_view.html:71
|
||||
msgid "in"
|
||||
msgstr "in"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:72
|
||||
#: .\cookbook\templates\recipe_view.html:293
|
||||
#: .\cookbook\templates\recipe_view.html:76
|
||||
#: .\cookbook\templates\recipe_view.html:310
|
||||
msgid "by"
|
||||
msgstr "von"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:84
|
||||
#: .\cookbook\templates\recipe_view.html:89
|
||||
msgid "Preparation time ca."
|
||||
msgstr "Zubereitungszeit ca."
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:89
|
||||
#: .\cookbook\templates\recipe_view.html:95
|
||||
msgid "Waiting time ca."
|
||||
msgstr "Wartezeit ca."
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:170
|
||||
#: .\cookbook\templates\recipe_view.html:186
|
||||
msgid "Recipe Image"
|
||||
msgstr "Rezept Bild"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:193
|
||||
#: .\cookbook\templates\recipe_view.html:227
|
||||
#: .\cookbook\templates\recipe_view.html:209
|
||||
#: .\cookbook\templates\recipe_view.html:243
|
||||
msgid "View external recipe"
|
||||
msgstr "Externes Rezept ansehen"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:205
|
||||
#: .\cookbook\templates\recipe_view.html:221
|
||||
msgid "Cloud not show a file preview. Maybe its not a PDF ?"
|
||||
msgstr ""
|
||||
"Datei konnte nicht angezeigt werden. Direkte anzeige funktioniert nur mit "
|
||||
"PDF Dateien."
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:212
|
||||
#: .\cookbook\templates\recipe_view.html:228
|
||||
msgid "External recipe"
|
||||
msgstr "Externes Rezept"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:214
|
||||
#: .\cookbook\templates\recipe_view.html:230
|
||||
msgid ""
|
||||
"\n"
|
||||
" This is an external recipe, which means "
|
||||
@@ -562,16 +631,16 @@ msgstr ""
|
||||
"bleibt weiterhin verfügbar.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:225
|
||||
#: .\cookbook\templates\recipe_view.html:241
|
||||
msgid "Convert now!"
|
||||
msgstr "Jetzt umwandeln!"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:289
|
||||
#: .\cookbook\templates\recipe_view.html:305
|
||||
msgid "Comments"
|
||||
msgstr "Kommentare"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:309 .\cookbook\views\delete.py:103
|
||||
#: .\cookbook\views\edit.py:234
|
||||
#: .\cookbook\templates\recipe_view.html:326 .\cookbook\views\delete.py:103
|
||||
#: .\cookbook\views\edit.py:237
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
@@ -580,10 +649,14 @@ msgid "Your username and password didn't match. Please try again."
|
||||
msgstr "Nutzername oder Passwort falsch. Bitte versuch es erneut."
|
||||
|
||||
#: .\cookbook\templates\settings.html:17
|
||||
msgid "Account"
|
||||
msgstr "Account"
|
||||
|
||||
#: .\cookbook\templates\settings.html:34
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: .\cookbook\templates\settings.html:42
|
||||
#: .\cookbook\templates\settings.html:59
|
||||
msgid "Style"
|
||||
msgstr "Stil"
|
||||
|
||||
@@ -595,15 +668,6 @@ msgstr "Einkaufsliste"
|
||||
msgid "Load"
|
||||
msgstr "Laden"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:37
|
||||
#: .\cookbook\templates\shopping_list.html:55
|
||||
msgid "Copy list to clipboard"
|
||||
msgstr "Kopiere Liste in Zwischenablage"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:48
|
||||
msgid "Copied!"
|
||||
msgstr "Kopiert!"
|
||||
|
||||
#: .\cookbook\templates\stats.html:4
|
||||
msgid "Stats"
|
||||
msgstr "Statistiken"
|
||||
@@ -644,22 +708,17 @@ msgstr[0] "Massenbearbeitung erfolgreich. %(count)d Rezept wurde aktualisiert."
|
||||
msgstr[1] ""
|
||||
"Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
|
||||
|
||||
#: .\cookbook\views\delete.py:48 .\cookbook\views\edit.py:251
|
||||
#: .\cookbook\views\lists.py:35
|
||||
msgid "Import"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: .\cookbook\views\delete.py:59
|
||||
msgid "Monitor"
|
||||
msgstr "Monitor"
|
||||
|
||||
#: .\cookbook\views\delete.py:92 .\cookbook\views\lists.py:53
|
||||
#: .\cookbook\views\new.py:64
|
||||
#: .\cookbook\views\new.py:65
|
||||
msgid "Storage Backend"
|
||||
msgstr "Speicher Quelle"
|
||||
|
||||
#: .\cookbook\views\delete.py:114 .\cookbook\views\edit.py:267
|
||||
#: .\cookbook\views\new.py:112
|
||||
#: .\cookbook\views\delete.py:114 .\cookbook\views\edit.py:270
|
||||
#: .\cookbook\views\new.py:114
|
||||
msgid "Recipe Book"
|
||||
msgstr "Rezeptbuch"
|
||||
|
||||
@@ -667,58 +726,82 @@ msgstr "Rezeptbuch"
|
||||
msgid "Bookmarks"
|
||||
msgstr "Lesezeichen"
|
||||
|
||||
#: .\cookbook\views\edit.py:117
|
||||
#: .\cookbook\views\edit.py:104
|
||||
msgid "There was an error converting your ingredients amount to a number: "
|
||||
msgstr "Es gab einen Fehler beim umwandeln der Menge in eine Zahl: "
|
||||
|
||||
#: .\cookbook\views\edit.py:120
|
||||
msgid "Recipe saved!"
|
||||
msgstr "Rezept gespeichert"
|
||||
msgstr "Rezept gespeichert!"
|
||||
|
||||
#: .\cookbook\views\edit.py:119
|
||||
#: .\cookbook\views\edit.py:122
|
||||
msgid "There was an error saving this recipe!"
|
||||
msgstr "Es gab einen Fehler beim Speichern des Rezepts"
|
||||
msgstr "Es gab einen Fehler beim Speichern des Rezepts!"
|
||||
|
||||
#: .\cookbook\views\edit.py:184
|
||||
#: .\cookbook\views\edit.py:187
|
||||
msgid "You cannot edit this storage!"
|
||||
msgstr "Du kannst diese Speicherquelle nicht bearbeiten!"
|
||||
|
||||
#: .\cookbook\views\edit.py:203
|
||||
#: .\cookbook\views\edit.py:206
|
||||
msgid "Storage saved!"
|
||||
msgstr "Speicherquelle gespeichert"
|
||||
msgstr "Speicherquelle gespeichert!"
|
||||
|
||||
#: .\cookbook\views\edit.py:205
|
||||
msgid "There was an error updating this storage backend.!"
|
||||
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle"
|
||||
#: .\cookbook\views\edit.py:208
|
||||
msgid "There was an error updating this storage backend!"
|
||||
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle!"
|
||||
|
||||
#: .\cookbook\views\edit.py:225
|
||||
#: .\cookbook\views\edit.py:228
|
||||
msgid "You cannot edit this comment!"
|
||||
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
|
||||
|
||||
#: .\cookbook\views\edit.py:303
|
||||
#: .\cookbook\views\edit.py:306
|
||||
msgid "Changes saved!"
|
||||
msgstr "Änderungen gespeichert"
|
||||
msgstr "Änderungen gespeichert!"
|
||||
|
||||
#: .\cookbook\views\edit.py:307
|
||||
#: .\cookbook\views\edit.py:310
|
||||
msgid "Error saving changes!"
|
||||
msgstr "Fehler beim Speichern der Daten."
|
||||
msgstr "Fehler beim Speichern der Daten!"
|
||||
|
||||
#: .\cookbook\views\edit.py:337
|
||||
#: .\cookbook\views\edit.py:340
|
||||
msgid "Units merged!"
|
||||
msgstr "Einheiten zusammengeführt"
|
||||
msgstr "Einheiten zusammengeführt!"
|
||||
|
||||
#: .\cookbook\views\edit.py:350
|
||||
#: .\cookbook\views\edit.py:353
|
||||
msgid "Ingredients merged!"
|
||||
msgstr "Zutaten zusammengeführt"
|
||||
msgstr "Zutaten zusammengeführt!"
|
||||
|
||||
#: .\cookbook\views\new.py:86
|
||||
#: .\cookbook\views\import_export.py:57
|
||||
msgid "Recipe imported successfully!"
|
||||
msgstr "Rezept erfolgreich importiert!"
|
||||
|
||||
#: .\cookbook\views\import_export.py:103
|
||||
msgid ""
|
||||
"External recipes cannot be exported, please share the file directly or "
|
||||
"select an internal recipe."
|
||||
msgstr ""
|
||||
"Externe Rezepte können nicht exportiert werden, bitte Datei direkt teilen "
|
||||
"oder ein Internes Rezept auswählen."
|
||||
|
||||
#: .\cookbook\views\lists.py:26
|
||||
msgid "Import Log"
|
||||
msgstr "Import Log"
|
||||
|
||||
#: .\cookbook\views\lists.py:35
|
||||
msgid "Discovery"
|
||||
msgstr "Entdeckung"
|
||||
|
||||
#: .\cookbook\views\new.py:88
|
||||
msgid "Imported new recipe!"
|
||||
msgstr "Importier neue Rezepte"
|
||||
msgstr "Importier neue Rezepte!"
|
||||
|
||||
#: .\cookbook\views\new.py:89
|
||||
#: .\cookbook\views\new.py:91
|
||||
msgid "There was an error importing this recipe!"
|
||||
msgstr "Beim importieren des Rezeptes ist ein Fehler aufgetreten"
|
||||
msgstr "Beim importieren des Rezeptes ist ein Fehler aufgetreten!"
|
||||
|
||||
#: .\cookbook\views\views.py:44
|
||||
#: .\cookbook\views\views.py:63
|
||||
msgid "Comment saved!"
|
||||
msgstr "Kommentar gespeichert"
|
||||
msgstr "Kommentar gespeichert!"
|
||||
|
||||
#: .\cookbook\views\views.py:54
|
||||
#: .\cookbook\views\views.py:73
|
||||
msgid "Bookmark saved!"
|
||||
msgstr "Lesezeichen gespeichert"
|
||||
msgstr "Lesezeichen gespeichert!"
|
||||
|
||||
18
cookbook/migrations/0032_userpreference_default_unit.py
Normal file
18
cookbook/migrations/0032_userpreference_default_unit.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.4 on 2020-04-13 20:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0031_auto_20200407_1841'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='default_unit',
|
||||
field=models.CharField(default='g', max_length=32),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0033_userpreference_default_page.py
Normal file
18
cookbook/migrations/0033_userpreference_default_page.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.4 on 2020-04-13 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0032_userpreference_default_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='default_page',
|
||||
field=models.CharField(choices=[('SEARCH', 'Search'), ('PLAN', 'Meal-Plan')], default='SEARCH', max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -41,9 +41,18 @@ class UserPreference(models.Model):
|
||||
|
||||
COLORS = ((PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), (SUCCESS, 'Success'), (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), (DARK, 'Dark'))
|
||||
|
||||
# Default Page
|
||||
SEARCH = 'SEARCH'
|
||||
PLAN = 'PLAN'
|
||||
BOOKS = 'BOOKS'
|
||||
|
||||
PAGES = ((SEARCH, _('Search')), (PLAN, _('Meal-Plan')), (BOOKS, _('Books')), )
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
|
||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||
default_unit = models.CharField(max_length=32, default='g')
|
||||
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
|
||||
|
||||
def __str__(self):
|
||||
return self.user
|
||||
@@ -145,8 +154,8 @@ class Ingredient(models.Model):
|
||||
|
||||
|
||||
class RecipeIngredient(models.Model):
|
||||
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT)
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.PROTECT)
|
||||
amount = models.DecimalField(default=0, decimal_places=2, max_digits=16)
|
||||
note = models.CharField(max_length=64, null=True, blank=True)
|
||||
|
||||
@@ -16,9 +16,10 @@ class Nextcloud(Provider):
|
||||
@staticmethod
|
||||
def get_client(storage):
|
||||
options = {
|
||||
'webdav_hostname': storage.url + '/remote.php/dav/files/' + storage.username,
|
||||
'webdav_hostname': storage.url,
|
||||
'webdav_login': storage.username,
|
||||
'webdav_password': storage.password
|
||||
'webdav_password': storage.password,
|
||||
'webdav_root': '/remote.php/dav/files/' + storage.username
|
||||
}
|
||||
return wc.Client(options)
|
||||
|
||||
|
||||
@@ -74,8 +74,9 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'index,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
|
||||
<a class="nav-link" href="{% url 'index' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'view_search,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
|
||||
<a class="nav-link" href="{% url 'view_search' %}"><i
|
||||
class="fas fa-book"></i> {% trans 'Cookbook' %}<span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
|
||||
@@ -121,13 +122,15 @@
|
||||
<a class="dropdown-item" href="{% url 'data_sync' %}"><i
|
||||
class="fas fa-sync-alt fa-fw"></i> {% trans 'Configure Sync' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'list_recipe_import' %}"><i
|
||||
class="far fa-file-alt fa-fw"></i> {% trans 'Import Recipes' %}</a>
|
||||
class="far fa-file-alt fa-fw"></i> {% trans 'Discovered Recipes' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'list_sync_log' %}"><i
|
||||
class="fas fa-history fa-fw"></i> {% trans 'Import Log' %}</a>
|
||||
class="fas fa-history fa-fw"></i> {% trans 'Discovery Log' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'data_stats' %}"><i
|
||||
class="fas fa-chart-line fa-fw"></i> {% trans 'Statistics' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'edit_ingredient' %}"><i
|
||||
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units & Ingredients' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'view_import' %}"><i
|
||||
class="fas fa-file-import"></i> {% trans 'Import Recipe' %}</a>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
@@ -137,7 +140,8 @@
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings' %}active{% endif %}">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false"><i class="fas fa-user-alt"></i> {{ user.get_user_name }}
|
||||
aria-haspopup="true" aria-expanded="false"><i
|
||||
class="fas fa-user-alt"></i> {{ user.get_user_name }}
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
|
||||
|
||||
70
cookbook/templates/export.html
Normal file
70
cookbook/templates/export.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-file-export"></i> {% trans 'Export' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if export %}
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<label for="id_export">
|
||||
{% trans 'Exported Recipe' %}</label>
|
||||
<textarea id="id_export" class="form-control" rows="12">
|
||||
{{ export }}
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-center">
|
||||
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
|
||||
data-placement="right" title="{% trans 'Copy to clipboard' %}" id="id_btn_copy"
|
||||
onmouseout="resetTooltip()"><i
|
||||
class="far fa-copy"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function copy() {
|
||||
let json = $('#id_export');
|
||||
|
||||
json.select();
|
||||
|
||||
$('#id_btn_copy').attr('data-original-title', '{% trans 'Copied!' %}').tooltip('show');
|
||||
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
function resetTooltip() {
|
||||
setTimeout(function () {
|
||||
$('#id_btn_copy').attr('data-original-title', '{% trans 'Copy list to clipboard' %}');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -189,7 +189,7 @@
|
||||
data.push({
|
||||
ingredient__name: "{% trans 'Ingredient' %}",
|
||||
amount: "100",
|
||||
unit__name: "g",
|
||||
unit__name: "{{ request.user.userpreference.default_unit }}",
|
||||
note: "",
|
||||
id: Math.floor(Math.random() * 10000000),
|
||||
delete: false,
|
||||
|
||||
19
cookbook/templates/import.html
Normal file
19
cookbook/templates/import.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'Import Recipes' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-file-import"></i> {% trans 'Import' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -52,16 +52,24 @@
|
||||
class="fas fa-pencil-alt"></i></a></h3>
|
||||
</div>
|
||||
<div class="col col-md-3 d-print-none" style="text-align: right">
|
||||
<button class="btn btn-success" onclick="$('#bookmarkModal').modal({'show':true})"><i
|
||||
<button class="btn btn-success" onclick="$('#bookmarkModal').modal({'show':true})" data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Add to Book' %}"><i
|
||||
class="fas fa-bookmark"></i></button>
|
||||
{% if ingredients %}
|
||||
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}"><i
|
||||
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}" data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Generate shopping list' %}"><i
|
||||
class="fas fa-shopping-cart"></i></a>
|
||||
{% endif %}
|
||||
<a class="btn btn-info" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}"><i
|
||||
<a class="btn btn-info" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}" data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Add to Mealplan' %}"><i
|
||||
class="fas fa-calendar"></i></a>
|
||||
<a class="btn btn-light" onclick="window.print()"><i
|
||||
<a class="btn btn-light" onclick="window.print()" data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Print' %}"><i
|
||||
class="fas fa-print"></i></a>
|
||||
<a class="btn btn-primary" href="{% url 'view_export' %}?r={{ recipe.pk }}" target="_blank"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Export recipe' %}"><i
|
||||
class="fas fa-file-export"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -382,5 +390,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-center">
|
||||
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
|
||||
data-placement="right" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
|
||||
class="far fa-copy"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ from bleach_whitelist import markdown_tags, markdown_attrs, all_styles, print_at
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from cookbook.models import get_model_name
|
||||
|
||||
register = template.Library()
|
||||
@@ -28,5 +29,5 @@ def delete_url(model, pk):
|
||||
@register.filter()
|
||||
def markdown(value):
|
||||
tags = markdown_tags + ['pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead']
|
||||
parsed_md = md.markdown(value, extensions=['markdown.extensions.fenced_code', 'tables', MarkdownFormatExtension()])
|
||||
parsed_md = md.markdown(value, extensions=['markdown.extensions.fenced_code', 'tables', UrlizeExtension(), MarkdownFormatExtension()])
|
||||
return bleach.clean(parsed_md, tags, markdown_attrs)
|
||||
|
||||
@@ -7,10 +7,10 @@ class TestViewsGeneral(TestViews):
|
||||
|
||||
def test_index(self):
|
||||
r = self.client.get(reverse('index'))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
r = self.anonymous_client.get(reverse('index'))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
def test_books(self):
|
||||
url = reverse('view_books')
|
||||
|
||||
@@ -3,16 +3,20 @@ from pydoc import locate
|
||||
from django.urls import path
|
||||
|
||||
from .views import *
|
||||
from cookbook.views import api
|
||||
from cookbook.views import api, import_export
|
||||
from cookbook.helper import dal
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('search/', views.search, name='view_search'),
|
||||
path('books/', views.books, name='view_books'),
|
||||
path('plan/', views.meal_plan, name='view_plan'),
|
||||
path('shopping/', views.shopping_list, name='view_shopping'),
|
||||
path('settings/', views.settings, name='view_settings'),
|
||||
|
||||
path('import/', import_export.import_recipe, name='view_import'),
|
||||
path('export/', import_export.export_recipe, name='view_export'),
|
||||
|
||||
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
|
||||
|
||||
path('new/recipe_import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
|
||||
|
||||
@@ -205,7 +205,7 @@ def edit_storage(request, pk):
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Storage saved!'))
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('There was an error updating this storage backend.!'))
|
||||
messages.add_message(request, messages.ERROR, _('There was an error updating this storage backend!'))
|
||||
else:
|
||||
pseudo_instance = instance
|
||||
pseudo_instance.password = '__NO__CHANGE__'
|
||||
|
||||
114
cookbook/views/import_export.py
Normal file
114
cookbook/views/import_export.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import IntegrityError
|
||||
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from cookbook.forms import ExportForm, ImportForm
|
||||
from cookbook.models import RecipeIngredient, Recipe, Unit, Ingredient, Keyword
|
||||
|
||||
|
||||
def import_recipe(request):
|
||||
if request.method == "POST":
|
||||
form = ImportForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = json.loads(form.cleaned_data['recipe'])
|
||||
|
||||
recipe = Recipe.objects.create(name=data['recipe']['name'], instructions=data['recipe']['instructions'],
|
||||
working_time=data['recipe']['working_time'], waiting_time=data['recipe']['waiting_time'],
|
||||
created_by=request.user, internal=True)
|
||||
|
||||
for k in data['keywords']:
|
||||
try:
|
||||
Keyword.objects.create(name=k['name'], icon=k['icon'], description=k['description']).save()
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
recipe.keywords.add(Keyword.objects.get(name=k['name']))
|
||||
|
||||
for u in data['units']:
|
||||
try:
|
||||
Unit.objects.create(name=u['name'], description=u['description']).save()
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
for i in data['ingredients']:
|
||||
try:
|
||||
Ingredient.objects.create(name=i['name']).save()
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
for ri in data['recipe_ingredients']:
|
||||
RecipeIngredient.objects.create(recipe=recipe, ingredient=Ingredient.objects.get(name=ri['ingredient']),
|
||||
unit=Unit.objects.get(name=ri['unit']), amount=ri['amount'], note=ri['note'])
|
||||
|
||||
if data['image']:
|
||||
fmt, img = data['image'].split(';base64,')
|
||||
ext = fmt.split('/')[-1]
|
||||
recipe.image = ContentFile(base64.b64decode(img), name=f'{recipe.pk}.{ext}')
|
||||
recipe.save()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Recipe imported successfully!'))
|
||||
return HttpResponseRedirect(reverse_lazy('view_recipe', args=[recipe.pk]))
|
||||
else:
|
||||
form = ImportForm()
|
||||
|
||||
return render(request, 'import.html', {'form': form})
|
||||
|
||||
|
||||
def export_recipe(request):
|
||||
context = {}
|
||||
if request.method == "POST":
|
||||
form = ExportForm(request.POST)
|
||||
if form.is_valid():
|
||||
recipe = form.cleaned_data['recipe']
|
||||
if recipe.internal:
|
||||
export = {
|
||||
'recipe': {'name': recipe.name, 'instructions': recipe.instructions, 'working_time': recipe.working_time, 'waiting_time': recipe.working_time},
|
||||
'units': [],
|
||||
'ingredients': [],
|
||||
'recipe_ingredients': [],
|
||||
'keywords': [],
|
||||
'image': None
|
||||
}
|
||||
|
||||
for k in recipe.keywords.all():
|
||||
export['keywords'].append({'name': k.name, 'icon': k.icon, 'description': k.description})
|
||||
|
||||
for ri in RecipeIngredient.objects.filter(recipe=recipe).all():
|
||||
if ri.unit not in export['units']:
|
||||
export['units'].append({'name': ri.unit.name, 'description': ri.unit.description})
|
||||
if ri.ingredient not in export['ingredients']:
|
||||
export['ingredients'].append({'name': ri.ingredient.name})
|
||||
|
||||
export['recipe_ingredients'].append({'ingredient': ri.ingredient.name, 'unit': ri.unit.name, 'amount': float(ri.amount), 'note': ri.note})
|
||||
|
||||
if recipe.image and form.cleaned_data['image']:
|
||||
with open(recipe.image.path, 'rb') as img_f:
|
||||
export['image'] = f'data:image/png;base64,{base64.b64encode(img_f.read()).decode("utf-8")}'
|
||||
|
||||
if form.cleaned_data['download']:
|
||||
response = HttpResponse(json.dumps(export), content_type='text/plain')
|
||||
response['Content-Disposition'] = f'attachment; filename={recipe.name}.json'
|
||||
return response
|
||||
|
||||
context['export'] = json.dumps(export, indent=4)
|
||||
else:
|
||||
form.add_error('recipe', _('External recipes cannot be exported, please share the file directly or select an internal recipe.'))
|
||||
else:
|
||||
form = ExportForm()
|
||||
recipe = request.GET.get('r')
|
||||
if recipe:
|
||||
if re.match(r'^([0-9])+$', recipe):
|
||||
if recipe := Recipe.objects.filter(pk=int(recipe)).first():
|
||||
form = ExportForm(initial={'recipe': recipe})
|
||||
|
||||
context['form'] = form
|
||||
|
||||
return render(request, 'export.html', context)
|
||||
@@ -32,7 +32,7 @@ def recipe_import(request):
|
||||
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Import"), 'table': table, 'import_btn': True})
|
||||
return render(request, 'generic/list_template.html', {'title': _("Discovery"), 'table': table, 'import_btn': True})
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
@@ -6,7 +6,9 @@ from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django_tables2 import RequestConfig
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -16,6 +18,21 @@ from cookbook.tables import RecipeTable
|
||||
|
||||
|
||||
def index(request):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse_lazy('view_search'))
|
||||
try:
|
||||
page_map = {
|
||||
UserPreference.SEARCH: reverse_lazy('view_search'),
|
||||
UserPreference.PLAN: reverse_lazy('view_plan'),
|
||||
UserPreference.BOOKS: reverse_lazy('view_books'),
|
||||
}
|
||||
|
||||
return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page))
|
||||
except UserPreference.DoesNotExist:
|
||||
return HttpResponseRedirect(reverse_lazy('view_search'))
|
||||
|
||||
|
||||
def search(request):
|
||||
if request.user.is_authenticated:
|
||||
f = RecipeFilter(request.GET, queryset=Recipe.objects.all().order_by('name'))
|
||||
|
||||
@@ -175,6 +192,8 @@ def settings(request):
|
||||
up = UserPreference(user=request.user)
|
||||
up.theme = form.cleaned_data['theme']
|
||||
up.nav_color = form.cleaned_data['nav_color']
|
||||
up.default_unit = form.cleaned_data['default_unit']
|
||||
up.default_page = form.cleaned_data['default_page']
|
||||
up.save()
|
||||
|
||||
if 'user_name_form' in request.POST:
|
||||
|
||||
33
docs/k8s/10-configmap.yaml
Normal file
33
docs/k8s/10-configmap.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
name: recipes-nginx-config
|
||||
data:
|
||||
nginx-config: |-
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 16M;
|
||||
|
||||
# serve static files
|
||||
location /static/ {
|
||||
alias /static/;
|
||||
}
|
||||
# serve media files
|
||||
location /media/ {
|
||||
alias /media/;
|
||||
}
|
||||
# pass requests for dynamic content to gunicorn
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
docs/k8s/30-pv.yaml
Normal file
50
docs/k8s/30-pv.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-db
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: db
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/db"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-media
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: media
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/media"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-static
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: static
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/static"
|
||||
52
docs/k8s/30-pvc.yaml
Normal file
52
docs/k8s/30-pvc.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-db
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: db
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-media
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: media
|
||||
app: recipes
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-static
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: static
|
||||
app: recipes
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
102
docs/k8s/50-deployment.yaml
Normal file
102
docs/k8s/50-deployment.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: recipes
|
||||
labels:
|
||||
app: recipes
|
||||
environment: production
|
||||
tier: frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: recipes
|
||||
environment: production
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
environment: production
|
||||
spec:
|
||||
containers:
|
||||
- name: recipes-nginx
|
||||
image: nginx:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
name: http
|
||||
volumeMounts:
|
||||
- mountPath: '/media'
|
||||
name: media
|
||||
- mountPath: '/static'
|
||||
name: static
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx-config
|
||||
readOnly: true
|
||||
- name: recipes
|
||||
image: 'vabene1111/recipes:latest'
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
volumeMounts:
|
||||
- mountPath: '/opt/recipes/mediafiles'
|
||||
name: media
|
||||
- mountPath: '/opt/recipes/staticfiles'
|
||||
name: static
|
||||
env:
|
||||
- name: DEBUG
|
||||
value: "0"
|
||||
- name: ALLOWED_HOSTS
|
||||
value: '*'
|
||||
- name: SECRET_KEY
|
||||
value: # CHANGEME
|
||||
- name: DB_ENGINE
|
||||
value: django.db.backends.postgresql_psycopg2
|
||||
- name: POSTGRES_HOST
|
||||
value: localhost
|
||||
- name: POSTGRES_PORT
|
||||
value: "5432"
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: # CHANGEME
|
||||
- name: recipes-db
|
||||
image: 'postgres:latest'
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- mountPath: '/var/lib/postgresql/data'
|
||||
name: database
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: # CHANGEME
|
||||
volumes:
|
||||
- name: database
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-db
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-media
|
||||
- name: static
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-static
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: recipes-nginx-config
|
||||
15
docs/k8s/60-service.yaml
Normal file
15
docs/k8s/60-service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: recipes
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
app: recipes
|
||||
environment: production
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
protocol: TCP
|
||||
25
docs/k8s/README.md
Normal file
25
docs/k8s/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Kubernetes
|
||||
|
||||
This is a basic kubernetes setup. Please note that this does not necessarily follow Kubernetes best practices and should only used as a basis to build your own setup from!
|
||||
|
||||
## Important notes
|
||||
|
||||
State (database, static files and media files) is handled via `PersistentVolumes`.
|
||||
|
||||
Note that you will most likely have to change the `PersistentVolumes` in `30-pv.yaml`. The current setup is only usable for a single-node cluster because it uses local storage on the kubernetes worker nodes under `/data/recipes/`. It should just serve as an example.
|
||||
|
||||
Currently, the deployment in `50-deployment.yaml` just pulls the `latest` tag of all containers. In a production setup, you should set this to a fixed version!
|
||||
|
||||
See env variables tagged with `CHANGEME` in `50-deployment.yaml` and make sure to change those! A better setup would use kubernetes secrets but this is not implemented yet.
|
||||
|
||||
## Updates
|
||||
|
||||
These manifests are not tested against new versions.
|
||||
|
||||
## Apply the manifets
|
||||
|
||||
To apply the manifest with `kubectl`, use the following command:
|
||||
|
||||
```
|
||||
kubectl apply -f ./docs/k8s/
|
||||
```
|
||||
BIN
docs/preview.png
BIN
docs/preview.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.5 MiB |
BIN
docs/preview.xcf
BIN
docs/preview.xcf
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-03-18 12:13+0100\n"
|
||||
"POT-Creation-Date: 2020-04-25 23:31+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,10 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:136
|
||||
#: .\recipes\settings.py:137
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: .\recipes\settings.py:137
|
||||
#: .\recipes\settings.py:138
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
Reference in New Issue
Block a user