Compare commits

..

54 Commits
0.1.0 ... 0.2.1

Author SHA1 Message Date
vabene1111
51fc05dda2 fixed and added german translations 2020-01-13 12:14:05 +01:00
vabene1111
3d695e3d0f added support for ingredients with 0 amout 2020-01-13 11:51:22 +01:00
vabene1111
22b8a4ac18 added help texts to storage form 2020-01-13 11:44:27 +01:00
vabene1111
2e26f9c84b Merge branch 'develop' of https://github.com/vabene1111/recipes into develop
merge
2020-01-13 11:31:03 +01:00
vabene1111
78105e28c8 improved ingredient table 2020-01-13 11:30:59 +01:00
vabene1111
202b92b156 readme 2020-01-01 22:15:14 +01:00
vabene1111
f11f6b7ed1 increased session lifetime 2020-01-01 21:57:21 +01:00
vabene1111
e22d71152b cleaned up form buttons 2020-01-01 21:00:44 +01:00
vabene1111
2956a34aa6 print view 2019-12-26 12:17:51 +01:00
vabene1111
38bfb96b46 various image related fixes 2019-12-26 12:06:18 +01:00
vabene1111
b6a04b9dbd added missing translations 2019-12-26 11:38:01 +01:00
vabene1111
42faafef9f added waiting time 2019-12-26 11:32:04 +01:00
vabene1111
40277f9b4f nginx max body size 2019-12-25 19:54:34 +01:00
vabene1111
9d0a6e63f8 fixed not beeing able to open external recipes when internal exists 2019-12-25 19:47:11 +01:00
vabene1111
6a010587bf tweaked compression and upload limit 2019-12-25 19:41:25 +01:00
vabene1111
3fbd2ef032 image display improvements 2019-12-25 17:09:07 +01:00
vabene1111
3cb01d6332 added recipe images 2019-12-25 16:46:16 +01:00
vabene1111
e2301c0c3a external delete button 2019-12-25 15:54:23 +01:00
vabene1111
4b0164a676 requirnments binary psycopg 2019-12-25 13:17:41 +01:00
vabene1111
49f7afd8d2 recipe books working 2019-12-25 13:11:18 +01:00
vabene1111
d90b012601 recipe books 2019-12-25 12:53:09 +01:00
vabene1111
8fcafcc25a bookmarks wip 2019-12-24 15:06:58 +01:00
vabene1111
417e372c42 changed emoji widget 2019-12-24 11:37:53 +01:00
vabene1111
efabed8b2a linebreak for mobile 2019-12-24 10:54:52 +01:00
vabene1111
4748eb0b4c multiply ingredients 2019-12-24 10:53:49 +01:00
vabene1111
3428e75b86 nice checkboxes 2019-12-24 09:39:53 +01:00
vabene1111
6c6264ce4d testing with chekcboxes 2019-12-24 00:09:07 +01:00
vabene1111
dbea9c80da added renaming for nextcloud/webdav backends 2019-12-24 00:04:41 +01:00
vabene1111
3b5dd7e51d added missing import 2019-12-23 23:42:04 +01:00
vabene1111
c985ada3d0 file renaming in provider testing 2019-12-23 23:22:00 +01:00
vabene1111
a5cc38ecbd testing safari 2019-12-23 18:33:45 +01:00
vabene1111
2a54099187 possible fix for safari 2019-12-23 18:26:16 +01:00
vabene1111
6e4f16275d markdown github format readme 2019-12-09 13:21:57 +01:00
vabene1111
efa1376343 image resize 2019-12-09 13:17:36 +01:00
vabene1111
a2db9f7265 image 2019-12-09 13:16:28 +01:00
vabene1111
0ee2f77fea readme 2019-12-09 13:15:59 +01:00
vabene1111
a4a62af3d2 storage permission 2019-12-09 11:34:44 +01:00
vabene1111
590e083b14 commment permission 2019-12-09 11:26:21 +01:00
vabene1111
ea8e708cb7 WIP comment object permission 2019-12-09 11:21:08 +01:00
vabene1111
c2a5f2b2e3 tweaked filter 2019-12-09 10:37:21 +01:00
vabene1111
f2e4467a32 enable different backends again 2019-12-08 18:49:30 +01:00
vabene1111
4cc6a98a2b increased filter 2019-12-08 18:35:00 +01:00
vabene1111
647f4c94cb add deps 2019-12-08 18:33:46 +01:00
vabene1111
1bf5ab2425 rename 2019-12-08 18:32:54 +01:00
vabene1111
3945c6bd1b testing extensions 2019-12-08 18:31:56 +01:00
vabene1111
a4f715997e testing similarity 2019-12-08 17:42:12 +01:00
vabene1111
9446f97ede Revert "test trigram filter"
This reverts commit 516b6039f7.
2019-12-08 17:28:02 +01:00
vabene1111
516b6039f7 test trigram filter 2019-12-08 17:27:36 +01:00
vabene1111
77b1089b7f added favicon 2019-12-08 17:09:25 +01:00
vabene1111
d8efd16763 changed image base 2019-12-08 17:09:08 +01:00
vabene1111
79679c105a refactor search ui 2019-12-08 16:12:55 +01:00
vabene1111
a0a9a93888 updated bootstrap 2019-12-08 13:59:31 +01:00
vabene1111
82986f21f4 updated tabulator + added auto focus 2019-12-08 13:56:24 +01:00
vabene1111
88ce2b9495 updated to django 3 2019-12-08 13:07:02 +01:00
44 changed files with 1036 additions and 349 deletions

2
.gitignore vendored
View File

@@ -61,6 +61,8 @@ target/
venv/
mediafiles/
*.sqlite3
\.idea/workspace\.xml

View File

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

View File

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

View File

@@ -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.
![Preview](preview.png)
### 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

View File

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

View File

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

View File

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

View 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(),
]

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

View 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')),
],
),
]

View 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/'),
),
]

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -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>&#x2063;</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">&times;</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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

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

View File

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

View File

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

View File

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