Compare commits

...

29 Commits
0.6.3 ... 0.6.5

Author SHA1 Message Date
vabene1111
c7046bc705 fixed markdown urlize 2020-04-26 15:52:07 +02:00
vabene1111
52946a8e4c fixed broken emoji 2020-04-26 00:29:56 +02:00
vabene1111
dd6b77e029 added screenshots + refactor preview + moved docu 2020-04-26 00:28:14 +02:00
vabene1111
396c1f3d5f added tooltips to recipe view 2020-04-25 23:35:01 +02:00
vabene1111
379d5a5177 import export cleanup + features 2020-04-25 23:32:15 +02:00
vabene1111
85a4d5d432 basic import export working 2020-04-25 22:26:59 +02:00
vabene1111
43eb10e488 added basic exporting capability 2020-04-25 22:05:55 +02:00
vabene1111
d702c08a12 fixed urlize breaking markdown links 2020-04-25 10:46:27 +02:00
vabene1111
e78323d214 Update docker-publish-latest.yml 2020-04-15 15:05:36 +02:00
vabene1111
d2e866dd74 cleanup/refactor workflows 2020-04-15 14:48:18 +02:00
vabene1111
76687ad5df testing multi platform deployment 2020-04-15 12:34:58 +02:00
vabene1111
dab77e8e4f imrpoved index redirect + fixed tests 2020-04-13 23:11:33 +02:00
vabene1111
0b250c71aa actually fixed action yml 2020-04-13 22:55:50 +02:00
vabene1111
571f670db0 fixed action intendations 2020-04-13 22:54:17 +02:00
vabene1111
4e9e628162 added ability to change default page 2020-04-13 22:52:02 +02:00
vabene1111
4f49b06704 user setting default ingredient unit 2020-04-13 22:37:50 +02:00
vabene1111
8eb0c36665 fixed action invalid yaml 2020-04-13 21:53:36 +02:00
vabene1111
6f69c09aca Merge pull request #55 from hakoerber/kubernetes-manifests
Kubernetes manifests
2020-04-13 21:51:58 +02:00
vabene1111
8e6f153882 Merge pull request #56 from tourn/clickable-links
Make links in recipe clickable
2020-04-13 21:50:25 +02:00
vabene1111
07183fd40f actions testing 2020-04-13 21:39:20 +02:00
tourn
04b7f0a398 Make links in recipe clickable 2020-04-13 21:27:22 +02:00
Hannes Körber
1735fda48f Add basic kubernetes manifest
Closes #50
2020-04-13 19:39:41 +02:00
Hannes Körber
1c9ea0eda7 Use relative links in README
See https://github.blog/2013-01-31-relative-links-in-markup-files/ for
more details.
2020-04-13 19:39:39 +02:00
vabene1111
83b5b6695c Update docker-release-publish.yml 2020-04-13 18:25:49 +02:00
vabene1111
342fb3c96d Update docker-release-publish.yml 2020-04-13 18:23:19 +02:00
vabene1111
b7a18466b5 testing multi plattform builds 2020-04-13 18:17:53 +02:00
vabene1111
0cdc4d51df Merge branch 'feature/webdav-root-option' into develop 2020-04-13 17:41:02 +02:00
vabene1111
e177669514 Merge pull request #51 from pataya23/develop
added 'webdav_root' option
2020-04-13 17:38:19 +02:00
pataya23
fd294dfcdd added 'webdav_root' option
otherwise I get an error (webdav3.exceptions.RemoteResourceNotFound: Remote resource: </path> not found)
2020-04-13 13:19:03 +02:00
36 changed files with 986 additions and 224 deletions

View File

@@ -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 }}

View File

@@ -0,0 +1,19 @@
name: publish latest image docker
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: latest
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -0,0 +1,25 @@
name: publish tagged release docker
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master#
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -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 }}

View File

@@ -3,6 +3,8 @@ Recipes is a Django application to manage, tag and search recipes using either b
![Preview](docs/preview.png)
[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.

View File

@@ -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'

View File

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

View File

@@ -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!"

View File

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

View File

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

View File

@@ -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)

View File

@@ -16,9 +16,10 @@ class Nextcloud(Provider):
@staticmethod
def get_client(storage):
options = {
'webdav_hostname': storage.url + '/remote.php/dav/files/' + storage.username,
'webdav_hostname': storage.url,
'webdav_login': storage.username,
'webdav_password': storage.password
'webdav_password': storage.password,
'webdav_root': '/remote.php/dav/files/' + storage.username
}
return wc.Client(options)

View File

@@ -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">

View File

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

View File

@@ -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,

View File

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

View File

@@ -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 %}

View File

@@ -34,7 +34,7 @@
<div class="row">
<div class="col col-md-12 text-center">
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
data-placement="top" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
data-placement="right" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
class="far fa-copy"></i></button>
</div>
</div>

View File

@@ -5,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)

View File

@@ -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')

View File

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

View File

@@ -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__'

View 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)

View File

@@ -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

View File

@@ -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:

View 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
View 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
View 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
View 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
View 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
View 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/
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

View File

@@ -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"