diff --git a/.github/workflows/main.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/main.yml rename to .github/workflows/ci.yml diff --git a/.github/workflows/docker-publish-dev.yml b/.github/workflows/docker-publish-dev.yml new file mode 100644 index 000000000..b777f1da7 --- /dev/null +++ b/.github/workflows/docker-publish-dev.yml @@ -0,0 +1,18 @@ +name: publish dev image docker +on: + push: + branches: + - '*' + - '*/*' + - '!master' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@2.13 + with: + name: vabene1111/recipes + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/docker-publish-latest.yml b/.github/workflows/docker-publish-latest.yml new file mode 100644 index 000000000..859027e27 --- /dev/null +++ b/.github/workflows/docker-publish-latest.yml @@ -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 }} diff --git a/.github/workflows/docker-release-publish.yml b/.github/workflows/docker-publish-release.yml similarity index 56% rename from .github/workflows/docker-release-publish.yml rename to .github/workflows/docker-publish-release.yml index 951d2e8b5..eee5088bc 100644 --- a/.github/workflows/docker-release-publish.yml +++ b/.github/workflows/docker-publish-release.yml @@ -1,4 +1,4 @@ -name: Deploy Docker Image +name: publish tagged release docker on: push: @@ -11,12 +11,15 @@ jobs: name: Build image job steps: - name: Checkout master - uses: actions/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: recipes - tag: test + imageName: vabene1111/recipes + tag: ${{ steps.get_version.outputs.VERSION }} dockerHubUser: ${{ secrets.DOCKER_USERNAME }} dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 519869f6a..000000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -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 }} diff --git a/README.md b/README.md index 402de933e..2ea1a8995 100644 --- a/README.md +++ b/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. diff --git a/cookbook/admin.py b/cookbook/admin.py index 8127f0ad9..ed8cad2a1 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -3,7 +3,7 @@ from .models import * class UserPreferenceAdmin(admin.ModelAdmin): - list_display = ('name', 'theme', 'nav_color') + list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style') @staticmethod def name(obj): @@ -80,7 +80,7 @@ class RecipeBookAdmin(admin.ModelAdmin): @staticmethod def user_name(obj): - return obj.user.get_user_name() + return obj.created_by.get_user_name() admin.site.register(RecipeBook, RecipeBookAdmin) @@ -98,7 +98,7 @@ class MealPlanAdmin(admin.ModelAdmin): @staticmethod def user(obj): - return obj.user.get_user_name() + return obj.created_by.get_user_name() admin.site.register(MealPlan, MealPlanAdmin) diff --git a/cookbook/forms.py b/cookbook/forms.py index 1355bd9c0..52eb993ed 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -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', 'search_style') 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' @@ -218,7 +237,8 @@ class ImportRecipeForm(forms.ModelForm): class RecipeBookForm(forms.ModelForm): class Meta: model = RecipeBook - fields = ('name',) + fields = ('name', 'icon', 'description', 'shared') + widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget} class MealPlanForm(forms.ModelForm): diff --git a/cookbook/helper/mdx_urlize.py b/cookbook/helper/mdx_urlize.py new file mode 100644 index 000000000..7df06430b --- /dev/null +++ b/cookbook/helper/mdx_urlize.py @@ -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'
' + +>>> md.convert('go to http://example.com') +u'go to http://example.com
' + +>>> md.convert('example.com') +u'' + +>>> md.convert('example.net') +u'' + +>>> md.convert('www.example.us') +u'' + +>>> md.convert('(www.example.us/path/?name=val)') +u'(www.example.us/path/?name=val)
' + +>>> md.convert('go togo to http://example.com now!
' + +Negative examples: + +>>> md.convert('del.icio.us') +u'del.icio.us
' + +""" + +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() diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py new file mode 100644 index 000000000..10a310cc9 --- /dev/null +++ b/cookbook/helper/permission_helper.py @@ -0,0 +1,68 @@ +""" +Source: https://djangosnippets.org/snippets/1703/ +""" +from django.contrib import messages +from django.contrib.auth.decorators import user_passes_test +from django.utils.translation import gettext as _ +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy, reverse + + +def get_allowed_groups(groups_required): + groups_allowed = tuple(groups_required) + if 'guest' in groups_required: + groups_allowed = groups_allowed + ('user', 'admin') + if 'user' in groups_required: + groups_allowed = groups_allowed + ('admin',) + return groups_allowed + + +def group_required(*groups_required): + """Requires user membership in at least one of the groups passed in.""" + + def in_groups(u): + groups_allowed = get_allowed_groups(groups_required) + if u.is_authenticated: + if u.is_superuser | bool(u.groups.filter(name__in=groups_allowed)): + return True + return False + + return user_passes_test(in_groups, login_url='index') + + +class GroupRequiredMixin(object): + """ + groups_required - list of strings, required param + """ + + groups_required = None + + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!')) + return HttpResponseRedirect(reverse_lazy('login')) + else: + if not request.user.is_superuser: + group_allowed = get_allowed_groups(self.groups_required) + user_groups = [] + for group in request.user.groups.values_list('name', flat=True): + user_groups.append(group) + if len(set(user_groups).intersection(group_allowed)) <= 0: + messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + return HttpResponseRedirect(reverse_lazy('index')) + return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs) + + +class OwnerRequiredMixin(object): + + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!')) + return HttpResponseRedirect(reverse_lazy('login')) + else: + obj = self.get_object() + if not (obj.created_by == request.user or request.user.is_superuser): + messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!')) + return HttpResponseRedirect(reverse('index')) + + return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs) diff --git a/cookbook/locale/de/LC_MESSAGES/django.mo b/cookbook/locale/de/LC_MESSAGES/django.mo index 07f72e42a..0552499b3 100644 Binary files a/cookbook/locale/de/LC_MESSAGES/django.mo and b/cookbook/locale/de/LC_MESSAGES/django.mo differ diff --git a/cookbook/locale/de/LC_MESSAGES/django.po b/cookbook/locale/de/LC_MESSAGES/django.po index 55d3f6024..7cc958be0 100644 --- a/cookbook/locale/de/LC_MESSAGES/django.po +++ b/cookbook/locale/de/LC_MESSAGES/django.po @@ -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- [ ] in list for easier usage in markdown based "
"documents."
@@ -70,51 +82,63 @@ msgstr ""
"Füge - [ ] 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 (/remote."
"php/webdav/ is added automatically)"
@@ -122,119 +146,128 @@ msgstr ""
"Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (/remote.php/"
"webdav/ 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 Ctrl+Space to insert new Ingredient!