mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-27 20:18:58 -05:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51fc05dda2 | ||
|
|
3d695e3d0f | ||
|
|
22b8a4ac18 | ||
|
|
2e26f9c84b | ||
|
|
78105e28c8 | ||
|
|
202b92b156 | ||
|
|
f11f6b7ed1 | ||
|
|
e22d71152b | ||
|
|
2956a34aa6 | ||
|
|
38bfb96b46 | ||
|
|
b6a04b9dbd | ||
|
|
42faafef9f | ||
|
|
40277f9b4f | ||
|
|
9d0a6e63f8 | ||
|
|
6a010587bf | ||
|
|
3fbd2ef032 | ||
|
|
3cb01d6332 | ||
|
|
e2301c0c3a | ||
|
|
4b0164a676 | ||
|
|
49f7afd8d2 | ||
|
|
d90b012601 | ||
|
|
8fcafcc25a | ||
|
|
417e372c42 | ||
|
|
efabed8b2a | ||
|
|
4748eb0b4c | ||
|
|
3428e75b86 | ||
|
|
6c6264ce4d | ||
|
|
dbea9c80da | ||
|
|
3b5dd7e51d | ||
|
|
c985ada3d0 | ||
|
|
a5cc38ecbd | ||
|
|
2a54099187 | ||
|
|
6e4f16275d | ||
|
|
efa1376343 | ||
|
|
a2db9f7265 | ||
|
|
0ee2f77fea | ||
|
|
a4a62af3d2 | ||
|
|
590e083b14 | ||
|
|
ea8e708cb7 | ||
|
|
c2a5f2b2e3 | ||
|
|
f2e4467a32 | ||
|
|
4cc6a98a2b | ||
|
|
647f4c94cb | ||
|
|
1bf5ab2425 | ||
|
|
3945c6bd1b | ||
|
|
a4f715997e | ||
|
|
9446f97ede | ||
|
|
516b6039f7 | ||
|
|
77b1089b7f | ||
|
|
d8efd16763 | ||
|
|
79679c105a | ||
|
|
a0a9a93888 | ||
|
|
82986f21f4 | ||
|
|
88ce2b9495 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -61,6 +61,8 @@ target/
|
||||
|
||||
venv/
|
||||
|
||||
mediafiles/
|
||||
|
||||
*.sqlite3
|
||||
|
||||
\.idea/workspace\.xml
|
||||
|
||||
1
.idea/inspectionProfiles/profiles_settings.xml
generated
1
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,5 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="PROJECT_PROFILE" value="Default" />
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
|
||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="file://$PROJECT_DIR$" libraries="{pretty-checkbox}" />
|
||||
</component>
|
||||
</project>
|
||||
24
Dockerfile
24
Dockerfile
@@ -1,32 +1,24 @@
|
||||
|
||||
FROM alpine
|
||||
|
||||
# Project Files and Settings
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN mkdir /Recipes
|
||||
WORKDIR /Recipes
|
||||
|
||||
ADD . /Recipes/
|
||||
|
||||
RUN apk update
|
||||
RUN apk upgrade
|
||||
RUN apk --no-cache add \
|
||||
RUN apt-get update
|
||||
RUN apt-get -y upgrade
|
||||
RUN apt-get install -y \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
postgresql-client \
|
||||
postgresql-dev \
|
||||
build-base \
|
||||
gettext \
|
||||
libgcrypt-dev libressl-dev curl-dev \
|
||||
libxml2-dev libxslt-dev python-dev
|
||||
gettext
|
||||
|
||||
RUN pip3 install --upgrade pip
|
||||
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
RUN apk del -r python3-dev
|
||||
RUN apt-get autoremove -y
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Server
|
||||
EXPOSE 8080
|
||||
EXPOSE 8080
|
||||
42
README.md
42
README.md
@@ -1,38 +1,40 @@
|
||||
# Recipes
|
||||
Recipes is a django application to manage, tag and search recipes using either built in models or external storage providers hosting PDF's, Images or other files.
|
||||
|
||||

|
||||
|
||||
### Features
|
||||
|
||||
<u>Features</u>
|
||||
- :package: **Sync** files with Dropbox and Nextcloud (more can easily be added)
|
||||
- :mag: Powerful **search** with djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- :label: Create and search for **tags**, assign them in batch to all files matching certain filters
|
||||
- :page_facing_up: **Create recipes** locally within a nice, standardized web interface
|
||||
- :person_with_blond_hair: **Share** recipes with friends and comment on them to suggest or remember changes you made
|
||||
- :whale: Easy setup with **Docker**
|
||||
|
||||
- Sync files with Dropbox and Nextcloud (more can easily be added)
|
||||
- Create and search for tags, assign them in batch to all files matching certain filters
|
||||
- Create recipes locally within a nice, standardized webinterface
|
||||
- Share recipes with friends and comment on them to suggest or remember changes you made
|
||||
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 will be implemented but this is not meant as a public website.
|
||||
# Documentation
|
||||
|
||||
## Usage
|
||||
Most things should be straight forward but there are some more complicated things.
|
||||
##### General
|
||||
Different kinds of objects, like tags or storage backends, can be viewed under the lists tab. This is also were you create
|
||||
new objects by pressing the plus button.
|
||||
Management options for your data, like batch edits and import logs, can be found under `Manage Data`.
|
||||
##### Storage Backends
|
||||
Currently only dropbox is supported as a storage backend. To add a new Dropbox go to `Lists >> Storage Backend` and add a new backend.
|
||||
A `Storage Backend` is a remote storage location where files are stored. To add a new backend klick 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.
|
||||
You can obtain the API token on [Dropboxes API explorer](https://dropbox.github.io/dropbox-api-v2-explorer/#auth_token/from_oauth1)
|
||||
with the button on the top right.
|
||||
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 Path's
|
||||
To add a new path from your Storage backend to the sync list, go to `Manage Data >> Configure Sync` and select the storage backend you want to use.
|
||||
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 `Manage Data >> Configure Sync`.
|
||||
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 Category and Tags while importing.
|
||||
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. For this go to
|
||||
`Manage Data >> Batch Edit`. Enter a word which should be contained in the recipe name and select the tags you want to apply.
|
||||
`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.
|
||||
@@ -40,8 +42,7 @@ When clicking submit every recipe containing the word will be updated (tags are
|
||||
## Installation
|
||||
|
||||
### Docker-Compose
|
||||
A docker-compose file is included in the repository. It is made for setups already running an nginx-reverse proxy network with
|
||||
lets encrypt companion. Copy `.env.template` to `.env` and fill in the missing values accordingly.
|
||||
When cloning this repository a simple docker-compose file is included. It is made for setups already running an nginx-reverse proxy network with lets encrypt companion but can be changed easily. Copy `.env.template` to `.env` and fill in the missing values accordingly.
|
||||
Now simply start the containers and run the `update.sh` script which will apply all migrations and collect static files.
|
||||
Create a default user by executing into the container with `docker-compose exec web_recipes sh` and run `python3 manage.py createsuperuser`.
|
||||
|
||||
@@ -61,6 +62,7 @@ To start developing:
|
||||
5. Start development server with `manage.py runserver`
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull Requests and ideas are welcome, feel free to contribute in any way.
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import django_filters
|
||||
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.db.models import Q
|
||||
from cookbook.forms import MultiSelectWidget
|
||||
from cookbook.models import Recipe, Keyword
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class RecipeFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
name = django_filters.CharFilter(method='filter_name')
|
||||
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
|
||||
method='filter_keywords')
|
||||
|
||||
@@ -17,6 +19,17 @@ class RecipeFilter(django_filters.FilterSet):
|
||||
queryset = queryset.filter(keywords=x)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def filter_name(queryset, name, value):
|
||||
if not name == 'name':
|
||||
return queryset
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
|
||||
queryset = queryset.annotate(similarity=TrigramSimilarity('name', value), ).filter(
|
||||
Q(similarity__gt=0.1) | Q(name__icontains=value)).order_by('-similarity')
|
||||
else:
|
||||
queryset = queryset.filter(name__icontains=value)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ['name', 'keywords']
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext as _
|
||||
from emoji_picker.widgets import EmojiPickerTextInput
|
||||
|
||||
from .models import *
|
||||
|
||||
@@ -10,11 +11,6 @@ class MultiSelectWidget(widgets.SelectMultiple):
|
||||
js = ('custom/js/form_multiselect.js',)
|
||||
|
||||
|
||||
class EmojiWidget(forms.TextInput):
|
||||
class Media:
|
||||
js = ('custom/js/form_emoji.js',)
|
||||
|
||||
|
||||
class ExternalRecipeForm(forms.ModelForm):
|
||||
file_path = forms.CharField(disabled=True, required=False)
|
||||
storage = forms.ModelChoiceField(queryset=Storage.objects.all(), disabled=True, required=False)
|
||||
@@ -22,12 +18,13 @@ class ExternalRecipeForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ('name', 'keywords', 'time', 'file_path', 'storage', 'file_uid')
|
||||
fields = ('name', 'keywords', 'working_time', 'waiting_time', 'file_path', 'storage', 'file_uid')
|
||||
|
||||
labels = {
|
||||
'name': _('Name'),
|
||||
'keywords': _('Keywords'),
|
||||
'time': _('Preparation time in minutes'),
|
||||
'working_time': _('Preparation time in minutes'),
|
||||
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
|
||||
'file_path': _('Path'),
|
||||
'file_uid': _('Storage UID'),
|
||||
}
|
||||
@@ -37,18 +34,21 @@ class ExternalRecipeForm(forms.ModelForm):
|
||||
class InternalRecipeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ('name', 'instructions', 'time', 'keywords')
|
||||
fields = ('name', 'instructions', 'image', 'working_time', 'waiting_time', 'keywords')
|
||||
|
||||
labels = {
|
||||
'name': _('Name'),
|
||||
'keywords': _('Keywords'),
|
||||
'instructions': _('Instructions'),
|
||||
'time': _('Preparation time in minutes'),
|
||||
'working_time': _('Preparation time in minutes'),
|
||||
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
|
||||
}
|
||||
widgets = {'keywords': MultiSelectWidget}
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
prefix = 'comment'
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ('text',)
|
||||
@@ -65,20 +65,41 @@ class KeywordForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Keyword
|
||||
fields = ('name', 'icon', 'description')
|
||||
widgets = {'icon': EmojiWidget}
|
||||
widgets = {'icon': EmojiPickerTextInput}
|
||||
|
||||
|
||||
class StorageForm(forms.ModelForm):
|
||||
username = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password'}), required=False)
|
||||
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
|
||||
required=False)
|
||||
required=False,
|
||||
help_text=_('Leave empty for dropbox and enter app password for nextcloud.'))
|
||||
token = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
|
||||
required=False)
|
||||
required=False,
|
||||
help_text=_('Leave empty for nextcloud and enter api token for dropbox.'))
|
||||
|
||||
class Meta:
|
||||
model = Storage
|
||||
fields = ('name', 'method', 'username', 'password', 'token', 'url')
|
||||
|
||||
help_texts = {
|
||||
'url': _(
|
||||
'Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'),
|
||||
}
|
||||
|
||||
|
||||
class RecipeBookForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = RecipeBook
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class RecipeBookEntryForm(forms.ModelForm):
|
||||
prefix = 'bookmark'
|
||||
|
||||
class Meta:
|
||||
model = RecipeBookEntry
|
||||
fields = ('book',)
|
||||
|
||||
|
||||
class SyncForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-21 14:34+0100\n"
|
||||
"POT-Creation-Date: 2020-01-13 12:08+0100\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,28 +18,32 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: cookbook/forms.py:28 cookbook/forms.py:43 cookbook/forms.py:101
|
||||
#: cookbook/forms.py:24 cookbook/forms.py:40 cookbook/forms.py:122
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: cookbook/forms.py:29 cookbook/forms.py:44 cookbook/forms.py:102
|
||||
#: cookbook/forms.py:25 cookbook/forms.py:41 cookbook/forms.py:123
|
||||
#: cookbook/templates/stats.html:22
|
||||
msgid "Keywords"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
#: cookbook/forms.py:30 cookbook/forms.py:46
|
||||
#: cookbook/forms.py:26 cookbook/forms.py:43
|
||||
msgid "Preparation time in minutes"
|
||||
msgstr "Zubereitungszeit in Minuten"
|
||||
|
||||
#: cookbook/forms.py:31 cookbook/forms.py:103
|
||||
#: cookbook/forms.py:27 cookbook/forms.py:44
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr "Wartezeit (kochen/backen) in Minuten"
|
||||
|
||||
#: cookbook/forms.py:28 cookbook/forms.py:124
|
||||
msgid "Path"
|
||||
msgstr "Pfad"
|
||||
|
||||
#: cookbook/forms.py:32
|
||||
#: cookbook/forms.py:29
|
||||
msgid "Storage UID"
|
||||
msgstr "Speicher ID"
|
||||
|
||||
#: cookbook/forms.py:45
|
||||
#: cookbook/forms.py:42
|
||||
msgid "Instructions"
|
||||
msgstr "Anleitung"
|
||||
|
||||
@@ -47,80 +51,93 @@ msgstr "Anleitung"
|
||||
msgid "Add your comment: "
|
||||
msgstr "Schreibe einen Kommentar:"
|
||||
|
||||
#: cookbook/forms.py:90
|
||||
#: cookbook/forms.py:75
|
||||
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:78
|
||||
msgid "Leave empty for nextcloud and enter api token for dropbox."
|
||||
msgstr "Bei Nextcloud leer lassen, bei Dropbox API Token eingeben"
|
||||
|
||||
#: cookbook/forms.py:86
|
||||
msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
msgstr "Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote."
|
||||
"php/webdav/</code> wird automatisch hinzugefügt)"
|
||||
|
||||
#: cookbook/forms.py:111
|
||||
msgid "Search String"
|
||||
msgstr "Such Wort"
|
||||
|
||||
#: cookbook/forms.py:104
|
||||
#: cookbook/forms.py:125
|
||||
msgid "File ID"
|
||||
msgstr "Datei ID"
|
||||
|
||||
#: cookbook/tables.py:75 cookbook/templates/forms/edit_internal_recipe.html:41
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:78
|
||||
#: cookbook/tables.py:75 cookbook/templates/forms/edit_internal_recipe.html:39
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:98
|
||||
#: 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:48 cookbook/templates/base.html:56
|
||||
#: cookbook/templates/base.html:64 cookbook/templates/base.html:72
|
||||
#: cookbook/templates/index.html:7
|
||||
msgid "Cookbook"
|
||||
msgstr "Kochbuch"
|
||||
|
||||
#: cookbook/templates/base.html:62
|
||||
#: cookbook/templates/base.html:76
|
||||
msgid "Books"
|
||||
msgstr "Bücher"
|
||||
|
||||
#: cookbook/templates/base.html:81
|
||||
msgid "Tags"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
#: cookbook/templates/base.html:66 cookbook/views/edit.py:102
|
||||
#: cookbook/views/edit.py:258 cookbook/views/lists.py:17
|
||||
#: cookbook/views/new.py:43
|
||||
#: cookbook/templates/base.html:85 cookbook/views/edit.py:130
|
||||
#: cookbook/views/edit.py:331 cookbook/views/lists.py:17
|
||||
#: cookbook/views/new.py:44
|
||||
msgid "Keyword"
|
||||
msgstr "Schlagwort"
|
||||
|
||||
#: cookbook/templates/base.html:68
|
||||
#: cookbook/templates/base.html:87
|
||||
msgid "Batch Edit"
|
||||
msgstr "Massenbearbeitung"
|
||||
|
||||
#: cookbook/templates/base.html:73
|
||||
#, fuzzy
|
||||
#| msgid "Manage Data"
|
||||
#: cookbook/templates/base.html:92
|
||||
msgid "Storage Data"
|
||||
msgstr "Daten Verwalten"
|
||||
msgstr "Datenquellen"
|
||||
|
||||
#: cookbook/templates/base.html:77
|
||||
#: cookbook/templates/base.html:96
|
||||
msgid "Storage Backends"
|
||||
msgstr "Speicher Quellen"
|
||||
|
||||
#: cookbook/templates/base.html:79
|
||||
#: cookbook/templates/base.html:98
|
||||
msgid "Configure Sync"
|
||||
msgstr "Sync Einstellen"
|
||||
|
||||
#: cookbook/templates/base.html:81
|
||||
#, fuzzy
|
||||
#| msgid "Import Recipe"
|
||||
#: cookbook/templates/base.html:100
|
||||
msgid "Import Recipes"
|
||||
msgstr "Rezept Importieren"
|
||||
msgstr "Importierte Rezepte"
|
||||
|
||||
#: cookbook/templates/base.html:83 cookbook/views/lists.py:25
|
||||
#, fuzzy
|
||||
#| msgid "Import Recipe"
|
||||
#: cookbook/templates/base.html:102 cookbook/views/lists.py:25
|
||||
msgid "Import Log"
|
||||
msgstr "Rezept Importieren"
|
||||
msgstr "Import Log"
|
||||
|
||||
#: cookbook/templates/base.html:85 cookbook/templates/stats.html:10
|
||||
#: cookbook/templates/base.html:104 cookbook/templates/stats.html:10
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiken"
|
||||
|
||||
#: cookbook/templates/base.html:92
|
||||
#: cookbook/templates/base.html:112
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: cookbook/templates/base.html:96
|
||||
#: cookbook/templates/base.html:116
|
||||
msgid "Logout"
|
||||
msgstr "Ausloggen"
|
||||
|
||||
#: cookbook/templates/base.html:99
|
||||
#: cookbook/templates/base.html:119
|
||||
msgid "Login"
|
||||
msgstr "Einloggen"
|
||||
|
||||
@@ -133,15 +150,12 @@ msgid "Batch edit Recipes"
|
||||
msgstr "Rezept massenbearbeitung"
|
||||
|
||||
#: cookbook/templates/batch/edit.html:20
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Add the specified category and keywords to all recipes containing a word"
|
||||
msgid "Add the specified keywords to all recipes containing a word"
|
||||
msgstr ""
|
||||
"Ausgewählte Kategorie und Schlagwörtern zu allen Rezepten die das Suchwort "
|
||||
"enthalten hinzufügen"
|
||||
"Ausgewählte Schlagwörter zu allen Rezepten die das Suchwort enthalten "
|
||||
"hinzufügen"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:6 cookbook/views/edit.py:86
|
||||
#: cookbook/templates/batch/monitor.html:6 cookbook/views/edit.py:114
|
||||
msgid "Sync"
|
||||
msgstr "Synchronisieren"
|
||||
|
||||
@@ -150,15 +164,11 @@ msgid "Manage watched Folders"
|
||||
msgstr "Überwachte Ordner verwalten"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:14
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "On this Page you can manage all DropBox folder locations that should be "
|
||||
#| "monitored and synced"
|
||||
msgid ""
|
||||
"On this Page you can manage all storage folder locations that should be "
|
||||
"monitored and synced"
|
||||
msgstr ""
|
||||
"Auf dieser Seite kannst du alle Dropbox Ordner verwalten die überwacht und "
|
||||
"Auf dieser Seite kannst du alle Ordner verwalten die überwacht und "
|
||||
"synchronisiert werden sollen"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:16
|
||||
@@ -171,10 +181,8 @@ msgstr "Jetzt Synchronisieren!"
|
||||
|
||||
#: cookbook/templates/batch/waiting.html:4
|
||||
#: cookbook/templates/batch/waiting.html:10
|
||||
#, fuzzy
|
||||
#| msgid "Import Recipe"
|
||||
msgid "Importing Recipes"
|
||||
msgstr "Rezept Importieren"
|
||||
msgstr "Rezept werden importiert"
|
||||
|
||||
#: cookbook/templates/batch/waiting.html:23
|
||||
msgid ""
|
||||
@@ -184,41 +192,62 @@ msgstr ""
|
||||
"Abhängig von der Anzahl der Rezepte kann dieser Vorgang einige Minuten "
|
||||
"dauern, bitte warten."
|
||||
|
||||
#: cookbook/templates/books.html:4 cookbook/templates/books.html:10
|
||||
msgid "Recipe Books"
|
||||
msgstr "Rezept Bücher"
|
||||
|
||||
#: cookbook/templates/books.html:14
|
||||
msgid "New Book"
|
||||
msgstr "Neues Buch"
|
||||
|
||||
#: cookbook/templates/books.html:53
|
||||
msgid "There are no recipes in this book yet."
|
||||
msgstr "In diesem Buch sind bisher keine Rezepte."
|
||||
|
||||
#: cookbook/templates/forms/edit_import_recipe.html:5
|
||||
#: cookbook/templates/forms/edit_import_recipe.html:9
|
||||
#, fuzzy
|
||||
#| msgid "Import Recipe"
|
||||
msgid "Import new Recipe"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:6
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:18
|
||||
#, fuzzy
|
||||
#| msgid "Recipe"
|
||||
msgid "Edit Recipe"
|
||||
msgstr "Rezept"
|
||||
#: cookbook/templates/forms/edit_import_recipe.html:14
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:37
|
||||
#: cookbook/templates/generic/edit_template.html:23
|
||||
#: cookbook/templates/generic/new_template.html:23
|
||||
#: cookbook/templates/recipe_view.html:207
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:28
|
||||
#: cookbook/templates/recipe_view.html:37
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:7
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:16
|
||||
msgid "Edit Recipe"
|
||||
msgstr "Rezept bearbeiten"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:26
|
||||
#: cookbook/templates/recipe_view.html:63
|
||||
msgid "Ingredients"
|
||||
msgstr "Zutaten"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:43
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:41
|
||||
#: cookbook/templates/generic/edit_template.html:27
|
||||
#: cookbook/templates/recipe_view.html:6
|
||||
#: cookbook/templates/recipe_view.html:7
|
||||
msgid "View"
|
||||
msgstr "Angucken"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:70
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:102
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:45
|
||||
#: cookbook/templates/generic/edit_template.html:30
|
||||
msgid "Delete original file"
|
||||
msgstr "Original löschen"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:90
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:127
|
||||
msgid "Ingredient"
|
||||
msgstr "Zutat"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:75
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:95
|
||||
msgid "Amount"
|
||||
msgstr "Menge"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:76
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:96
|
||||
msgid "Unit"
|
||||
msgstr "Einheit"
|
||||
|
||||
@@ -227,6 +256,10 @@ msgstr "Einheit"
|
||||
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"
|
||||
|
||||
#: cookbook/templates/generic/delete_template.html:21
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: cookbook/templates/generic/edit_template.html:6
|
||||
#: cookbook/templates/generic/edit_template.html:14
|
||||
msgid "Edit"
|
||||
@@ -238,14 +271,11 @@ msgid "List"
|
||||
msgstr "Liste"
|
||||
|
||||
#: cookbook/templates/generic/list_template.html:19
|
||||
#, fuzzy
|
||||
#| msgid "Auto import all"
|
||||
msgid "Import all"
|
||||
msgstr "Alle importieren"
|
||||
|
||||
#: cookbook/templates/generic/new_template.html:6
|
||||
#: cookbook/templates/generic/new_template.html:14
|
||||
#: cookbook/templates/index.html:27
|
||||
msgid "New"
|
||||
msgstr "Neu"
|
||||
|
||||
@@ -258,8 +288,8 @@ msgid "next"
|
||||
msgstr "nächste"
|
||||
|
||||
#: cookbook/templates/include/recipe_open_modal.html:28
|
||||
#: cookbook/views/edit.py:207 cookbook/views/edit.py:225
|
||||
#: cookbook/views/new.py:31
|
||||
#: cookbook/views/edit.py:258 cookbook/views/edit.py:278
|
||||
#: cookbook/views/edit.py:298 cookbook/views/new.py:32
|
||||
msgid "Recipe"
|
||||
msgstr "Rezept"
|
||||
|
||||
@@ -267,7 +297,7 @@ msgstr "Rezept"
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: cookbook/templates/include/recipe_open_modal.html:52
|
||||
#: cookbook/templates/include/recipe_open_modal.html:56
|
||||
msgid "Open Recipe"
|
||||
msgstr "Rezept öffnen"
|
||||
|
||||
@@ -283,9 +313,8 @@ msgid ""
|
||||
" This is necessary because they are needed to make API requests, but "
|
||||
"it also increases the risk of\n"
|
||||
" someone stealing it. <br/>\n"
|
||||
" To limit the possible damage use read only tokens or accounts if "
|
||||
"available or create separate accounts\n"
|
||||
" with limited access (only to recipes).\n"
|
||||
" To limit the possible damage tokens or accounts with limited access "
|
||||
"can be used.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
@@ -295,49 +324,51 @@ msgstr ""
|
||||
"anfragen zu machen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/"
|
||||
">\n"
|
||||
" Um das Risiko zu minimieren sollten, wenn möglich, Tokens benutzt "
|
||||
"werden die keinen Schreibzugriff haben. Alternativ können vollständig "
|
||||
"seperate Accounts mit limitiertem Zugriff genutzt werden\n"
|
||||
"oder Accounts mit limitiertem Zugriff verwendet werden.\n"
|
||||
" "
|
||||
|
||||
#: cookbook/templates/index.html:19 cookbook/templates/index.html:24
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
#: cookbook/templates/index.html:21
|
||||
msgid "Search recipe ..."
|
||||
msgstr "Suche Rezept ..."
|
||||
|
||||
#: cookbook/templates/index.html:26
|
||||
msgid "Reset"
|
||||
msgstr "Reset"
|
||||
#: cookbook/templates/index.html:40
|
||||
msgid "Advanced Search"
|
||||
msgstr "Erweiterte Suche"
|
||||
|
||||
#: cookbook/templates/index.html:28
|
||||
msgid "Random"
|
||||
msgstr "Zufällig"
|
||||
|
||||
#: cookbook/templates/index.html:43
|
||||
#: cookbook/templates/index.html:59
|
||||
msgid "Log in to view Recipies"
|
||||
msgstr "Bitte einloggen um Rezepte zu sehen"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:12
|
||||
#: cookbook/templates/recipe_view.html:27
|
||||
msgid "in"
|
||||
msgstr "in"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:17
|
||||
#: cookbook/templates/recipe_view.html:99
|
||||
#: cookbook/templates/recipe_view.html:32
|
||||
#: cookbook/templates/recipe_view.html:174
|
||||
msgid "by"
|
||||
msgstr "von"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:29
|
||||
#: cookbook/templates/recipe_view.html:43
|
||||
msgid "Preparation time ca."
|
||||
msgstr "Zubereitungszeit ca."
|
||||
|
||||
#: cookbook/templates/recipe_view.html:55
|
||||
#: cookbook/templates/recipe_view.html:48
|
||||
msgid "Waiting time ca."
|
||||
msgstr "Zubereitungszeit ca."
|
||||
|
||||
#: cookbook/templates/recipe_view.html:110
|
||||
msgid "Recipe Image"
|
||||
msgstr "Rezept Bild"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:126
|
||||
msgid "View external recipe"
|
||||
msgstr "Externes Rezept ansehen"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:65
|
||||
#, fuzzy
|
||||
#| msgid "Open Recipe"
|
||||
#: cookbook/templates/recipe_view.html:137
|
||||
msgid "External recipe"
|
||||
msgstr "Rezept öffnen"
|
||||
msgstr "Externes Rezept"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:67
|
||||
#: cookbook/templates/recipe_view.html:139
|
||||
msgid ""
|
||||
"\n"
|
||||
" This is an external recipe, which means you can only "
|
||||
@@ -352,18 +383,19 @@ msgstr ""
|
||||
"nur durch klicken auf den link geöffnet werden kann.\n"
|
||||
" Das Rezept kann durch drücken des Umwandeln Knopfes "
|
||||
"in ein schickes lokales Rezept verwandelt werden. Die originale Datei "
|
||||
"bleibt weiterhin verfügbar\n"
|
||||
"bleibt weiterhin verfügbar.\n"
|
||||
" "
|
||||
|
||||
#: cookbook/templates/recipe_view.html:75
|
||||
#: cookbook/templates/recipe_view.html:147
|
||||
msgid "Convert now!"
|
||||
msgstr "Jetzt umwandeln!"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:84
|
||||
#: cookbook/templates/recipe_view.html:156
|
||||
msgid "Comments"
|
||||
msgstr "Kommentare"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:91 cookbook/views/edit.py:168
|
||||
#: cookbook/views/edit.py:280
|
||||
#: cookbook/templates/recipe_view.html:165 cookbook/views/edit.py:191
|
||||
#: cookbook/views/edit.py:353
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
@@ -380,14 +412,10 @@ msgid "Number of objects"
|
||||
msgstr "Anzahl der Objekte"
|
||||
|
||||
#: cookbook/templates/stats.html:20
|
||||
#, fuzzy
|
||||
#| msgid "Recipe"
|
||||
msgid "Recipes"
|
||||
msgstr "Rezepte"
|
||||
|
||||
#: cookbook/templates/stats.html:24
|
||||
#, fuzzy
|
||||
#| msgid "Recipe"
|
||||
msgid "Recipe Imports"
|
||||
msgstr "Rezept Importe"
|
||||
|
||||
@@ -399,91 +427,81 @@ msgstr "Objekt Statistiken"
|
||||
msgid "Recipes without Keywords"
|
||||
msgstr "Rezepte ohne Schlagwort"
|
||||
|
||||
#: cookbook/views/api.py:48
|
||||
#: cookbook/views/api.py:63
|
||||
msgid "Sync successful!"
|
||||
msgstr "Synchronisation erfolgreich!"
|
||||
|
||||
#: cookbook/views/api.py:51
|
||||
#: cookbook/views/api.py:66
|
||||
msgid "Error synchronizing with Storage"
|
||||
msgstr "Fehler beim Synchronisieren"
|
||||
|
||||
#: cookbook/views/data.py:71
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Batch edit done. %(count)d recipe where updated."
|
||||
#| msgid_plural "Batch edit done. %(count)d Recipes where updated."
|
||||
#, python-format
|
||||
msgid "Batch edit done. %(count)d recipe was updated."
|
||||
msgid_plural "Batch edit done. %(count)d Recipes where updated."
|
||||
msgstr[0] "Massenbearbeitung erfolgreich. %(count)d Rezept wurde aktualisiert."
|
||||
msgstr[1] "Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
|
||||
msgstr[1] ""
|
||||
"Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
|
||||
|
||||
#: cookbook/views/edit.py:61
|
||||
#, fuzzy
|
||||
#| msgid "Recipe"
|
||||
#: cookbook/views/edit.py:88
|
||||
msgid "Recipe saved!"
|
||||
msgstr "Rezept"
|
||||
msgstr "Rezept gespeichert"
|
||||
|
||||
#: cookbook/views/edit.py:64 cookbook/views/new.py:80
|
||||
#: cookbook/views/edit.py:91 cookbook/views/new.py:87
|
||||
msgid "There was an error importing this recipe!"
|
||||
msgstr "Beim importieren des Rezeptes ist ein Fehler aufgetreten"
|
||||
|
||||
#: cookbook/views/edit.py:118 cookbook/views/edit.py:269
|
||||
#: cookbook/views/lists.py:42 cookbook/views/new.py:55
|
||||
msgid "Storage Backend"
|
||||
msgstr "Speicher Quelle"
|
||||
#: cookbook/views/edit.py:139 cookbook/views/edit.py:182
|
||||
msgid "You cannot edit this comment!"
|
||||
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
|
||||
|
||||
#: cookbook/views/edit.py:142
|
||||
#, fuzzy
|
||||
#| msgid "Changes saved!"
|
||||
#: cookbook/views/edit.py:158
|
||||
msgid "Storage saved!"
|
||||
msgstr "Änderungen gespeichert"
|
||||
msgstr "Speicherquelle gespeichert"
|
||||
|
||||
#: cookbook/views/edit.py:145
|
||||
#: cookbook/views/edit.py:161
|
||||
msgid "There was an error updating this storage backend.!"
|
||||
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle"
|
||||
|
||||
#: cookbook/views/edit.py:185 cookbook/views/edit.py:236
|
||||
#: cookbook/views/edit.py:208 cookbook/views/edit.py:309
|
||||
#: cookbook/views/lists.py:34
|
||||
#, fuzzy
|
||||
#| msgid "Import Recipe"
|
||||
msgid "Import"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: cookbook/views/edit.py:195
|
||||
#: cookbook/views/edit.py:224 cookbook/views/edit.py:364
|
||||
#: cookbook/views/new.py:110
|
||||
msgid "Recipe Book"
|
||||
msgstr "Rezeptbuch"
|
||||
|
||||
#: cookbook/views/edit.py:246
|
||||
msgid "Changes saved!"
|
||||
msgstr "Änderungen gespeichert"
|
||||
|
||||
#: cookbook/views/edit.py:199
|
||||
#: cookbook/views/edit.py:250
|
||||
msgid "Error saving changes!"
|
||||
msgstr "Fehler beim Speichern der Daten."
|
||||
|
||||
#: cookbook/views/edit.py:247
|
||||
#: cookbook/views/edit.py:320
|
||||
msgid "Monitor"
|
||||
msgstr "Monitor"
|
||||
|
||||
#: cookbook/views/new.py:77
|
||||
#, fuzzy
|
||||
#| msgid "Imported Recipes"
|
||||
#: cookbook/views/edit.py:342 cookbook/views/lists.py:42
|
||||
#: cookbook/views/new.py:62
|
||||
msgid "Storage Backend"
|
||||
msgstr "Speicher Quelle"
|
||||
|
||||
#: cookbook/views/edit.py:375
|
||||
msgid "Bookmarks"
|
||||
msgstr "Lesezeichen"
|
||||
|
||||
#: cookbook/views/new.py:84
|
||||
msgid "Imported new recipe!"
|
||||
msgstr "Importierte Rezepte"
|
||||
msgstr "Importier neue Rezepte"
|
||||
|
||||
#: cookbook/views/views.py:42
|
||||
#, fuzzy
|
||||
#| msgid "Changes saved!"
|
||||
msgid "Comment saved!"
|
||||
msgstr "Änderungen gespeichert"
|
||||
msgstr "Kommentar gespeichert"
|
||||
|
||||
#: cookbook/views/views.py:45
|
||||
msgid "There was an error saving this comment!"
|
||||
msgstr "Es gab einen Fehler beim speichern dieses Kommentars"
|
||||
|
||||
#~ msgid "Category"
|
||||
#~ msgstr "Kategorie"
|
||||
|
||||
#~ msgid "Save"
|
||||
#~ msgstr "Speichern"
|
||||
|
||||
#~ msgid "Update"
|
||||
#~ msgstr "Aktualisieren"
|
||||
|
||||
#~ msgid "Manage imported Recipes"
|
||||
#~ msgstr "Importe verwalten"
|
||||
#: cookbook/views/views.py:52
|
||||
msgid "Bookmark saved!"
|
||||
msgstr "Lesezeichen gespeichert"
|
||||
|
||||
12
cookbook/migrations/0003_enable_pgtrm.py
Normal file
12
cookbook/migrations/0003_enable_pgtrm.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.contrib.postgres.operations import TrigramExtension
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0002_auto_20191119_2035'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
TrigramExtension(),
|
||||
]
|
||||
22
cookbook/migrations/0004_storage_created_by.py
Normal file
22
cookbook/migrations/0004_storage_created_by.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.0 on 2019-12-09 10:30
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0003_enable_pgtrm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='storage',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
32
cookbook/migrations/0005_recipebook_recipebookentry.py
Normal file
32
cookbook/migrations/0005_recipebook_recipebookentry.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 2.2.9 on 2019-12-24 11:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0004_storage_created_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RecipeBook',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeBookEntry',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.RecipeBook')),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0006_recipe_image.py
Normal file
18
cookbook/migrations/0006_recipe_image.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.1 on 2019-12-25 15:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0005_recipebook_recipebookentry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='recipe',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='recipes/'),
|
||||
),
|
||||
]
|
||||
23
cookbook/migrations/0007_auto_20191226_0852.py
Normal file
23
cookbook/migrations/0007_auto_20191226_0852.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.1 on 2019-12-26 07:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0006_recipe_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='recipe',
|
||||
old_name='time',
|
||||
new_name='working_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipe',
|
||||
name='waiting_time',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
@@ -13,6 +14,7 @@ class Storage(models.Model):
|
||||
password = models.CharField(max_length=128, blank=True, null=True)
|
||||
token = models.CharField(max_length=512, blank=True, null=True)
|
||||
url = models.URLField(blank=True, null=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -52,12 +54,14 @@ class Keyword(models.Model):
|
||||
class Recipe(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
instructions = models.TextField(blank=True)
|
||||
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
|
||||
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
|
||||
file_uid = models.CharField(max_length=256, default="")
|
||||
file_path = models.CharField(max_length=512, default="")
|
||||
link = models.CharField(max_length=512, default="")
|
||||
keywords = models.ManyToManyField(Keyword, blank=True)
|
||||
time = models.IntegerField(default=0)
|
||||
working_time = models.IntegerField(default=0)
|
||||
waiting_time = models.IntegerField(default=0)
|
||||
internal = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -95,3 +99,19 @@ class RecipeImport(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeBook(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeBookEntry(models.Model):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.recipe.name
|
||||
|
||||
@@ -27,14 +27,15 @@ class Dropbox(Provider):
|
||||
try:
|
||||
recipes = r.json()
|
||||
except ValueError:
|
||||
log_entry = SyncLog(status='ERROR', msg=str(r), monitor=monitor)
|
||||
log_entry = SyncLog(status='ERROR', msg=str(r), sync=monitor)
|
||||
log_entry.save()
|
||||
return r
|
||||
|
||||
import_count = 0
|
||||
for recipe in recipes['entries']: # TODO check if has_more is set and import that as well
|
||||
path = recipe['path_lower']
|
||||
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(file_path=path).exists():
|
||||
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(
|
||||
file_path=path).exists():
|
||||
name = os.path.splitext(recipe['name'])[0]
|
||||
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage, file_uid=recipe['id'])
|
||||
new_recipe.save()
|
||||
@@ -75,7 +76,7 @@ class Dropbox(Provider):
|
||||
}
|
||||
|
||||
data = {
|
||||
"path": recipe.file_uid
|
||||
"path": recipe.file_path,
|
||||
}
|
||||
|
||||
r = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
@@ -86,3 +87,38 @@ class Dropbox(Provider):
|
||||
|
||||
response = Dropbox.create_share_link(recipe)
|
||||
return response['url']
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
url = "https://api.dropboxapi.com/2/files/move_v2"
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + recipe.storage.token,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"from_path": recipe.file_path,
|
||||
"to_path": os.path.dirname(recipe.file_path) + '/' + new_name + os.path.splitext(recipe.file_path)[1]
|
||||
}
|
||||
|
||||
r = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
|
||||
return r.json()
|
||||
|
||||
@staticmethod
|
||||
def delete_file(recipe):
|
||||
url = "https://api.dropboxapi.com/2/files/delete_v2"
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + recipe.storage.token,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"path": recipe.file_path
|
||||
}
|
||||
|
||||
r = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
|
||||
return r.json()
|
||||
|
||||
@@ -12,13 +12,17 @@ from cookbook.provider.provider import Provider
|
||||
class Nextcloud(Provider):
|
||||
|
||||
@staticmethod
|
||||
def import_all(monitor):
|
||||
def get_client(storage):
|
||||
options = {
|
||||
'webdav_hostname': monitor.storage.url + '/remote.php/dav/files/' + monitor.storage.username,
|
||||
'webdav_login': monitor.storage.username,
|
||||
'webdav_password': monitor.storage.password
|
||||
'webdav_hostname': storage.url + '/remote.php/dav/files/' + storage.username,
|
||||
'webdav_login': storage.username,
|
||||
'webdav_password': storage.password
|
||||
}
|
||||
client = wc.Client(options)
|
||||
return wc.Client(options)
|
||||
|
||||
@staticmethod
|
||||
def import_all(monitor):
|
||||
client = Nextcloud.get_client(monitor.storage)
|
||||
|
||||
files = client.list(monitor.path)
|
||||
files.pop(0) # remove first element because its the folder itself
|
||||
@@ -26,7 +30,8 @@ class Nextcloud(Provider):
|
||||
import_count = 0
|
||||
for file in files:
|
||||
path = monitor.path + '/' + file
|
||||
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(file_path=path).exists():
|
||||
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(
|
||||
file_path=path).exists():
|
||||
name = os.path.splitext(file)[0]
|
||||
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage)
|
||||
new_recipe.save()
|
||||
@@ -51,7 +56,8 @@ class Nextcloud(Provider):
|
||||
|
||||
data = {'path': recipe.file_path, 'shareType': 3}
|
||||
|
||||
r = requests.post(url, headers=headers, auth=HTTPBasicAuth(recipe.storage.username, recipe.storage.password), data=data)
|
||||
r = requests.post(url, headers=headers, auth=HTTPBasicAuth(recipe.storage.username, recipe.storage.password),
|
||||
data=data)
|
||||
|
||||
response_json = r.json()
|
||||
|
||||
@@ -74,3 +80,20 @@ class Nextcloud(Provider):
|
||||
return element['url']
|
||||
|
||||
return Nextcloud.create_share_link(recipe)
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
client = Nextcloud.get_client(recipe.storage)
|
||||
|
||||
client.move(recipe.file_path,
|
||||
os.path.dirname(recipe.file_path) + '/' + new_name + os.path.splitext(recipe.file_path)[1])
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def delete_file(recipe):
|
||||
client = Nextcloud.get_client(recipe.storage)
|
||||
|
||||
client.clean(recipe.file_path)
|
||||
|
||||
return True
|
||||
|
||||
@@ -10,3 +10,11 @@ class Provider:
|
||||
@staticmethod
|
||||
def get_share_link(recipe):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
@staticmethod
|
||||
def delete_file(recipe, new_name):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
BIN
cookbook/static/favicon.png
Normal file
BIN
cookbook/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
1
cookbook/static/favicon.svg
Normal file
1
cookbook/static/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M128 152v-32c0-4.4 3.6-8 8-8h208c4.4 0 8 3.6 8 8v32c0 4.4-3.6 8-8 8H136c-4.4 0-8-3.6-8-8zm8 88h208c4.4 0 8-3.6 8-8v-32c0-4.4-3.6-8-8-8H136c-4.4 0-8 3.6-8 8v32c0 4.4 3.6 8 8 8zm299.1 159.7c-4.2 13-4.2 51.6 0 64.6 7.3 1.4 12.9 7.9 12.9 15.7v16c0 8.8-7.2 16-16 16H80c-44.2 0-80-35.8-80-80V80C0 35.8 35.8 0 80 0h352c8.8 0 16 7.2 16 16v368c0 7.8-5.5 14.2-12.9 15.7zm-41.1.3H80c-17.6 0-32 14.4-32 32 0 17.7 14.3 32 32 32h314c-2.7-17.3-2.7-46.7 0-64zm6-352H80c-17.7 0-32 14.3-32 32v278.7c9.8-4.3 20.6-6.7 32-6.7h320V48z"></path></svg>
|
||||
|
After Width: | Height: | Size: 740 B |
11
cookbook/static/tabulator/tabulator.min.js
vendored
Normal file
11
cookbook/static/tabulator/tabulator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
cookbook/static/tabulator/tabulator_bootstrap4.min.css
vendored
Normal file
3
cookbook/static/tabulator/tabulator_bootstrap4.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<html>
|
||||
@@ -8,27 +8,43 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
|
||||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{% static 'favicon.png' %}">
|
||||
<link rel="shortcut icon" href="{% static 'favicon.png' %}">
|
||||
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}" sizes="96x96">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon.png' %}">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||
|
||||
<!-- Bootstrap 4 -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
|
||||
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
|
||||
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
|
||||
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
|
||||
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
|
||||
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
|
||||
<!-- Select2 for use with django autocomplete light -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/css/select2.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/js/select2.min.js"></script>
|
||||
|
||||
<!-- Bootstrap theme for select2 -->
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/select2-bootstrap-theme/0.1.0-beta.10/select2-bootstrap.css"
|
||||
integrity="sha256-zFnNbsU+u3l0K+MaY92RvJI6AdAVAxK3/QrBApHvlH8=" crossorigin="anonymous"/>
|
||||
<script type="text/javascript">
|
||||
$.fn.select2.defaults.set("theme", "bootstrap");
|
||||
</script>
|
||||
|
||||
<!-- Fontawesome icons -->
|
||||
<link rel="stylesheet" href="{% static "fontawesome/fontawesome_all.min.css" %}">
|
||||
|
||||
<link rel="stylesheet" href="{% static "emojionearea/emojionearea.min.css" %}">
|
||||
<script type="text/javascript" src="{% static "emojionearea/emojionearea.min.js" %}"></script>
|
||||
|
||||
{% block extra_head %} <!-- block for templates to put stuff into header -->
|
||||
{% endblock %}
|
||||
|
||||
@@ -56,6 +72,9 @@
|
||||
<a class="nav-link" href="{% url 'index' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'view_books' %}"><i class="fas fa-bookmark"></i> {% trans 'Books' %}</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
@@ -89,7 +108,8 @@
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-shield"></i> {% trans 'Admin' %}</a>
|
||||
<a class="nav-link" href="{% url 'admin:index' %}"><i
|
||||
class="fas fa-user-shield"></i> {% trans 'Admin' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
62
cookbook/templates/books.html
Normal file
62
cookbook/templates/books.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Recipe Books' %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
<h2>{% trans 'Recipe Books' %}</h2>
|
||||
</div>
|
||||
<div class="col col-md-3" style="text-align: right">
|
||||
<a href="{% url 'new_book' %}" class="btn btn-success"><i
|
||||
class="fas fa-plus-circle"></i> {% trans 'New Book' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
{% for b in book_list %}
|
||||
<div class="row">
|
||||
<div class="col col-md-10">
|
||||
<a data-toggle="collapse" href="#collapse_{{ b.book.pk }}" role="button" aria-expanded="false"
|
||||
aria-controls="collapse_{{ b.book.pk }}"><h4>{{ b.book.name }}</h4></a>
|
||||
</div>
|
||||
<div class="col col-md-2" style="text-align: right">
|
||||
<h4>
|
||||
|
||||
<a href="{% url 'edit_recipe_book' b.book.pk %}"> <i class="fas fa-pencil-alt"></i></a>
|
||||
<a href="{% url 'delete_recipe_book' b.book.pk %}"><i class="fas fa-trash-alt"></i></a>
|
||||
</h4>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="collapse" id="collapse_{{ b.book.pk }}">
|
||||
{% if b.recipes %}
|
||||
<ul>
|
||||
{% for r in b.recipes %}
|
||||
<div class="row">
|
||||
<div class="col col-md-10">
|
||||
<li><a href="#" onClick='openRecipe({{ r.recipe.pk }})'>{{ r.recipe.name }}</a></li>
|
||||
</div>
|
||||
<div class="col col-md-2" style="text-align: right">
|
||||
<a href="{% url 'delete_recipe_book_entry' r.pk %}"><i class="fas fa-trash-alt"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% trans 'There are no recipes in this book yet.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'include/recipe_open_modal.html' %}
|
||||
{% endblock %}
|
||||
@@ -11,7 +11,7 @@
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<input type="submit" value="Submit" class="btn btn-success">
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -2,22 +2,20 @@
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans 'Edit Recipe' %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.4.3/js/tabulator.min.js"
|
||||
integrity="sha256-u2YCVBkzzkIuLh6bMHUmqv6uuuHLxGgc6XF+rCJUV5k=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.4.3/css/bootstrap/tabulator_bootstrap4.min.css"
|
||||
integrity="sha256-+AmauyGZPl0HNTBQ5AMZBxfzP+rzXJjraezMKpWwWSE=" crossorigin="anonymous"/>
|
||||
<script src="{% static 'tabulator/tabulator.min.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'tabulator/tabulator_bootstrap4.min.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{% trans 'Edit Recipe' %}</h3>
|
||||
|
||||
<form action="." method="post">
|
||||
<form action="." method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
@@ -36,15 +34,36 @@
|
||||
|
||||
<input type="hidden" id="ingredients_data_input" name="ingredients">
|
||||
<hr>
|
||||
<input type="submit" value="Submit" class="btn btn-success">
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
<a href="{% url 'redirect_delete' form.instance|get_class|lower form.instance.pk %}"
|
||||
class="btn btn-danger">{% trans 'Delete' %}</a>
|
||||
class="btn btn-danger"><i class="fas fa-trash-alt"></i> {% trans 'Delete' %}</a>
|
||||
{% if view_url %}
|
||||
<a href="{{ view_url }}" class="btn btn-info">{% trans 'View' %} <i class="far fa-eye"></i></a>
|
||||
<a href="{{ view_url }}" class="btn btn-info"><i class="far fa-eye"></i> {% trans 'View' %}</a>
|
||||
{% endif %}
|
||||
{% if form.instance.storage %}
|
||||
<a href="{% url 'delete_recipe_source' form.instance.pk %}" class="btn btn-warning"><i
|
||||
class="fas fa-exclamation-triangle"></i> {% trans 'Delete original file' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function selectText(node) {
|
||||
|
||||
if (document.body.createTextRange) {
|
||||
const range = document.body.createTextRange();
|
||||
range.moveToElementText(node);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.");
|
||||
}
|
||||
}
|
||||
|
||||
//converts multiselct in recipe edit to searchable multiselect
|
||||
//shitty solution that needs to be redone at some point
|
||||
$(document).ready(function () {
|
||||
@@ -66,6 +85,7 @@
|
||||
movableRows: true,
|
||||
headerSort: false,
|
||||
columns: [
|
||||
{ title: "<i class='fas fa-sort'></i>", rowHandle:true, formatter:"handle", headerSort:false, frozen:true, width:36, minWidth:36},
|
||||
{
|
||||
title: "{% trans 'Ingredient' %}",
|
||||
field: "name",
|
||||
@@ -91,7 +111,12 @@
|
||||
table.deleteRow(cur.id);
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cellClick: function (e, cell) {
|
||||
input = cell.getElement().childNodes[0]
|
||||
input.focus()
|
||||
input.select()
|
||||
},
|
||||
});
|
||||
|
||||
// load initial value
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% blocktrans %}Are you sure you want to delete the {{ title }}: <b>{{ object }}</b> {% endblocktrans %}
|
||||
</div>
|
||||
{{ form|crispy }}
|
||||
<input type="submit" value="Submit" class="btn btn-success">
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-trash-alt"></i> {% trans 'Confirm' %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -20,11 +20,14 @@
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<input type="submit" value="Submit" class="btn btn-success">
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
<a href="{% url 'redirect_delete' form.instance|get_class|lower form.instance.pk %}"
|
||||
class="btn btn-danger">{% trans 'Delete' %}</a>
|
||||
{% if view_url %}
|
||||
<a href="{{ view_url }}" class="btn btn-info">{% trans 'View' %} <i class="far fa-eye"></i></a>
|
||||
<a href="{{ view_url }}" class="btn btn-info"><i class="far fa-eye"></i> {% trans 'View' %}</a>
|
||||
{% endif %}
|
||||
{% if delete_external_url %}
|
||||
<a href="{{ delete_external_url }}" class="btn btn-warning"><i class="fas fa-exclamation-triangle"></i> {% trans 'Delete original file' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<input type="submit" value="Submit" class="btn btn-success">
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -43,11 +43,15 @@
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function openRecipe(id) {
|
||||
function openRecipe(id, force_external = false) {
|
||||
var link = $('#a_recipe_open');
|
||||
link.hide();
|
||||
$('#div_loader').show();
|
||||
|
||||
var url = "{% url 'api_get_file_link' recipe_id=12345 %}".replace(/12345/, id);
|
||||
if (force_external) {
|
||||
url = "{% url 'api_get_external_file_link' recipe_id=12345 %}".replace(/12345/, id);
|
||||
}
|
||||
|
||||
link.text("{% trans 'Open Recipe' %}");
|
||||
$('#modal_recipe').modal('show');
|
||||
@@ -55,10 +59,14 @@
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
window.open(this.responseText);
|
||||
$('#modal_recipe').modal('hide');
|
||||
//link.attr("href", this.responseText);
|
||||
//link.show();
|
||||
if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
|
||||
link.attr("href", this.responseText);
|
||||
link.show();
|
||||
} else {
|
||||
window.open(this.responseText);
|
||||
$('#modal_recipe').modal('hide');
|
||||
}
|
||||
|
||||
$('#div_loader').hide();
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.
|
||||
This is necessary because they are needed to make API requests, but it also increases the risk of
|
||||
someone stealing it. <br/>
|
||||
To limit the possible damage use read only tokens or accounts if available or create separate accounts
|
||||
with limited access (only to recipes).
|
||||
To limit the possible damage tokens or accounts with limited access can be used.
|
||||
{% endblocktrans %}</p>
|
||||
</div>
|
||||
@@ -11,29 +11,45 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col md-12">
|
||||
{% if filter %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-search"></i> {% trans "Search" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="" method="get" id="search_form">
|
||||
{{ filter.form|crispy }}
|
||||
<button class="btn btn-primary" value="1"><i class="fas fa-search"></i> {% trans 'Search' %}</button>
|
||||
<a href="#" onclick="window.location = window.location.pathname;"
|
||||
class="btn btn-warning"><i class="fas fa-backspace"></i> {% trans 'Reset' %}</a>
|
||||
<a href="{% url 'new_recipe' %}" class="btn btn-success"><i class="fas fa-plus-circle"></i> {% trans 'New' %}</a>
|
||||
<a href="" class="btn btn-info"><i class="fas fa-dice"></i> {% trans 'Random' %}</a>
|
||||
</form>
|
||||
{% if filter %}
|
||||
<form action="" method="get" id="search_form">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div class="row">
|
||||
<div class="col md-12">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="{% trans 'Search recipe ...' %}"
|
||||
id="{{ filter.form.name.id_for_label }}" name="{{ filter.form.name.name }}"
|
||||
aria-describedby="button-addon4">
|
||||
<div class="input-group-append" id="button-addon4">
|
||||
<button class="btn btn-primary" type="submit"><i class="fas fa-search"></i></button>
|
||||
<button class="btn btn-warning" type="button" onclick="window.location = window.location.pathname;"><i class="fas fa-backspace"></i></button>
|
||||
<button class="btn btn-success" type="button" onclick="location.href='{% url 'new_recipe' %}'"><i class="fas fa-plus-circle"></i></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-2 offset-md-10" style="text-align: right">
|
||||
<a class="" data-toggle="collapse" href="#collapse_adv_search" role="button"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapse_adv_search"><i
|
||||
class="fas fa-search"></i> {% trans 'Advanced Search' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="collapse col-md-12" id="collapse_adv_search">
|
||||
<div>
|
||||
{{ filter.form.keywords | as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<br/>
|
||||
|
||||
{% if user.is_authenticated and recipes %}
|
||||
|
||||
@@ -1,54 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}{% trans 'View' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pretty-checkbox@3.0/dist/pretty-checkbox.min.css"
|
||||
integrity="sha384-ICB8i/maQ/5+tGLDUEcswB7Ch+OO9Oj8Z4Ov/Gs0gxqfTgLLkD3F43MhcEJ2x6/D" crossorigin="anonymous">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
<h3>{{ recipe.name }} <a href="{% url 'edit_recipe' recipe.pk %}" class="d-print-none"><i
|
||||
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
|
||||
class="fas fa-bookmark"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{{ recipe.name }} <a href="{% url 'edit_recipe' recipe.pk %}"><i class="fas fa-pencil-alt"></i></a></h3>
|
||||
{% if recipe.storage %}
|
||||
<small>{% trans 'in' %} <a
|
||||
href="{% url 'edit_storage' recipe.storage.pk %}">{{ recipe.storage.name }}</a></small><br/>
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.internal %}
|
||||
<small>{% trans 'by' %} {{ recipe.created_by.username }}</small><br/>
|
||||
<small>{% trans 'by' %} {{ recipe.created_by.username }}<br/></small>
|
||||
<br/>
|
||||
{% endif %}
|
||||
|
||||
<br/>
|
||||
|
||||
{% if recipe.all_tags %}
|
||||
{{ recipe.all_tags }}
|
||||
<br/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.time and recipe.time != 0 %}
|
||||
<small>{% trans 'Preparation time ca.' %} {{ recipe.time }} min </small>
|
||||
{% if recipe.working_time and recipe.working_time != 0 %}
|
||||
<span class="badge badge-secondary">{% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.waiting_time and recipe.waiting_time != 0 %}
|
||||
<span
|
||||
class="badge badge-secondary">{% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.waiting_time and recipe.waiting_time != 0 or recipe.working_time and recipe.working_time != 0 %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
|
||||
{% if ingredients %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="row">
|
||||
{% if ingredients %}
|
||||
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{% trans 'Ingredients' %}</h5>
|
||||
<div class="row">
|
||||
<div class="col col-md-9">
|
||||
<h4 class="card-title">{% trans 'Ingredients' %}</h4>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
|
||||
<div class="input-group d-print-none">
|
||||
<input type="number" value="1" maxlength="3" class="form-control" id="in_factor"
|
||||
onchange="reloadIngredients()"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-calculator"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<table class="">
|
||||
{% for i in ingredients %}
|
||||
<tr>
|
||||
<td>{{ i.amount.normalize }} {{ i.unit }}</td>
|
||||
<td style="padding-left: 8px">{{ i.name }}</td>
|
||||
<td style="font-size: large">
|
||||
<div class="pretty p-default p-curve">
|
||||
<input type="checkbox"/>
|
||||
<div class="state p-success">
|
||||
<label>
|
||||
{% if i.amount != 0 %}
|
||||
<span id="ing_{{ i.pk }}">{{ i.amount.normalize }}</span>
|
||||
{{ i.unit }}
|
||||
{% else %}
|
||||
<span>⁣</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td style="font-size: large">{{ i.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if recipe.image %}
|
||||
<div class="col-md-6 order-md-2 col-sm-12 order-sm-1 col-12 order-1 " style="text-align: center">
|
||||
<img class="img img-fluid rounded" src="{{ recipe.image.url }}" style="max-height: 30vh;"
|
||||
alt="{% trans 'Recipe Image' %}">
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if recipe.ingredients or recipe.image %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
@@ -58,8 +123,9 @@
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.storage %}
|
||||
<a href='#' onClick='openRecipe({{ recipe.id }})'>{% trans 'View external recipe' %} <i
|
||||
class="fas fa-external-link-alt"></i></a>
|
||||
<a href='#' onClick='openRecipe({{ recipe.id }}, true)' class="d-print-none">{% trans 'View external recipe' %}
|
||||
<i
|
||||
class="fas fa-external-link-alt"></i></a>
|
||||
{% endif %}
|
||||
|
||||
{% if not recipe.internal %}
|
||||
@@ -88,22 +154,25 @@
|
||||
<br/>
|
||||
|
||||
<h5>{% trans 'Comments' %}</h5>
|
||||
<div class="d-print-none">
|
||||
|
||||
<form method="POST" class="post-form">
|
||||
{% csrf_token %}
|
||||
<div class="input-group mb-3">
|
||||
<textarea name="text" cols="15" rows="2" class="textarea form-control" required id="id_text"></textarea>
|
||||
<div class="input-group-append">
|
||||
<input type="submit" value="{% trans 'Comment' %}" class="btn btn-success">
|
||||
<form method="POST" class="post-form">
|
||||
{% csrf_token %}
|
||||
<div class="input-group mb-3">
|
||||
<textarea name="comment-text" cols="15" rows="2" class="textarea form-control" required
|
||||
id="comment-id_text"></textarea>
|
||||
<div class="input-group-append">
|
||||
<input type="submit" value="{% trans 'Comment' %}" class="btn btn-success">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for c in comments %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
|
||||
href="{% url 'edit_comment' c.pk %}"><i class="fas fa-pencil-alt"></i></a><br/>
|
||||
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i class="fas fa-pencil-alt"></i></a><br/>
|
||||
{{ c.text }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,4 +182,49 @@
|
||||
{% if recipe.storage %}
|
||||
{% include 'include/recipe_open_modal.html' %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Bookmark Modal -->
|
||||
<div class="modal fade" id="bookmarkModal" tabindex="-1" role="dialog" aria-labelledby="bookmarkModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="bookmarkModalLabel">Modal title</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form method="POST" class="post-form">
|
||||
<div class="modal-body">
|
||||
|
||||
{% csrf_token %}
|
||||
{{ bookmark_form|crispy }}
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<input type="submit" value="{% trans 'Save' %}" class="btn btn-success">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function reloadIngredients() {
|
||||
factor = Number($('#in_factor').val());
|
||||
ingredients = {
|
||||
{% for i in ingredients %}
|
||||
{{ i.pk }}: {{ i.amount|unlocalize }},
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
for (var key in ingredients) {
|
||||
$('#ing_' + key).html(Math.round(ingredients[key] * factor))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@ from cookbook.helper import dal
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('test', views.test, name='test'),
|
||||
path('books', views.books, name='view_books'),
|
||||
|
||||
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
|
||||
|
||||
@@ -14,6 +14,7 @@ urlpatterns = [
|
||||
path('new/recipe_import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
|
||||
path('new/keyword/', new.KeywordCreate.as_view(), name='new_keyword'),
|
||||
path('new/storage/', new.StorageCreate.as_view(), name='new_storage'),
|
||||
path('new/book/', new.RecipeBookCreate.as_view(), name='new_book'),
|
||||
|
||||
path('list/keyword', lists.keyword, name='list_keyword'),
|
||||
path('list/import_log', lists.sync_log, name='list_import_log'),
|
||||
@@ -21,24 +22,30 @@ urlpatterns = [
|
||||
path('list/storage', lists.storage, name='list_storage'),
|
||||
|
||||
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
|
||||
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only
|
||||
path('edit/recipe/external/<int:pk>/', edit.RecipeUpdate.as_view(), name='edit_external_recipe'), # for internal use only
|
||||
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'), # for internal use only
|
||||
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'),
|
||||
# for internal use only
|
||||
path('edit/recipe/external/<int:pk>/', edit.RecipeUpdate.as_view(), name='edit_external_recipe'),
|
||||
# for internal use only
|
||||
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'), # for internal use only
|
||||
|
||||
path('edit/keyword/<int:pk>/', edit.KeywordUpdate.as_view(), name='edit_keyword'),
|
||||
path('edit/sync/<int:pk>/', edit.SyncUpdate.as_view(), name='edit_sync'),
|
||||
path('edit/import/<int:pk>/', edit.ImportUpdate.as_view(), name='edit_import'),
|
||||
path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
|
||||
path('edit/comment/<int:pk>/', edit.CommentUpdate.as_view(), name='edit_comment'),
|
||||
path('edit/recipe-book/<int:pk>/', edit.RecipeBookUpdate.as_view(), name='edit_recipe_book'),
|
||||
|
||||
path('redirect/delete/<slug:name>/<int:pk>/', edit.delete_redirect, name='redirect_delete'),
|
||||
|
||||
path('delete/recipe/<int:pk>/', edit.RecipeDelete.as_view(), name='delete_recipe'),
|
||||
path('delete/recipe-source/<int:pk>/', edit.RecipeSourceDelete.as_view(), name='delete_recipe_source'),
|
||||
path('delete/keyword/<int:pk>/', edit.KeywordDelete.as_view(), name='delete_keyword'),
|
||||
path('delete/sync/<int:pk>/', edit.MonitorDelete.as_view(), name='delete_sync'),
|
||||
path('delete/import/<int:pk>/', edit.ImportDelete.as_view(), name='delete_import'),
|
||||
path('delete/storage/<int:pk>/', edit.StorageDelete.as_view(), name='delete_storage'),
|
||||
path('delete/comment/<int:pk>/', edit.CommentDelete.as_view(), name='delete_comment'),
|
||||
path('delete/recipe-book/<int:pk>/', edit.RecipeBookDelete.as_view(), name='delete_recipe_book'),
|
||||
path('delete/recipe-book-entry/<int:pk>/', edit.RecipeBookEntryDelete.as_view(), name='delete_recipe_book_entry'),
|
||||
|
||||
path('data/sync', data.sync, name='data_sync'), # TODO move to generic "new" view
|
||||
path('data/batch/edit', data.batch_edit, name='data_batch_edit'),
|
||||
@@ -47,6 +54,9 @@ urlpatterns = [
|
||||
path('data/statistics', data.statistics, name='data_stats'),
|
||||
|
||||
path('api/get_file_link/<int:recipe_id>/', api.get_file_link, name='api_get_file_link'),
|
||||
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
|
||||
|
||||
|
||||
path('api/sync_all/', api.sync_all, name='api_sync'),
|
||||
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from cookbook.models import Recipe, Sync, Storage
|
||||
from cookbook.provider import dropbox
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
@@ -15,9 +14,25 @@ from cookbook.provider.nextcloud import Nextcloud
|
||||
def get_file_link(request, recipe_id):
|
||||
recipe = Recipe.objects.get(id=recipe_id)
|
||||
|
||||
if recipe.instructions:
|
||||
if recipe.internal:
|
||||
return HttpResponse(reverse('view_recipe', args=[recipe_id]))
|
||||
if recipe.storage.method == Storage.DROPBOX:
|
||||
if recipe.storage.method == Storage.DROPBOX: # TODO move to central location (as all provider related functions)
|
||||
if recipe.link == "":
|
||||
recipe.link = Dropbox.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
if recipe.storage.method == Storage.NEXTCLOUD:
|
||||
if recipe.link == "":
|
||||
recipe.link = Nextcloud.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
|
||||
return HttpResponse(recipe.link)
|
||||
|
||||
|
||||
@login_required
|
||||
def get_external_file_link(request, recipe_id):
|
||||
recipe = Recipe.objects.get(id=recipe_id)
|
||||
|
||||
if recipe.storage.method == Storage.DROPBOX: # TODO move to central location (as all provider related functions)
|
||||
if recipe.link == "":
|
||||
recipe.link = Dropbox.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
import simplejson as json
|
||||
from PIL import Image
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.files import File
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, get_object_or_404, render
|
||||
from django.urls import reverse_lazy, reverse
|
||||
@@ -10,7 +14,10 @@ from django.utils.translation import gettext as _
|
||||
from django.views.generic import UpdateView, DeleteView
|
||||
|
||||
from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, InternalRecipeForm, CommentForm
|
||||
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredients
|
||||
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredients, RecipeBook, \
|
||||
RecipeBookEntry
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -36,12 +43,29 @@ def internal_recipe_update(request, pk):
|
||||
recipe_instance = get_object_or_404(Recipe, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = InternalRecipeForm(request.POST)
|
||||
form = InternalRecipeForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
recipe = recipe_instance
|
||||
recipe.name = form.cleaned_data['name']
|
||||
recipe.instructions = form.cleaned_data['instructions']
|
||||
recipe.time = form.cleaned_data['time']
|
||||
recipe.working_time = form.cleaned_data['working_time']
|
||||
recipe.waiting_time = form.cleaned_data['waiting_time']
|
||||
|
||||
if form.cleaned_data['image']:
|
||||
recipe.image = form.cleaned_data['image']
|
||||
|
||||
img = Image.open(recipe.image)
|
||||
|
||||
basewidth = 720
|
||||
wpercent = (basewidth / float(img.size[0]))
|
||||
hsize = int((float(img.size[1]) * float(wpercent)))
|
||||
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'JPEG', quality=70)
|
||||
recipe.image = File(im_io, name=(str(recipe.pk) + '.jpeg'))
|
||||
elif 'image' in form.changed_data and form.cleaned_data['image'] is False:
|
||||
recipe.image = None
|
||||
|
||||
recipe.save()
|
||||
|
||||
@@ -71,7 +95,8 @@ def internal_recipe_update(request, pk):
|
||||
ingredients = RecipeIngredients.objects.filter(recipe=recipe_instance)
|
||||
|
||||
return render(request, 'forms/edit_internal_recipe.html',
|
||||
{'form': form, 'ingredients': json.dumps(list(ingredients.values())), 'view_url': reverse('view_recipe', args=[pk])})
|
||||
{'form': form, 'ingredients': json.dumps(list(ingredients.values())),
|
||||
'view_url': reverse('view_recipe', args=[pk])})
|
||||
|
||||
|
||||
class SyncUpdate(LoginRequiredMixin, UpdateView):
|
||||
@@ -106,26 +131,14 @@ class KeywordUpdate(LoginRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
|
||||
class StorageUpdate(LoginRequiredMixin, UpdateView):
|
||||
template_name = "generic/edit_template.html"
|
||||
model = Storage
|
||||
form_class = StorageForm
|
||||
|
||||
# TODO add msg box
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('edit_storage', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(StorageUpdate, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Storage Backend")
|
||||
return context
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_storage(request, pk):
|
||||
instance = get_object_or_404(Storage, pk=pk)
|
||||
|
||||
if not (instance.created_by == request.user or request.user.is_superuser):
|
||||
messages.add_message(request, messages.ERROR, _('You cannot edit this comment!'))
|
||||
return HttpResponseRedirect(reverse('list_storage'))
|
||||
|
||||
if request.method == "POST":
|
||||
form = StorageForm(request.POST)
|
||||
if form.is_valid():
|
||||
@@ -163,6 +176,13 @@ class CommentUpdate(LoginRequiredMixin, UpdateView):
|
||||
|
||||
# TODO add msg box
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if not (obj.created_by == request.user or request.user.is_superuser):
|
||||
messages.add_message(request, messages.ERROR, _('You cannot edit this comment!'))
|
||||
return HttpResponseRedirect(reverse('view_recipe', args=[obj.recipe.pk]))
|
||||
return super(CommentUpdate, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('edit_comment', kwargs={'pk': self.object.pk})
|
||||
|
||||
@@ -189,12 +209,40 @@ class ImportUpdate(LoginRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookUpdate(LoginRequiredMixin, UpdateView):
|
||||
template_name = "generic/edit_template.html"
|
||||
model = RecipeBook
|
||||
fields = ['name']
|
||||
|
||||
# TODO add msg box
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookUpdate, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe Book")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeUpdate(LoginRequiredMixin, UpdateView):
|
||||
model = Recipe
|
||||
form_class = ExternalRecipeForm
|
||||
template_name = "generic/edit_template.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
old_recipe = Recipe.objects.get(pk=self.object.pk)
|
||||
if not old_recipe.name == self.object.name:
|
||||
if self.object.storage.method == Storage.DROPBOX:
|
||||
Dropbox.rename_file(old_recipe,
|
||||
self.object.name) # TODO central location to handle storage type switches
|
||||
if self.object.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.rename_file(old_recipe, self.object.name)
|
||||
|
||||
self.object.file_path = os.path.dirname(self.object.file_path) + '/' + self.object.name + \
|
||||
os.path.splitext(self.object.file_path)[1]
|
||||
|
||||
messages.add_message(self.request, messages.SUCCESS, _('Changes saved!'))
|
||||
return super(RecipeUpdate, self).form_valid(form)
|
||||
|
||||
@@ -209,6 +257,8 @@ class RecipeUpdate(LoginRequiredMixin, UpdateView):
|
||||
context = super(RecipeUpdate, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe")
|
||||
context['view_url'] = reverse('view_recipe', args=[self.object.pk])
|
||||
if self.object.storage:
|
||||
context['delete_external_url'] = reverse('delete_recipe_source', args=[self.object.pk])
|
||||
return context
|
||||
|
||||
|
||||
@@ -229,6 +279,26 @@ class RecipeDelete(LoginRequiredMixin, DeleteView):
|
||||
return context
|
||||
|
||||
|
||||
class RecipeSourceDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Recipe
|
||||
success_url = reverse_lazy('index')
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if self.object.storage.method == Storage.DROPBOX:
|
||||
Dropbox.delete_file(self.object) # TODO central location to handle storage type switches
|
||||
if self.object.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.delete_file(self.object)
|
||||
|
||||
return super(RecipeSourceDelete, self).delete(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeSourceDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe")
|
||||
return context
|
||||
|
||||
|
||||
class ImportDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeImport
|
||||
@@ -282,3 +352,25 @@ class CommentDelete(LoginRequiredMixin, DeleteView):
|
||||
context = super(CommentDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Comment")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeBook
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe Book")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookEntryDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeBookEntry
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Bookmarks")
|
||||
return context
|
||||
|
||||
@@ -7,14 +7,15 @@ from django.urls import reverse_lazy, reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import CreateView
|
||||
|
||||
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm
|
||||
from cookbook.models import Keyword, Recipe
|
||||
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \
|
||||
RecipeBookForm
|
||||
from cookbook.models import Keyword, Recipe, RecipeBook
|
||||
|
||||
|
||||
class RecipeCreate(LoginRequiredMixin, CreateView):
|
||||
template_name = "generic/new_template.html"
|
||||
model = Recipe
|
||||
fields = ('name', )
|
||||
fields = ('name',)
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
@@ -50,6 +51,12 @@ class StorageCreate(LoginRequiredMixin, CreateView):
|
||||
form_class = StorageForm
|
||||
success_url = reverse_lazy('list_storage')
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.created_by = self.request.user
|
||||
obj.save()
|
||||
return HttpResponseRedirect(reverse('edit_storage', kwargs={'pk': obj.pk}))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(StorageCreate, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Storage Backend")
|
||||
@@ -80,6 +87,25 @@ def create_new_external_recipe(request, import_id):
|
||||
messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!'))
|
||||
else:
|
||||
new_recipe = RecipeImport.objects.get(id=import_id)
|
||||
form = ImportRecipeForm(initial={'file_path': new_recipe.file_path, 'name': new_recipe.name, 'file_uid': new_recipe.file_uid})
|
||||
form = ImportRecipeForm(
|
||||
initial={'file_path': new_recipe.file_path, 'name': new_recipe.name, 'file_uid': new_recipe.file_uid})
|
||||
|
||||
return render(request, 'forms/edit_import_recipe.html', {'form': form})
|
||||
|
||||
|
||||
class RecipeBookCreate(LoginRequiredMixin, CreateView):
|
||||
template_name = "generic/new_template.html"
|
||||
model = RecipeBook
|
||||
form_class = RecipeBookForm
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.user = self.request.user
|
||||
obj.save()
|
||||
return HttpResponseRedirect(reverse('view_books'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookCreate, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe Book")
|
||||
return context
|
||||
|
||||
@@ -30,24 +30,42 @@ def recipe_view(request, pk):
|
||||
comments = Comment.objects.filter(recipe=recipe)
|
||||
|
||||
if request.method == "POST":
|
||||
form = CommentForm(request.POST)
|
||||
if form.is_valid():
|
||||
comment_form = CommentForm(request.POST, prefix='comment')
|
||||
if comment_form.is_valid():
|
||||
comment = Comment()
|
||||
comment.recipe = recipe
|
||||
comment.text = form.cleaned_data['text']
|
||||
comment.text = comment_form.cleaned_data['text']
|
||||
comment.created_by = request.user
|
||||
|
||||
comment.save()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Comment saved!'))
|
||||
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('There was an error saving this comment!'))
|
||||
else:
|
||||
form = CommentForm()
|
||||
|
||||
return render(request, 'recipe_view.html', {'recipe': recipe, 'ingredients': ingredients, 'comments': comments, 'form': form})
|
||||
bookmark_form = RecipeBookEntryForm(request.POST, prefix='bookmark')
|
||||
if bookmark_form.is_valid():
|
||||
bookmark = RecipeBookEntry()
|
||||
bookmark.recipe = recipe
|
||||
bookmark.book = bookmark_form.cleaned_data['book']
|
||||
|
||||
bookmark.save()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Bookmark saved!'))
|
||||
|
||||
comment_form = CommentForm()
|
||||
bookmark_form = RecipeBookEntryForm()
|
||||
|
||||
return render(request, 'recipe_view.html',
|
||||
{'recipe': recipe, 'ingredients': ingredients, 'comments': comments, 'comment_form': comment_form,
|
||||
'bookmark_form': bookmark_form})
|
||||
|
||||
|
||||
def test(request):
|
||||
return render(request, 'test.html')
|
||||
@login_required()
|
||||
def books(request):
|
||||
book_list = []
|
||||
|
||||
books = RecipeBook.objects.filter(user=request.user).all()
|
||||
|
||||
for b in books:
|
||||
book_list.append({'book': b, 'recipes': RecipeBookEntry.objects.filter(book=b).all()})
|
||||
|
||||
return render(request, 'books.html', {'book_list': book_list})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
client_max_body_size 16M;
|
||||
|
||||
# serve static files
|
||||
location /static/ {
|
||||
alias /static/;
|
||||
|
||||
BIN
preview.png
Normal file
BIN
preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-11-21 14:34+0100\n"
|
||||
"POT-Creation-Date: 2020-01-13 12:08+0100\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:133
|
||||
#: recipes/settings.py:136
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: recipes/settings.py:134
|
||||
#: recipes/settings.py:137
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
@@ -27,7 +27,7 @@ LOGIN_REDIRECT_URL = "index"
|
||||
LOGOUT_REDIRECT_URL = "index"
|
||||
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
SESSION_COOKIE_AGE = 365 * 60 * 24
|
||||
SESSION_COOKIE_AGE = 365 * 60 * 24 * 60
|
||||
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
DJANGO_TABLES2_TEMPLATE = 'cookbook/templates/generic/table_template.html'
|
||||
@@ -50,7 +50,9 @@ INSTALLED_APPS = [
|
||||
'django_tables2',
|
||||
'django_filters',
|
||||
'crispy_forms',
|
||||
'emoji_picker',
|
||||
'rest_framework',
|
||||
'django_cleanup.apps.CleanupConfig',
|
||||
'cookbook.apps.CookbookConfig',
|
||||
]
|
||||
|
||||
@@ -78,6 +80,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.template.context_processors.media',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -119,7 +122,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'DE-de'
|
||||
LANGUAGE_CODE = 'de'
|
||||
|
||||
TIME_ZONE = 'Europe/Berlin'
|
||||
|
||||
@@ -138,5 +141,7 @@ LANGUAGES = [
|
||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles")
|
||||
|
||||
@@ -13,7 +13,8 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import include, path
|
||||
from django.contrib import admin
|
||||
|
||||
@@ -22,3 +23,6 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
django
|
||||
Pillow
|
||||
django-tables2
|
||||
django-filter
|
||||
django-crispy-forms
|
||||
djangorestframework
|
||||
django-autocomplete-light
|
||||
django-emoji-picker
|
||||
django-cleanup
|
||||
six
|
||||
requests
|
||||
markdown
|
||||
simplejson
|
||||
lxml
|
||||
webdavclient3
|
||||
python-dotenv==0.7.1
|
||||
psycopg2==2.7.4
|
||||
python-dotenv==0.10.3
|
||||
psycopg2-binary
|
||||
gunicorn==19.7.1
|
||||
|
||||
Reference in New Issue
Block a user