Compare commits

...

55 Commits
0.2.2 ... 0.3.2

Author SHA1 Message Date
vabene1111
b8f16b50a7 simplified viewer + fixed characters escaping 2020-02-19 20:19:08 +01:00
vabene1111
752df5a1d2 cleaned up viewer 2020-02-19 19:39:32 +01:00
vabene1111
fe6e351349 testing pdf viewerr 2020-02-19 19:12:11 +01:00
vabene1111
8cc9273268 basic viewer working 2020-02-19 19:02:47 +01:00
vabene1111
0c1763b347 pdf display working 2020-02-19 18:13:11 +01:00
vabene1111
88dc713683 WIP pdf embedding 2020-02-19 16:55:13 +01:00
vabene1111
fc1cc70870 basic pdf embedding 2020-02-19 00:08:32 +01:00
vabene1111
42e09fcae9 updated translations 2020-02-18 23:26:14 +01:00
vabene1111
4843568d10 added searching for ingredients 2020-02-18 23:16:05 +01:00
vabene1111
2e7e4b23dd highlight active tab 2020-02-18 23:03:37 +01:00
vabene1111
8192a8dc8f improved shopping list ui 2020-02-18 23:00:23 +01:00
vabene1111
c98dbd065e allow choosing output format of shopping list 2020-02-18 22:41:37 +01:00
vabene1111
43d03ed17d group ingredients in shopping list 2020-02-18 22:33:31 +01:00
vabene1111
8ba34414a1 added shoping to nav 2020-02-18 22:28:22 +01:00
vabene1111
46dffe2f63 added ingredient merging 2020-02-18 22:25:21 +01:00
vabene1111
04cbe6cb2c removed empty script tag when no default is given 2020-02-18 22:17:27 +01:00
vabene1111
8fd6dcc81c updated translations 2020-02-17 23:34:17 +01:00
vabene1111
b89e96476a fixed status badge
since GH actions currently dont triggert on fast forward merge pushs the badge is changed to show passing status of develop
2020-02-17 00:33:21 +01:00
vabene1111
67b4ec8215 fixed shopping ingredient list 2020-02-17 00:24:33 +01:00
vabene1111
db2e67dd71 incresed instruction font size 2020-02-17 00:22:36 +01:00
vabene1111
de355abd19 fixed tests to reflect new name 2020-02-17 00:17:51 +01:00
vabene1111
41bfa95cb2 nav color theming 2020-02-17 00:11:15 +01:00
vabene1111
ad9944dd01 fixed broken tabulator on default theme 2020-02-16 23:59:16 +01:00
vabene1111
a1160c310c fixed fix of migration 2020-02-16 23:54:45 +01:00
vabene1111
0444286d11 hotkeys for recipe editing 2020-02-16 23:50:58 +01:00
vabene1111
7d4630e3af normalized ingredients 2020-02-16 23:22:44 +01:00
vabene1111
f77aa7c8f0 migrating ingredients 2020-02-16 23:12:16 +01:00
vabene1111
81677a74bb made flatly default theme + fixed preview image 2020-02-16 22:58:24 +01:00
vabene1111
2cc385ceac preview image + readme update 2020-02-16 22:52:42 +01:00
vabene1111
b4cdc92207 catch non existing relation 2020-02-14 00:40:17 +01:00
vabene1111
ffdcbff540 dark theming tabulator + select 2020-02-14 00:35:52 +01:00
vabene1111
cc7422a503 theming refactor
moved server side for a better page loading experience and less javascript mess
2020-02-13 23:47:24 +01:00
vabene1111
c08e30c5a9 case insensitive filter 2020-02-12 23:38:05 +01:00
vabene1111
60477cdb9e settings button 2020-02-04 22:26:40 +01:00
vabene1111
bc066d29f6 dark mode reverted + meal plan button 2020-02-04 22:18:10 +01:00
vabene1111
c96159e15c dark mode 2020-02-04 22:00:47 +01:00
vabene1111
2d70680214 recipe buttons wip 2020-02-03 11:33:44 +01:00
vabene1111
00fdab1678 unit merging 2020-02-03 11:00:11 +01:00
vabene1111
6ccafe3c2f Update README.md 2020-02-02 23:03:46 +01:00
vabene1111
4080301dbc Testing GitHub actions as CI 2020-02-02 23:01:31 +01:00
vabene1111
e7227f84ca some more recipe edit cleanup 2020-02-02 22:52:25 +01:00
vabene1111
6753a2c0b5 basic recipe edit test 2020-02-02 22:46:37 +01:00
vabene1111
56e841879b login template enhancements 2020-02-02 22:12:04 +01:00
vabene1111
19f5b44e50 some basic tests 2020-02-02 22:09:30 +01:00
vabene1111
305a4949fb cleanup recipe edit 2020-02-02 16:06:24 +01:00
vabene1111
07502fecc0 fixed possible markdown xss 2020-02-02 16:06:12 +01:00
vabene1111
4da1293898 fixed it 2020-02-01 21:11:26 +01:00
vabene1111
ab2ce26d9d nearly working 2020-01-30 12:45:55 +01:00
vabene1111
a2348f531b basics of ingredient unit normalization 2020-01-30 12:26:47 +01:00
vabene1111
227d90d49d basic shopping view 2020-01-30 00:28:01 +01:00
vabene1111
6a61c934cd update readme 2020-01-19 14:40:06 +01:00
vabene1111
6f4a40acdd meal plan prev+next week buttons 2020-01-19 14:34:50 +01:00
vabene1111
becdcdc6a4 small meal plan fixes 2020-01-17 18:31:46 +01:00
vabene1111
afa69c647d basic meal plan working 2020-01-17 17:47:23 +01:00
vabene1111
7449380434 meal plan WIP 2020-01-17 16:02:14 +01:00
74 changed files with 2722 additions and 318 deletions

26
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Continous Integration
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: [3.7]
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Django Testing project
run: |
python3 manage.py test

2
.gitignore vendored
View File

@@ -63,7 +63,7 @@ venv/
mediafiles/
*.sqlite3
*.sqlite3*
\.idea/workspace\.xml

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{pretty-checkbox}" />
<file url="file://$PROJECT_DIR$" libraries="{jquery-3.4.1, pdf, pdf_viewer, pretty-checkbox}" />
</component>
</project>

View File

@@ -1,17 +1,21 @@
# 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.
# Recipes ![Continous Integration](https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop)
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
- :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)
- :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
- :iphone: Optimized for use on **mobile** devices like phones and tablets
- :shopping_cart: Generate **shopping** lists from recipes
- :calendar: Create a **Plan** on what to eat when
- :person_with_blond_hair: **Share** recipes with friends and comment on them to suggest or remember changes you made
- :whale: Easy setup with **Docker**
- :heavy_plus_sign: Many more like recipe scaling, image compression, cookbooks, ...
- :art: Customize your interface with **themes**
- :heavy_plus_sign: Many more like recipe scaling, image compression, cookbooks, printing views, ...
This application is meant for people with a collection of recipes they want to share with family and friends or simply store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as a public page.

View File

@@ -2,14 +2,17 @@ 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 cookbook.models import Recipe, Keyword, Ingredient
from django.conf import settings
from django.utils.translation import gettext as _
class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
method='filter_keywords')
ingredients = django_filters.ModelMultipleChoiceFilter(queryset=Ingredient.objects.all(), widget=MultiSelectWidget,
method='filter_ingredients', label=_('Ingredients'))
@staticmethod
def filter_keywords(queryset, name, value):
@@ -19,6 +22,14 @@ class RecipeFilter(django_filters.FilterSet):
queryset = queryset.filter(keywords=x)
return queryset
@staticmethod
def filter_ingredients(queryset, name, value):
if not name == 'ingredients':
return queryset
for x in value:
queryset = queryset.filter(recipeingredient__ingredient=x).distinct()
return queryset
@staticmethod
def filter_name(queryset, name, value):
if not name == 'name':
@@ -32,14 +43,4 @@ class RecipeFilter(django_filters.FilterSet):
class Meta:
model = Recipe
fields = ['name', 'keywords']
class QuickRecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='contains')
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
method='filter_keywords')
class Meta:
model = Recipe
fields = ['name', 'keywords']
fields = ['name', 'keywords', 'ingredients']

View File

@@ -1,3 +1,4 @@
from dal import autocomplete
from django import forms
from django.forms import widgets
from django.utils.translation import gettext as _
@@ -6,11 +7,35 @@ from emoji_picker.widgets import EmojiPickerTextInput
from .models import *
class SelectWidget(widgets.Select):
class Media:
js = ('custom/js/form_select.js',)
class MultiSelectWidget(widgets.SelectMultiple):
class Media:
js = ('custom/js/form_multiselect.js',)
# yes there are some stupid browsers that still dont support this but i dont support people using these browsers
class DateWidget(forms.DateInput):
input_type = 'date'
def __init__(self, **kwargs):
kwargs["format"] = "%Y-%m-%d"
super().__init__(**kwargs)
class UserPreferenceForm(forms.ModelForm):
class Meta:
model = UserPreference
fields = ('theme', 'nav_color')
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!')
}
class ExternalRecipeForm(forms.ModelForm):
file_path = forms.CharField(disabled=True, required=False)
storage = forms.ModelChoiceField(queryset=Storage.objects.all(), disabled=True, required=False)
@@ -32,6 +57,8 @@ class ExternalRecipeForm(forms.ModelForm):
class InternalRecipeForm(forms.ModelForm):
ingredients = forms.CharField(widget=forms.HiddenInput(), required=False)
class Meta:
model = Recipe
fields = ('name', 'instructions', 'image', 'working_time', 'waiting_time', 'keywords')
@@ -46,6 +73,52 @@ class InternalRecipeForm(forms.ModelForm):
widgets = {'keywords': MultiSelectWidget}
class ShoppingForm(forms.Form):
recipe = forms.ModelMultipleChoiceField(
queryset=Recipe.objects.all(),
widget=MultiSelectWidget
)
markdown_format = forms.BooleanField(
help_text=_('Include <code>- [ ]</code> in list for easier usage in markdown based documents.'),
required=False,
initial=False
)
class UnitMergeForm(forms.Form):
prefix = 'unit'
new_unit = forms.ModelChoiceField(
queryset=Unit.objects.all(),
widget=SelectWidget,
label=_('New Unit'),
help_text=_('New unit that other gets replaced by.'),
)
old_unit = forms.ModelChoiceField(
queryset=Unit.objects.all(),
widget=SelectWidget,
label=_('Old Unit'),
help_text=_('Unit that should be replaced.'),
)
class IngredientMergeForm(forms.Form):
prefix = 'ingredient'
new_ingredient = forms.ModelChoiceField(
queryset=Ingredient.objects.all(),
widget=SelectWidget,
label=_('New Ingredient'),
help_text=_('New ingredient that other gets replaced by.'),
)
old_ingredient = forms.ModelChoiceField(
queryset=Ingredient.objects.all(),
widget=SelectWidget,
label=_('Old Ingredient'),
help_text=_('Ingredient that should be replaced.'),
)
class CommentForm(forms.ModelForm):
prefix = 'comment'
@@ -87,12 +160,6 @@ class StorageForm(forms.ModelForm):
}
class RecipeBookForm(forms.ModelForm):
class Meta:
model = RecipeBook
fields = ('name',)
class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark'
@@ -125,3 +192,17 @@ class ImportRecipeForm(forms.ModelForm):
'file_uid': _('File ID'),
}
widgets = {'keywords': MultiSelectWidget}
class RecipeBookForm(forms.ModelForm):
class Meta:
model = RecipeBook
fields = ('name',)
class MealPlanForm(forms.ModelForm):
class Meta:
model = MealPlan
fields = ('recipe', 'meal', 'note', 'date')
widgets = {'recipe': SelectWidget, 'date': DateWidget}

View File

@@ -1,6 +1,6 @@
from dal import autocomplete
from cookbook.models import Keyword, RecipeIngredients
from cookbook.models import Keyword, RecipeIngredient, Recipe, Unit, Ingredient
class KeywordAutocomplete(autocomplete.Select2QuerySetView):
@@ -19,11 +19,37 @@ class KeywordAutocomplete(autocomplete.Select2QuerySetView):
class IngredientsAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return RecipeIngredients.objects.none()
return Ingredient.objects.none()
qs = RecipeIngredients.objects.all()
qs = Ingredient.objects.all()
if self.q:
qs = qs.filter(name__istartswith=self.q)
qs = qs.filter(name__icontains=self.q)
return qs
class RecipeAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return Recipe.objects.none()
qs = Recipe.objects.all()
if self.q:
qs = qs.filter(name__icontains=self.q)
return qs
class UnitAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return Unit.objects.none()
qs = Unit.objects.all()
if self.q:
qs = qs.filter(name__icontains=self.q)
return qs

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-01-13 12:08+0100\n"
"POT-Creation-Date: 2020-02-18 23:20+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,152 +18,247 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cookbook/forms.py:24 cookbook/forms.py:40 cookbook/forms.py:122
#: .\cookbook\filters.py:15
#: .\cookbook\templates\forms\edit_internal_recipe.html:28
#: .\cookbook\templates\forms\ingredients.html:33
#: .\cookbook\templates\recipe_view.html:67
msgid "Ingredients"
msgstr "Zutaten"
#: .\cookbook\forms.py:35
msgid ""
"Color of the top navigation bar. Not all colors work with all themes, just "
"try them out!"
msgstr ""
"Farbe der oberen Navigationsleiste. Nicht alle Farben passen, daher einfach mal ausprobieren!"
#: .\cookbook\forms.py:49 .\cookbook\forms.py:67 .\cookbook\forms.py:189
msgid "Name"
msgstr "Name"
#: cookbook/forms.py:25 cookbook/forms.py:41 cookbook/forms.py:123
#: cookbook/templates/stats.html:22
#: .\cookbook\forms.py:50 .\cookbook\forms.py:68 .\cookbook\forms.py:190
#: .\cookbook\templates\stats.html:22
msgid "Keywords"
msgstr "Schlagwörter"
#: cookbook/forms.py:26 cookbook/forms.py:43
#: .\cookbook\forms.py:51 .\cookbook\forms.py:70
msgid "Preparation time in minutes"
msgstr "Zubereitungszeit in Minuten"
#: cookbook/forms.py:27 cookbook/forms.py:44
#: .\cookbook\forms.py:52 .\cookbook\forms.py:71
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Wartezeit (kochen/backen) in Minuten"
#: cookbook/forms.py:28 cookbook/forms.py:124
#: .\cookbook\forms.py:53 .\cookbook\forms.py:191
msgid "Path"
msgstr "Pfad"
#: cookbook/forms.py:29
#: .\cookbook\forms.py:54
msgid "Storage UID"
msgstr "Speicher ID"
#: cookbook/forms.py:42
#: .\cookbook\forms.py:69
msgid "Instructions"
msgstr "Anleitung"
#: cookbook/forms.py:57
#: .\cookbook\forms.py:82
msgid ""
"Include <code>- [ ]</code> in list for easier usage in markdown based "
"documents."
msgstr ""
"Füge <code>- [ ]</code> vor den Zutaten ein um sie besser in einem Markdown Dokument "
"zu verwenden."
#: .\cookbook\forms.py:94
msgid "New Unit"
msgstr "Neue Einheit"
#: .\cookbook\forms.py:95
msgid "New unit that other gets replaced by."
msgstr "Neue Einheit die die alte ersetzt."
#: .\cookbook\forms.py:100
msgid "Old Unit"
msgstr "Alte Einheit"
#: .\cookbook\forms.py:101
msgid "Unit that should be replaced."
msgstr "Einheit die ersetzt werden soll."
#: .\cookbook\forms.py:111
msgid "New Ingredient"
msgstr "Neue Zutat"
#: .\cookbook\forms.py:112
msgid "New ingredient that other gets replaced by."
msgstr "Neue Zutat die die alte ersetzt."
#: .\cookbook\forms.py:117
msgid "Old Ingredient"
msgstr "Alte Zutat"
#: .\cookbook\forms.py:118
msgid "Ingredient that should be replaced."
msgstr "Zutat die ersetzt werden soll."
#: .\cookbook\forms.py:130
msgid "Add your comment: "
msgstr "Schreibe einen Kommentar:"
#: cookbook/forms.py:75
#: .\cookbook\forms.py:148
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
#: .\cookbook\forms.py:151
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
#: .\cookbook\forms.py:159
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)"
msgstr ""
"Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote.php/"
"webdav/</code> wird automatisch hinzugefügt)"
#: cookbook/forms.py:111
#: .\cookbook\forms.py:178
msgid "Search String"
msgstr "Such Wort"
#: cookbook/forms.py:125
#: .\cookbook\forms.py:192
msgid "File ID"
msgstr "Datei ID"
#: 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
#: .\cookbook\models.py:172
msgid "Breakfast"
msgstr "Frühstück"
#: .\cookbook\models.py:172
msgid "Lunch"
msgstr "Mittagessen"
#: .\cookbook\models.py:172
msgid "Dinner"
msgstr "Abendessen"
#: .\cookbook\models.py:172
msgid "Other"
msgstr "Andere"
#: .\cookbook\tables.py:75
#: .\cookbook\templates\forms\edit_internal_recipe.html:49
#: .\cookbook\templates\forms\edit_internal_recipe.html:176
#: .\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:64 cookbook/templates/base.html:72
#: cookbook/templates/index.html:7
#: .\cookbook\templates\base.html:70 .\cookbook\templates\base.html:78
#: .\cookbook\templates\forms\ingredients.html:7
#: .\cookbook\templates\index.html:7 .\cookbook\templates\shopping_list.html:7
msgid "Cookbook"
msgstr "Kochbuch"
#: cookbook/templates/base.html:76
#: .\cookbook\templates\base.html:82
msgid "Books"
msgstr "Bücher"
#: cookbook/templates/base.html:81
#: .\cookbook\templates\base.html:86 .\cookbook\templates\meal_plan.html:4
#: .\cookbook\templates\meal_plan.html:13 .\cookbook\views\edit.py:261
#: .\cookbook\views\edit.py:462 .\cookbook\views\new.py:130
msgid "Meal-Plan"
msgstr "Plan"
#: .\cookbook\templates\base.html:90
#, fuzzy
#| msgid "Shopping List"
msgid "Shopping"
msgstr "Einkaufsliste"
#: .\cookbook\templates\base.html:96
msgid "Tags"
msgstr "Schlagwörter"
#: 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
#: .\cookbook\templates\base.html:100 .\cookbook\views\edit.py:151
#: .\cookbook\views\edit.py:407 .\cookbook\views\lists.py:17
#: .\cookbook\views\new.py:46
msgid "Keyword"
msgstr "Schlagwort"
#: cookbook/templates/base.html:87
#: .\cookbook\templates\base.html:102
msgid "Batch Edit"
msgstr "Massenbearbeitung"
#: cookbook/templates/base.html:92
#: .\cookbook\templates\base.html:107
msgid "Storage Data"
msgstr "Datenquellen"
#: cookbook/templates/base.html:96
#: .\cookbook\templates\base.html:111
msgid "Storage Backends"
msgstr "Speicher Quellen"
#: cookbook/templates/base.html:98
#: .\cookbook\templates\base.html:113
msgid "Configure Sync"
msgstr "Sync Einstellen"
#: cookbook/templates/base.html:100
#: .\cookbook\templates\base.html:115
msgid "Import Recipes"
msgstr "Importierte Rezepte"
#: cookbook/templates/base.html:102 cookbook/views/lists.py:25
#: .\cookbook\templates\base.html:117 .\cookbook\views\lists.py:25
msgid "Import Log"
msgstr "Import Log"
#: cookbook/templates/base.html:104 cookbook/templates/stats.html:10
#: .\cookbook\templates\base.html:119 .\cookbook\templates\stats.html:10
msgid "Statistics"
msgstr "Statistiken"
#: cookbook/templates/base.html:112
#: .\cookbook\templates\base.html:121
msgid "Units & Ingredients"
msgstr "Einheiten & Zutaten"
#: .\cookbook\templates\base.html:130 .\cookbook\templates\settings.html:6
#: .\cookbook\templates\settings.html:11
msgid "Settings"
msgstr "Einstellungen"
#: .\cookbook\templates\base.html:135
msgid "Admin"
msgstr "Admin"
#: cookbook/templates/base.html:116
#: .\cookbook\templates\base.html:140
msgid "Logout"
msgstr "Ausloggen"
#: cookbook/templates/base.html:119
#: .\cookbook\templates\base.html:143
#: .\cookbook\templates\registration\login.html:44
msgid "Login"
msgstr "Einloggen"
#: cookbook/templates/batch/edit.html:6
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
msgstr "Kategorie massenbearbeitung"
#: cookbook/templates/batch/edit.html:15
#: .\cookbook\templates\batch\edit.html:15
msgid "Batch edit Recipes"
msgstr "Rezept massenbearbeitung"
#: cookbook/templates/batch/edit.html:20
#: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
msgstr ""
"Ausgewählte Schlagwörter zu allen Rezepten die das Suchwort enthalten "
"hinzufügen"
#: cookbook/templates/batch/monitor.html:6 cookbook/views/edit.py:114
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:135
msgid "Sync"
msgstr "Synchronisieren"
#: cookbook/templates/batch/monitor.html:10
#: .\cookbook\templates\batch\monitor.html:10
msgid "Manage watched Folders"
msgstr "Überwachte Ordner verwalten"
#: cookbook/templates/batch/monitor.html:14
#: .\cookbook\templates\batch\monitor.html:14
msgid ""
"On this Page you can manage all storage folder locations that should be "
"monitored and synced"
@@ -171,20 +266,20 @@ msgstr ""
"Auf dieser Seite kannst du alle Ordner verwalten die überwacht und "
"synchronisiert werden sollen"
#: cookbook/templates/batch/monitor.html:16
#: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format"
msgstr "Der Pfad muss in folgendem Format sein"
#: cookbook/templates/batch/monitor.html:27
#: .\cookbook\templates\batch\monitor.html:27
msgid "Sync Now!"
msgstr "Jetzt Synchronisieren!"
#: cookbook/templates/batch/waiting.html:4
#: cookbook/templates/batch/waiting.html:10
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
msgid "Importing Recipes"
msgstr "Rezept werden importiert"
#: cookbook/templates/batch/waiting.html:23
#: .\cookbook\templates\batch\waiting.html:23
msgid ""
"This can take a few minutes, depending on the number of recipes in sync, "
"please wait."
@@ -192,120 +287,165 @@ 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
#: .\cookbook\templates\books.html:4 .\cookbook\templates\books.html:10
msgid "Recipe Books"
msgstr "Rezept Bücher"
#: cookbook/templates/books.html:14
#: .\cookbook\templates\books.html:14
msgid "New Book"
msgstr "Neues Buch"
#: cookbook/templates/books.html:53
#: .\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
#: .\cookbook\templates\forms\edit_import_recipe.html:5
#: .\cookbook\templates\forms\edit_import_recipe.html:9
msgid "Import new Recipe"
msgstr "Rezept Importieren"
#: cookbook/templates/forms/edit_import_recipe.html:14
#: cookbook/templates/forms/edit_internal_recipe.html:37
#: cookbook/templates/generic/edit_template.html:23
#: cookbook/templates/generic/new_template.html:23
#: cookbook/templates/recipe_view.html:207
#: .\cookbook\templates\forms\edit_import_recipe.html:14
#: .\cookbook\templates\forms\edit_internal_recipe.html:47
#: .\cookbook\templates\generic\edit_template.html:23
#: .\cookbook\templates\generic\new_template.html:23
#: .\cookbook\templates\recipe_view.html:214
#: .\cookbook\templates\settings.html:33 .\cookbook\templates\settings.html:47
msgid "Save"
msgstr "Speichern"
#: cookbook/templates/forms/edit_internal_recipe.html:7
#: cookbook/templates/forms/edit_internal_recipe.html:16
#: .\cookbook\templates\forms\edit_internal_recipe.html:8
#: .\cookbook\templates\forms\edit_internal_recipe.html:18
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:37
msgid ""
"Use <b>Ctrl</b>+<b>Space</b> to insert new Ingredient!<br/>You can also save "
"the recipe using <b>Ctrl</b>+<b>Shift</b>+<b>S</b>."
msgstr ""
#: cookbook/templates/forms/edit_internal_recipe.html:41
#: cookbook/templates/generic/edit_template.html:27
#: cookbook/templates/recipe_view.html:7
#: .\cookbook\templates\forms\edit_internal_recipe.html:51
#: .\cookbook\templates\generic\edit_template.html:27
#: .\cookbook\templates\recipe_view.html:7
msgid "View"
msgstr "Angucken"
#: cookbook/templates/forms/edit_internal_recipe.html:45
#: cookbook/templates/generic/edit_template.html:30
#: .\cookbook\templates\forms\edit_internal_recipe.html:55
#: .\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
#: .\cookbook\templates\forms\edit_internal_recipe.html:159
#: .\cookbook\templates\forms\edit_internal_recipe.html:208
msgid "Ingredient"
msgstr "Zutat"
#: cookbook/templates/forms/edit_internal_recipe.html:95
#: .\cookbook\templates\forms\edit_internal_recipe.html:164
msgid "Amount"
msgstr "Menge"
#: cookbook/templates/forms/edit_internal_recipe.html:96
#: .\cookbook\templates\forms\edit_internal_recipe.html:166
msgid "Unit"
msgstr "Einheit"
#: cookbook/templates/generic/delete_template.html:18
#: .\cookbook\templates\forms\edit_internal_recipe.html:179
msgid "Are you sure that you want to delete this ingredient?"
msgstr "Bist du sicher das du diese Zutat löschen willst?"
#: .\cookbook\templates\forms\ingredients.html:15
msgid "Edit Ingredients"
msgstr "Zutaten Bearbeiten"
#: .\cookbook\templates\forms\ingredients.html:16
msgid ""
"\n"
" The following form can be used if, accidentally, two (or more) units "
"or ingredients where created that should be\n"
" the same.\n"
" It merges two units or ingredients and updates all recipes using "
"them.\n"
" "
msgstr ""
"\n"
" Dieses Formular kann genutzt werden wenn versehentlich zwei (oder mehr) Einheiten"
"oder Zutaten erstellt wurden die eigentlich identisch\n"
" sein sollen.\n"
" Es vereint zwei Zutaten oder Einheiten und aktualisiert alle entsprechenden "
"Rezepte.\n"
" "
#: .\cookbook\templates\forms\ingredients.html:24
msgid "Units"
msgstr "Einheiten"
#: .\cookbook\templates\forms\ingredients.html:29
msgid "Are you sure that you want to merge these two units ?"
msgstr "Bist du sicher diese beiden Einheiten zusammengeführt werden sollen ?"
#: .\cookbook\templates\forms\ingredients.html:30
#: .\cookbook\templates\forms\ingredients.html:39
msgid "Merge"
msgstr "Zusammenführen"
#: .\cookbook\templates\forms\ingredients.html:38
msgid "Are you sure that you want to merge these two ingredients ?"
msgstr "Bist du sicher diese beiden Zutaten zusammengeführt werden sollen ?"
#: .\cookbook\templates\generic\delete_template.html:18
#, python-format
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
msgstr "Bist du sicher das %(title)s: <b>%(object)s</b> gelöscht werden soll"
#: cookbook/templates/generic/delete_template.html:21
#: .\cookbook\templates\generic\delete_template.html:21
msgid "Confirm"
msgstr ""
msgstr "Bestätigen"
#: cookbook/templates/generic/edit_template.html:6
#: cookbook/templates/generic/edit_template.html:14
#: .\cookbook\templates\generic\edit_template.html:6
#: .\cookbook\templates\generic\edit_template.html:14
msgid "Edit"
msgstr "Bearbeiten"
#: cookbook/templates/generic/list_template.html:6
#: cookbook/templates/generic/list_template.html:12
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:12
msgid "List"
msgstr "Liste"
#: cookbook/templates/generic/list_template.html:19
#: .\cookbook\templates\generic\list_template.html:19
msgid "Import all"
msgstr "Alle importieren"
#: cookbook/templates/generic/new_template.html:6
#: cookbook/templates/generic/new_template.html:14
#: .\cookbook\templates\generic\new_template.html:6
#: .\cookbook\templates\generic\new_template.html:14
msgid "New"
msgstr "Neu"
#: cookbook/templates/generic/table_template.html:76
#: .\cookbook\templates\generic\table_template.html:76
msgid "previous"
msgstr "vorherige"
#: cookbook/templates/generic/table_template.html:98
#: .\cookbook\templates\generic\table_template.html:98
msgid "next"
msgstr "nächste"
#: cookbook/templates/include/recipe_open_modal.html:28
#: cookbook/views/edit.py:258 cookbook/views/edit.py:278
#: cookbook/views/edit.py:298 cookbook/views/new.py:32
#: .\cookbook\templates\include\recipe_open_modal.html:28
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:354
#: .\cookbook\views\edit.py:374 .\cookbook\views\new.py:34
msgid "Recipe"
msgstr "Rezept"
#: cookbook/templates/include/recipe_open_modal.html:39
#: .\cookbook\templates\include\recipe_open_modal.html:39
msgid "Close"
msgstr "Schließen"
#: cookbook/templates/include/recipe_open_modal.html:56
#: .\cookbook\templates\include\recipe_open_modal.html:56
msgid "Open Recipe"
msgstr "Rezept öffnen"
#: cookbook/templates/include/storage_backend_warning.html:4
#: .\cookbook\templates\include\storage_backend_warning.html:4
msgid "Security Warning"
msgstr "Sicherheitswarnung"
#: cookbook/templates/include/storage_backend_warning.html:5
#: .\cookbook\templates\include\storage_backend_warning.html:5
msgid ""
"\n"
" The <b>Password and Token</b> field are stored as <b>plain text</b> "
@@ -327,48 +467,52 @@ msgstr ""
"oder Accounts mit limitiertem Zugriff verwendet werden.\n"
" "
#: cookbook/templates/index.html:21
#: .\cookbook\templates\index.html:21
msgid "Search recipe ..."
msgstr "Suche Rezept ..."
#: cookbook/templates/index.html:40
#: .\cookbook\templates\index.html:40
msgid "Advanced Search"
msgstr "Erweiterte Suche"
#: cookbook/templates/index.html:59
#: .\cookbook\templates\index.html:62
msgid "Log in to view Recipies"
msgstr "Bitte einloggen um Rezepte zu sehen"
#: cookbook/templates/recipe_view.html:27
#: .\cookbook\templates\meal_plan.html:20
msgid "Week"
msgstr "Woche"
#: .\cookbook\templates\recipe_view.html:31
msgid "in"
msgstr "in"
#: cookbook/templates/recipe_view.html:32
#: cookbook/templates/recipe_view.html:174
#: .\cookbook\templates\recipe_view.html:36
#: .\cookbook\templates\recipe_view.html:181
msgid "by"
msgstr "von"
#: cookbook/templates/recipe_view.html:43
#: .\cookbook\templates\recipe_view.html:47
msgid "Preparation time ca."
msgstr "Zubereitungszeit ca."
#: cookbook/templates/recipe_view.html:48
#: .\cookbook\templates\recipe_view.html:52
msgid "Waiting time ca."
msgstr "Zubereitungszeit ca."
#: cookbook/templates/recipe_view.html:110
#: .\cookbook\templates\recipe_view.html:114
msgid "Recipe Image"
msgstr "Rezept Bild"
#: cookbook/templates/recipe_view.html:126
#: .\cookbook\templates\recipe_view.html:133
msgid "View external recipe"
msgstr "Externes Rezept ansehen"
#: cookbook/templates/recipe_view.html:137
#: .\cookbook\templates\recipe_view.html:144
msgid "External recipe"
msgstr "Externes Rezept"
#: cookbook/templates/recipe_view.html:139
#: .\cookbook\templates\recipe_view.html:146
msgid ""
"\n"
" This is an external recipe, which means you can only "
@@ -386,56 +530,81 @@ msgstr ""
"bleibt weiterhin verfügbar.\n"
" "
#: cookbook/templates/recipe_view.html:147
#: .\cookbook\templates\recipe_view.html:154
msgid "Convert now!"
msgstr "Jetzt umwandeln!"
#: cookbook/templates/recipe_view.html:156
#: .\cookbook\templates\recipe_view.html:163
msgid "Comments"
msgstr "Kommentare"
#: cookbook/templates/recipe_view.html:165 cookbook/views/edit.py:191
#: cookbook/views/edit.py:353
#: .\cookbook\templates\recipe_view.html:172 .\cookbook\views\edit.py:212
#: .\cookbook\views\edit.py:429
msgid "Comment"
msgstr "Kommentar"
#: cookbook/templates/registration/login.html:8
#: .\cookbook\templates\registration\login.html:8
msgid "Your username and password didn't match. Please try again."
msgstr "Nutzername oder Passwort falsch. Bitte versuch es erneut."
#: cookbook/templates/stats.html:4
#: .\cookbook\templates\settings.html:17
msgid "Language"
msgstr "Sprache"
#: .\cookbook\templates\settings.html:42
msgid "Style"
msgstr "Stil"
#: .\cookbook\templates\shopping_list.html:15
msgid "Shopping List"
msgstr "Einkaufsliste"
#: .\cookbook\templates\shopping_list.html:20
msgid "Load"
msgstr "Laden"
#: .\cookbook\templates\shopping_list.html:37
#: .\cookbook\templates\shopping_list.html:55
msgid "Copy list to clipboard"
msgstr "Kopiere Liste in Zwischenablage"
#: .\cookbook\templates\shopping_list.html:48
msgid "Copied!"
msgstr "Kopiert!"
#: .\cookbook\templates\stats.html:4
msgid "Stats"
msgstr "Statistiken"
#: cookbook/templates/stats.html:17
#: .\cookbook\templates\stats.html:17
msgid "Number of objects"
msgstr "Anzahl der Objekte"
#: cookbook/templates/stats.html:20
#: .\cookbook\templates\stats.html:20
msgid "Recipes"
msgstr "Rezepte"
#: cookbook/templates/stats.html:24
#: .\cookbook\templates\stats.html:24
msgid "Recipe Imports"
msgstr "Rezept Importe"
#: cookbook/templates/stats.html:32
#: .\cookbook\templates\stats.html:32
msgid "Objects stats"
msgstr "Objekt Statistiken"
#: cookbook/templates/stats.html:35
#: .\cookbook\templates\stats.html:35
msgid "Recipes without Keywords"
msgstr "Rezepte ohne Schlagwort"
#: cookbook/views/api.py:63
#: .\cookbook\views\api.py:63
msgid "Sync successful!"
msgstr "Synchronisation erfolgreich!"
#: cookbook/views/api.py:66
#: .\cookbook\views\api.py:66
msgid "Error synchronizing with Storage"
msgstr "Fehler beim Synchronisieren"
#: cookbook/views/data.py:71
#: .\cookbook\views\data.py:71
#, python-format
msgid "Batch edit done. %(count)d recipe was updated."
msgid_plural "Batch edit done. %(count)d Recipes where updated."
@@ -443,65 +612,77 @@ msgstr[0] "Massenbearbeitung erfolgreich. %(count)d Rezept wurde aktualisiert."
msgstr[1] ""
"Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
#: cookbook/views/edit.py:88
#: .\cookbook\views\edit.py:109
msgid "Recipe saved!"
msgstr "Rezept gespeichert"
#: 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:111
msgid "There was an error saving this recipe!"
msgstr "Es gab einen Fehler beim Speichern des Rezepts"
#: cookbook/views/edit.py:139 cookbook/views/edit.py:182
#: .\cookbook\views\edit.py:160 .\cookbook\views\edit.py:203
msgid "You cannot edit this comment!"
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
#: cookbook/views/edit.py:158
#: .\cookbook\views\edit.py:179
msgid "Storage saved!"
msgstr "Speicherquelle gespeichert"
#: cookbook/views/edit.py:161
#: .\cookbook\views\edit.py:182
msgid "There was an error updating this storage backend.!"
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle"
#: cookbook/views/edit.py:208 cookbook/views/edit.py:309
#: cookbook/views/lists.py:34
#: .\cookbook\views\edit.py:229 .\cookbook\views\edit.py:385
#: .\cookbook\views\lists.py:34
msgid "Import"
msgstr "Rezept Importieren"
#: cookbook/views/edit.py:224 cookbook/views/edit.py:364
#: cookbook/views/new.py:110
#: .\cookbook\views\edit.py:245 .\cookbook\views\edit.py:440
#: .\cookbook\views\new.py:112
msgid "Recipe Book"
msgstr "Rezeptbuch"
#: cookbook/views/edit.py:246
#: .\cookbook\views\edit.py:283
msgid "Changes saved!"
msgstr "Änderungen gespeichert"
#: cookbook/views/edit.py:250
#: .\cookbook\views\edit.py:287
msgid "Error saving changes!"
msgstr "Fehler beim Speichern der Daten."
#: cookbook/views/edit.py:320
#: .\cookbook\views\edit.py:317
msgid "Units merged!"
msgstr "Einheiten zusammengeführt"
#: .\cookbook\views\edit.py:330
msgid "Ingredients merged!"
msgstr "Zutaten zusammengeführt"
#: .\cookbook\views\edit.py:396
msgid "Monitor"
msgstr "Monitor"
#: cookbook/views/edit.py:342 cookbook/views/lists.py:42
#: cookbook/views/new.py:62
#: .\cookbook\views\edit.py:418 .\cookbook\views\lists.py:42
#: .\cookbook\views\new.py:64
msgid "Storage Backend"
msgstr "Speicher Quelle"
#: cookbook/views/edit.py:375
#: .\cookbook\views\edit.py:451
msgid "Bookmarks"
msgstr "Lesezeichen"
#: cookbook/views/new.py:84
#: .\cookbook\views\new.py:86
msgid "Imported new recipe!"
msgstr "Importier neue Rezepte"
#: cookbook/views/views.py:42
#: .\cookbook\views\new.py:89
msgid "There was an error importing this recipe!"
msgstr "Beim importieren des Rezeptes ist ein Fehler aufgetreten"
#: .\cookbook\views\views.py:44
msgid "Comment saved!"
msgstr "Kommentar gespeichert"
#: cookbook/views/views.py:52
#: .\cookbook\views\views.py:54
msgid "Bookmark saved!"
msgstr "Lesezeichen gespeichert"

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.2 on 2020-01-17 14:55
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', '0007_auto_20191226_0852'),
]
operations = [
migrations.CreateModel(
name='MealPlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('meal', models.CharField(choices=[('BREAKFAST', 'Breakfast'), ('LUNCH', 'Lunch'), ('DINNER', 'Dinner'), ('OTHER', 'Other')], default='BREAKFAST', max_length=128)),
('note', models.TextField(blank=True)),
('date', models.DateField()),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.2 on 2020-01-30 09:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0008_mealplan'),
]
operations = [
migrations.CreateModel(
name='Unit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True)),
('description', models.TextField(blank=True, null=True)),
],
),
migrations.AddField(
model_name='recipeingredients',
name='unit_key',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.Unit'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.0.2 on 2020-01-30 09:59
from django.db import migrations
def migrate_ingredient_units(apps, schema_editor):
Unit = apps.get_model('cookbook', 'Unit')
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
for u in RecipeIngredients.objects.values('unit').distinct():
unit = Unit()
unit.name = u['unit']
unit.save()
for i in RecipeIngredients.objects.all():
i.unit_key = Unit.objects.get(name=i.unit)
i.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0009_auto_20200130_1056'),
]
operations = [
migrations.RunPython(migrate_ingredient_units),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.0.2 on 2020-01-30 10:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0010_auto_20200130_1059'),
]
operations = [
migrations.RemoveField(
model_name='recipeingredients',
name='unit',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.2 on 2020-01-30 10:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0011_remove_recipeingredients_unit'),
]
operations = [
migrations.RenameField(
model_name='recipeingredients',
old_name='unit_key',
new_name='unit',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.2 on 2020-02-13 22:15
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', '0012_auto_20200130_1116'),
]
operations = [
migrations.CreateModel(
name='UserPreference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('theme', models.CharField(choices=[('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly')], default='BOOTSTRAP', max_length=128)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.0.2 on 2020-02-13 22:32
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', '0013_userpreference'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='theme',
field=models.CharField(choices=[('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero')], default='BOOTSTRAP', max_length=128),
),
migrations.AlterField(
model_name='userpreference',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, unique=True),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.2 on 2020-02-13 22:34
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', '0014_auto_20200213_2332'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.2 on 2020-02-13 22:35
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', '0015_auto_20200213_2334'),
]
operations = [
migrations.RemoveField(
model_name='userpreference',
name='id',
),
migrations.AlterField(
model_name='userpreference',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.2 on 2020-02-16 21:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0016_auto_20200213_2335'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='theme',
field=models.CharField(choices=[('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero')], default='FLATLY', max_length=128),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.0.2 on 2020-02-16 22:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0017_auto_20200216_2257'),
]
operations = [
migrations.RenameModel(
old_name='RecipeIngredients',
new_name='RecipeIngredient',
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.0.2 on 2020-02-16 22:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0018_auto_20200216_2303'),
]
operations = [
migrations.CreateModel(
name='Ingredient',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True)),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.2 on 2020-02-16 22:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0019_ingredient'),
]
operations = [
migrations.AddField(
model_name='recipeingredient',
name='ingredient',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.Ingredient'),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.0.2 on 2020-02-16 22:09
from django.db import migrations
def migrate_ingredients(apps, schema_editor):
Ingredient = apps.get_model('cookbook', 'Ingredient')
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
for u in RecipeIngredient.objects.values('name').distinct():
ingredient = Ingredient()
ingredient.name = u['name']
ingredient.save()
for i in RecipeIngredient.objects.all():
i.ingredient = Ingredient.objects.get(name=i.name)
i.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0020_recipeingredient_ingredient'),
]
operations = [
migrations.RunPython(migrate_ingredients),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.0.2 on 2020-02-16 22:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0021_auto_20200216_2309'),
]
operations = [
migrations.RemoveField(
model_name='recipeingredient',
name='name',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.2 on 2020-02-16 22:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0022_remove_recipeingredient_name'),
]
operations = [
migrations.RenameField(
model_name='recipeingredient',
old_name='ingredient',
new_name='name',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.2 on 2020-02-16 22:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0023_auto_20200216_2311'),
]
operations = [
migrations.RenameField(
model_name='recipeingredient',
old_name='name',
new_name='ingredient',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.2 on 2020-02-16 23:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0024_auto_20200216_2313'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='nav_color',
field=models.CharField(choices=[('PRIMARY', 'Primary'), ('SECONDARY', 'Secondary'), ('SUCCESS', 'Success'), ('INFO', 'Info'), ('WARNING', 'Warning'), ('DANGER', 'Danger'), ('LIGHT', 'Light'), ('DARK', 'Dark')], default='PRIMARY', max_length=128),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.2 on 2020-02-19 15:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0025_userpreference_nav_color'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='cors_link',
field=models.CharField(blank=True, max_length=1024, null=True),
),
migrations.AlterField(
model_name='recipe',
name='link',
field=models.CharField(blank=True, max_length=512, null=True),
),
]

View File

@@ -1,8 +1,34 @@
from django.contrib.auth.models import User
from django.utils.translation import gettext as _
from django.db import models
class UserPreference(models.Model):
# Themes
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
THEMES = ((BOOTSTRAP, 'Bootstrap'), (DARKLY, 'Darkly'), (FLATLY, 'Flatly'), (SUPERHERO, 'Superhero'))
# Nav colors
PRIMARY = 'PRIMARY'
SECONDARY = 'SECONDARY'
SUCCESS = 'SUCCESS'
INFO = 'INFO'
WARNING = 'WARNING'
DANGER = 'DANGER'
LIGHT = 'LIGHT'
DARK = 'DARK'
COLORS = ((PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), (SUCCESS, 'Success'), (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), (DARK, 'Dark'))
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
class Storage(models.Model):
DROPBOX = 'DB'
NEXTCLOUD = 'NEXTCLOUD'
@@ -58,7 +84,8 @@ class Recipe(models.Model):
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="")
link = models.CharField(max_length=512, null=True, blank=True)
cors_link = models.CharField(max_length=1024, null=True, blank=True)
keywords = models.ManyToManyField(Keyword, blank=True)
working_time = models.IntegerField(default=0)
waiting_time = models.IntegerField(default=0)
@@ -75,12 +102,30 @@ class Recipe(models.Model):
return ' '.join([(x.icon + x.name) for x in self.keywords.all()])
class RecipeIngredients(models.Model):
name = models.CharField(max_length=128)
class Unit(models.Model):
name = models.CharField(unique=True, max_length=128)
description = models.TextField(blank=True, null=True)
def __str__(self):
return self.name
class Ingredient(models.Model):
name = models.CharField(unique=True, max_length=128)
def __str__(self):
return self.name
class RecipeIngredient(models.Model):
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT, null=True)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
unit = models.CharField(max_length=128)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True)
amount = models.DecimalField(default=0, decimal_places=2, max_digits=16)
def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.ingredient)
class Comment(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
@@ -89,6 +134,9 @@ class Comment(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.text
class RecipeImport(models.Model):
name = models.CharField(max_length=128)
@@ -115,3 +163,20 @@ class RecipeBookEntry(models.Model):
def __str__(self):
return self.recipe.name
class MealPlan(models.Model):
BREAKFAST = 'BREAKFAST'
LUNCH = 'LUNCH'
DINNER = 'DINNER'
OTHER = 'OTHER'
MEAL_TYPES = ((BREAKFAST, _('Breakfast')), (LUNCH, _('Lunch')), (DINNER, _('Dinner')), (OTHER, _('Other')),)
user = models.ForeignKey(User, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
meal = models.CharField(choices=MEAL_TYPES, max_length=128, default=BREAKFAST)
note = models.TextField(blank=True)
date = models.DateField()
def __str__(self):
return self.meal + ' (' + str(self.date) + ') ' + str(self.recipe)

View File

@@ -1,3 +1,4 @@
import base64
import os
from datetime import datetime
@@ -88,6 +89,16 @@ class Dropbox(Provider):
response = Dropbox.create_share_link(recipe)
return response['url']
@staticmethod
def get_base64_file(recipe):
if not recipe.link:
recipe.link = Dropbox.get_share_link(recipe)
recipe.save()
response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
return base64.b64encode(response.content)
@staticmethod
def rename_file(recipe, new_name):
url = "https://api.dropboxapi.com/2/files/move_v2"

View File

@@ -1,8 +1,10 @@
import base64
import os
import tempfile
from datetime import datetime
import webdav3.client as wc
import requests
from io import BytesIO
from requests.auth import HTTPBasicAuth
from cookbook.models import Recipe, RecipeImport, SyncLog
@@ -81,6 +83,20 @@ class Nextcloud(Provider):
return Nextcloud.create_share_link(recipe)
@staticmethod
def get_base64_file(recipe):
client = Nextcloud.get_client(recipe.storage)
tmp_file_path = tempfile.gettempdir() + '/' + recipe.name + '.pdf'
client.download_file(remote_path=recipe.file_path, local_path=tmp_file_path)
val = base64.b64encode(open(tmp_file_path, 'rb').read())
os.remove(tmp_file_path)
return val
@staticmethod
def rename_file(recipe, new_name):
client = Nextcloud.get_client(recipe.storage)

View File

@@ -11,10 +11,14 @@ class Provider:
def get_share_link(recipe):
raise Exception('Method not implemented in storage provider')
@staticmethod
def get_base64_file(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):
def delete_file(recipe):
raise Exception('Method not implemented in storage provider')

View File

@@ -0,0 +1,3 @@
$(document).ready(function () {
$('.selectwidget').select2();
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12
cookbook/static/themes/darkly.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
cookbook/static/themes/flatly.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,722 @@
/*!
* Select2 Bootstrap Theme v0.1.0-beta.10 (https://select2.github.io/select2-bootstrap-theme)
* Copyright 2015-2017 Florian Kissling and contributors (https://github.com/select2/select2-bootstrap-theme/graphs/contributors)
* Licensed under MIT (https://github.com/select2/select2-bootstrap-theme/blob/master/LICENSE)
*/
.select2-container--bootstrap {
display: block;
/*------------------------------------* #COMMON STYLES
\*------------------------------------*/
/**
* Search field in the Select2 dropdown.
*/
/**
* No outline for all search fields - in the dropdown
* and inline in multi Select2s.
*/
/**
* Adjust Select2's choices hover and selected styles to match
* Bootstrap 3's default dropdown styles.
*
* @see http://getbootstrap.com/components/#dropdowns
*/
/**
* Clear the selection.
*/
/**
* Address disabled Select2 styles.
*
* @see https://select2.github.io/examples.html#disabled
* @see http://getbootstrap.com/css/#forms-control-disabled
*/
/*------------------------------------* #DROPDOWN
\*------------------------------------*/
/**
* Dropdown border color and box-shadow.
*/
/**
* Limit the dropdown height.
*/
/*------------------------------------* #SINGLE SELECT2
\*------------------------------------*/
/*------------------------------------* #MULTIPLE SELECT2
\*------------------------------------*/
/**
* Address Bootstrap control sizing classes
*
* 1. Reset Bootstrap defaults.
* 2. Adjust the dropdown arrow button icon position.
*
* @see http://getbootstrap.com/css/#forms-control-sizes
*/
/* 1 */
/*------------------------------------* #RTL SUPPORT
\*------------------------------------*/
}
.select2-container--bootstrap .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
color: #555555;
font-size: 14px;
outline: 0;
}
.select2-container--bootstrap .select2-selection.form-control {
border-radius: 4px;
}
.select2-container--bootstrap .select2-search--dropdown .select2-search__field {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
color: #555555;
font-size: 14px;
}
.select2-container--bootstrap .select2-search__field {
outline: 0;
/* Firefox 18- */
/**
* Firefox 19+
*
* @see http://stackoverflow.com/questions/24236240/color-for-styled-placeholder-text-is-muted-in-firefox
*/
}
.select2-container--bootstrap .select2-search__field::-webkit-input-placeholder {
color: #999;
}
.select2-container--bootstrap .select2-search__field:-moz-placeholder {
color: #999;
}
.select2-container--bootstrap .select2-search__field::-moz-placeholder {
color: #999;
opacity: 1;
}
.select2-container--bootstrap .select2-search__field:-ms-input-placeholder {
color: #999;
}
.select2-container--bootstrap .select2-results__option {
padding: 6px 12px;
color: #555555;
/**
* Disabled results.
*
* @see https://select2.github.io/examples.html#disabled-results
*/
/**
* Hover state.
*/
/**
* Selected state.
*/
}
.select2-container--bootstrap .select2-results__option[role=group] {
padding: 0;
}
.select2-container--bootstrap .select2-results__option[aria-disabled=true] {
color: #777777;
cursor: not-allowed;
}
.select2-container--bootstrap .select2-results__option[aria-selected=true] {
background-color: #f5f5f5;
color: #262626;
}
.select2-container--bootstrap .select2-results__option--highlighted[aria-selected] {
background-color: #337ab7;
color: #fff;
}
.select2-container--bootstrap .select2-results__option .select2-results__option {
padding: 6px 12px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option {
margin-left: -12px;
padding-left: 24px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -24px;
padding-left: 36px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -36px;
padding-left: 48px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -48px;
padding-left: 60px;
}
.select2-container--bootstrap .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -60px;
padding-left: 72px;
}
.select2-container--bootstrap .select2-results__group {
color: #777777;
display: block;
padding: 6px 12px;
font-size: 12px;
line-height: 1.42857143;
white-space: nowrap;
}
.select2-container--bootstrap.select2-container--focus .select2-selection, .select2-container--bootstrap.select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s;
border-color: #66afe9;
}
.select2-container--bootstrap.select2-container--open {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
/**
* Handle border radii of the container when the dropdown is showing.
*/
}
.select2-container--bootstrap.select2-container--open .select2-selection .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 4px 4px 4px;
}
.select2-container--bootstrap.select2-container--open.select2-container--below .select2-selection {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-color: transparent;
}
.select2-container--bootstrap.select2-container--open.select2-container--above .select2-selection {
border-top-right-radius: 0;
border-top-left-radius: 0;
border-top-color: transparent;
}
.select2-container--bootstrap .select2-selection__clear {
color: #999;
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px;
}
.select2-container--bootstrap .select2-selection__clear:hover {
color: #333;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection {
border-color: #ccc;
-webkit-box-shadow: none;
box-shadow: none;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection,
.select2-container--bootstrap.select2-container--disabled .select2-search__field {
cursor: not-allowed;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection,
.select2-container--bootstrap.select2-container--disabled .select2-selection--multiple .select2-selection__choice {
background-color: #eeeeee;
}
.select2-container--bootstrap.select2-container--disabled .select2-selection__clear,
.select2-container--bootstrap.select2-container--disabled .select2-selection--multiple .select2-selection__choice__remove {
display: none;
}
.select2-container--bootstrap .select2-dropdown {
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
border-color: #66afe9;
overflow-x: hidden;
margin-top: -1px;
}
.select2-container--bootstrap .select2-dropdown--above {
-webkit-box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.175);
margin-top: 1px;
}
.select2-container--bootstrap .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
}
.select2-container--bootstrap .select2-selection--single {
height: 34px;
line-height: 1.42857143;
padding: 6px 24px 6px 12px;
/**
* Adjust the single Select2's dropdown arrow button appearance.
*/
}
.select2-container--bootstrap .select2-selection--single .select2-selection__arrow {
position: absolute;
bottom: 0;
right: 12px;
top: 0;
width: 4px;
}
.select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
border-color: #999 transparent transparent transparent;
border-style: solid;
border-width: 4px 4px 0 4px;
height: 0;
left: 0;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--bootstrap .select2-selection--single .select2-selection__rendered {
color: #555555;
padding: 0;
}
.select2-container--bootstrap .select2-selection--single .select2-selection__placeholder {
color: #999;
}
.select2-container--bootstrap .select2-selection--multiple {
min-height: 34px;
padding: 0;
height: auto;
/**
* Make Multi Select2's choices match Bootstrap 3's default button styles.
*/
/**
* Minus 2px borders.
*/
/**
* Clear the selection.
*/
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__rendered {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
display: block;
line-height: 1.42857143;
list-style: none;
margin: 0;
overflow: hidden;
padding: 0;
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__placeholder {
color: #999;
float: left;
margin-top: 5px;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
color: #555555;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
cursor: default;
float: left;
margin: 5px 0 0 6px;
padding: 0 6px;
}
.select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
background: transparent;
padding: 0 12px;
height: 32px;
line-height: 1.42857143;
margin-top: 0;
min-width: 5em;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 3px;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333;
}
.select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 6px;
}
.select2-container--bootstrap .select2-selection--single.input-sm,
.input-group-sm .select2-container--bootstrap .select2-selection--single,
.form-group-sm .select2-container--bootstrap .select2-selection--single {
border-radius: 3px;
font-size: 12px;
height: 30px;
line-height: 1.5;
padding: 5px 22px 5px 10px;
/* 2 */
}
.select2-container--bootstrap .select2-selection--single.input-sm .select2-selection__arrow b,
.input-group-sm .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b,
.form-group-sm .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
margin-left: -5px;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple {
min-height: 30px;
border-radius: 3px;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-selection__choice,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
font-size: 12px;
line-height: 1.5;
margin: 4px 0 0 5px;
padding: 0 5px;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-search--inline .select2-search__field,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
padding: 0 10px;
font-size: 12px;
height: 28px;
line-height: 1.5;
}
.select2-container--bootstrap .select2-selection--multiple.input-sm .select2-selection__clear,
.input-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear,
.form-group-sm .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 5px;
}
.select2-container--bootstrap .select2-selection--single.input-lg,
.input-group-lg .select2-container--bootstrap .select2-selection--single,
.form-group-lg .select2-container--bootstrap .select2-selection--single {
border-radius: 6px;
font-size: 18px;
height: 46px;
line-height: 1.3333333;
padding: 10px 31px 10px 16px;
/* 1 */
}
.select2-container--bootstrap .select2-selection--single.input-lg .select2-selection__arrow,
.input-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow,
.form-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow {
width: 5px;
}
.select2-container--bootstrap .select2-selection--single.input-lg .select2-selection__arrow b,
.input-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b,
.form-group-lg .select2-container--bootstrap .select2-selection--single .select2-selection__arrow b {
border-width: 5px 5px 0 5px;
margin-left: -5px;
margin-left: -10px;
margin-top: -2.5px;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple {
min-height: 46px;
border-radius: 6px;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-selection__choice,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__choice {
font-size: 18px;
line-height: 1.3333333;
border-radius: 4px;
margin: 9px 0 0 8px;
padding: 0 10px;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-search--inline .select2-search__field,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-search--inline .select2-search__field {
padding: 0 16px;
font-size: 18px;
height: 44px;
line-height: 1.3333333;
}
.select2-container--bootstrap .select2-selection--multiple.input-lg .select2-selection__clear,
.input-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear,
.form-group-lg .select2-container--bootstrap .select2-selection--multiple .select2-selection__clear {
margin-top: 10px;
}
.select2-container--bootstrap .select2-selection.input-lg.select2-container--open .select2-selection--single {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
}
.select2-container--bootstrap .select2-selection.input-lg.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 5px 5px 5px;
}
.input-group-lg .select2-container--bootstrap .select2-selection.select2-container--open .select2-selection--single {
/**
* Make the dropdown arrow point up while the dropdown is visible.
*/
}
.input-group-lg .select2-container--bootstrap .select2-selection.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #999 transparent;
border-width: 0 5px 5px 5px;
}
.select2-container--bootstrap[dir="rtl"] {
/**
* Single Select2
*
* 1. Makes sure that .select2-selection__placeholder is positioned
* correctly.
*/
/**
* Multiple Select2
*/
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single {
padding-left: 24px;
padding-right: 12px;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 0;
padding-left: 0;
text-align: right;
/* 1 */
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 12px;
right: auto;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--single .select2-selection__arrow b {
margin-left: 0;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice,
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 0;
margin-right: 6px;
}
.select2-container--bootstrap[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
/*------------------------------------* #ADDITIONAL GOODIES
\*------------------------------------*/
/**
* Address Bootstrap's validation states
*
* If a Select2 widget parent has one of Bootstrap's validation state modifier
* classes, adjust Select2's border colors and focus states accordingly.
* You may apply said classes to the Select2 dropdown (body > .select2-container)
* via JavaScript match Bootstraps' to make its styles match.
*
* @see http://getbootstrap.com/css/#forms-control-validation
*/
.has-warning .select2-dropdown,
.has-warning .select2-selection {
border-color: #8a6d3b;
}
.has-warning .select2-container--focus .select2-selection,
.has-warning .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
border-color: #66512c;
}
.has-warning.select2-drop-active {
border-color: #66512c;
}
.has-warning.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #66512c;
}
.has-error .select2-dropdown,
.has-error .select2-selection {
border-color: #a94442;
}
.has-error .select2-container--focus .select2-selection,
.has-error .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
border-color: #843534;
}
.has-error.select2-drop-active {
border-color: #843534;
}
.has-error.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #843534;
}
.has-success .select2-dropdown,
.has-success .select2-selection {
border-color: #3c763d;
}
.has-success .select2-container--focus .select2-selection,
.has-success .select2-container--open .select2-selection {
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
border-color: #2b542c;
}
.has-success.select2-drop-active {
border-color: #2b542c;
}
.has-success.select2-drop-active.select2-drop.select2-drop-above {
border-top-color: #2b542c;
}
/**
* Select2 widgets in Bootstrap Input Groups
*
* @see http://getbootstrap.com/components/#input-groups
* @see https://github.com/twbs/bootstrap/blob/master/less/input-groups.less
*/
/**
* Reset rounded corners
*/
.input-group > .select2-hidden-accessible:first-child + .select2-container--bootstrap > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:first-child + .select2-container--bootstrap > .selection > .select2-selection.form-control {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.input-group > .select2-hidden-accessible:not(:first-child) + .select2-container--bootstrap:not(:last-child) > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:not(:first-child) + .select2-container--bootstrap:not(:last-child) > .selection > .select2-selection.form-control {
border-radius: 0;
}
.input-group > .select2-hidden-accessible:not(:first-child):not(:last-child) + .select2-container--bootstrap:last-child > .selection > .select2-selection,
.input-group > .select2-hidden-accessible:not(:first-child):not(:last-child) + .select2-container--bootstrap:last-child > .selection > .select2-selection.form-control {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.input-group > .select2-container--bootstrap {
display: table;
table-layout: fixed;
position: relative;
z-index: 2;
width: 100%;
margin-bottom: 0;
/**
* Adjust z-index like Bootstrap does to show the focus-box-shadow
* above appended buttons in .input-group and .form-group.
*/
/**
* Adjust alignment of Bootstrap buttons in Bootstrap Input Groups to address
* Multi Select2's height which - depending on how many elements have been selected -
* may grow taller than its initial size.
*
* @see http://getbootstrap.com/components/#input-groups
*/
}
.input-group > .select2-container--bootstrap > .selection > .select2-selection.form-control {
float: none;
}
.input-group > .select2-container--bootstrap.select2-container--open, .input-group > .select2-container--bootstrap.select2-container--focus {
z-index: 3;
}
.input-group > .select2-container--bootstrap,
.input-group > .select2-container--bootstrap .input-group-btn,
.input-group > .select2-container--bootstrap .input-group-btn .btn {
vertical-align: top;
}
/**
* Temporary fix for https://github.com/select2/select2-bootstrap-theme/issues/9
*
* Provides `!important` for certain properties of the class applied to the
* original `<select>` element to hide it.
*
* @see https://github.com/select2/select2/pull/3301
* @see https://github.com/fk/select2/commit/31830c7b32cb3d8e1b12d5b434dee40a6e753ada
*/
.form-control.select2-hidden-accessible {
position: absolute !important;
width: 1px !important;
}
/**
* Display override for inline forms
*/
@media (min-width: 768px) {
.form-inline .select2-container--bootstrap {
display: inline-block;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,7 @@ from .models import *
class RecipeTable(tables.Table):
id = tables.LinkColumn('edit_recipe', args=[A('id')])
name = tables.TemplateColumn(
"<a href='#' onClick='openRecipe({{record.id}})'>{{record.name}}</a>")
name = tables.LinkColumn('view_recipe', args=[A('id')])
all_tags = tables.Column(
attrs={'td': {'class': 'd-none d-lg-table-cell'}, 'th': {'class': 'd-none d-lg-table-cell'}})

View File

@@ -1,5 +1,6 @@
{% load static %}
{% load i18n %}
{% load theming_tags %}
<html>
<head>
@@ -18,11 +19,11 @@
<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"
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.4.1.js"
integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
@@ -31,13 +32,18 @@
crossorigin="anonymous"></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>
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/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"/>
<link rel="stylesheet"
href="{% static 'themes/select2-bootstrap-theme.css' %}"
crossorigin="anonymous"/>
<script type="text/javascript">
$.fn.select2.defaults.set("theme", "bootstrap");
</script>
@@ -60,7 +66,7 @@
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}" id="id_main_nav">
<!--<a class="navbar-brand" href="{% url 'index' %}">{% trans 'Cookbook' %}</a>-->
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
@@ -68,12 +74,21 @@
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<li class="nav-item {% if request.resolver_match.url_name == "index" %}active{% endif %}">
<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 class="nav-item {% if request.resolver_match.url_name == "view_books" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_books' %}"><i class="fas fa-bookmark"></i> {% trans 'Books' %}
</a>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_plan" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_plan' %}"><i class="fas fa-calendar"></i> {% trans 'Meal-Plan' %}
</a>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_shopping" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_shopping' %}"><i class="fas fa-shopping-cart"></i> {% trans 'Shopping' %}
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
@@ -102,15 +117,24 @@
class="fas fa-history"></i> {% trans 'Import Log' %}</a>
<a class="dropdown-item" href="{% url 'data_stats' %}"><i
class="fas fa-chart-line"></i> {% trans 'Statistics' %}</a>
<a class="dropdown-item" href="{% url 'edit_ingredient' %}"><i
class="fas fa-balance-scale"></i> {% trans 'Units & Ingredients' %}</a>
</div>
</li>
</ul>
<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>
<li class="nav-item {% if request.resolver_match.url_name == "view_settings" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_settings' %}"><i
class="fas fa-user-cog"></i> {% trans 'Settings' %}</a>
</li>
{% if user.is_superuser %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield"></i> {% trans 'Admin' %}</a>
</li>
{% endif %}
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'logout' %}">{% trans 'Logout' %} {{ user.get_username }} <i
@@ -141,5 +165,8 @@
{% endblock %}
</div>
{% block script %}
{% endblock script %}
</body>
</html>

View File

@@ -41,7 +41,7 @@
{% 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>
<li><a href="{% url 'view_recipe' 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>
@@ -58,5 +58,4 @@
<br/>
{% endfor %}
{% include 'include/recipe_open_modal.html' %}
{% endblock %}

View File

@@ -2,20 +2,22 @@
{% load crispy_forms_tags %}
{% load i18n %}
{% load custom_tags %}
{% load theming_tags %}
{% load static %}
{% block title %}{% trans 'Edit Recipe' %}{% endblock %}
{% block extra_head %}
<script src="{% static 'tabulator/tabulator.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'tabulator/tabulator_bootstrap4.min.css' %}"/>
<link rel="stylesheet" href="{% tabulator_theme_url request %}"/>
{% endblock %}
{% block content %}
<h3>{% trans 'Edit Recipe' %}</h3>
<form action="." method="post" enctype="multipart/form-data">
<form action="." method="post" enctype="multipart/form-data" id="id_form">
{% csrf_token %}
{% for field in form %}
@@ -26,13 +28,21 @@
<label>{% trans 'Ingredients' %}</label>
<div id="ingredients-table"></div>
<br>
<div class="table-controls">
<button class="btn" id="new_empty" type="button"><i class="fas fa-plus-circle"></i></button>
<div class="table-controls" style="text-align: center">
<button class="btn btn-success" id="new_empty" type="button" style="min-width: 20vw"><i
class="fas fa-plus-circle"></i></button>
<button type="button" class="btn btn-secondary" data-container="body" data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
data-content="{% trans 'Use <b>Ctrl</b>+<b>Space</b> to insert new Ingredient!<br/>You can also save the recipe using <b>Ctrl</b>+<b>Shift</b>+<b>S</b>.' %}">
<i class="fas fa-question"></i>
</button>
<br/>
<br/>
</div>
{% endif %}
{% endfor %}
<input type="hidden" id="ingredients_data_input" name="ingredients">
<hr>
<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 %}"
@@ -47,6 +57,57 @@
</form>
<script>
$(function () {
$('[data-toggle="popover"]').popover()
});
$('.popover-dismiss').popover({
trigger: 'focus'
});
let select2UnitEditor = function (cell, onRendered, success, cancel, editorParams) {
return select2Editor(cell, onRendered, success, cancel, editorParams, '{% url 'dal_unit' %}')
};
let select2IngredientEditor = function (cell, onRendered, success, cancel, editorParams) {
return select2Editor(cell, onRendered, success, cancel, editorParams, '{% url 'dal_ingredient' %}')
};
let select2Editor = function (cell, onRendered, success, cancel, editorParams, url) {
let editor = document.createElement("select");
editor.setAttribute("class", "form-control");
editor.setAttribute("style", "height: 100%; color: #00ff00");
onRendered(function () {
let select_2 = $(editor);
select_2.select2({
tags: true,
ajax: {
url: url,
dataType: 'json'
}
});
select_2.select2('open');
select_2.on('select2:select', function (e) {
success(e.params.data.text);
});
select_2.on('select2:close', function (e) {
if (e.target.textContent === "") {
cancel();
}
});
});
//add editor to cell
return editor;
};
function selectText(node) {
if (document.body.createTextRange) {
@@ -69,15 +130,15 @@
$(document).ready(function () {
$('#id_keywords').select2();
var ingredients = {{ ingredients|safe }}
let ingredients = {{ ingredients|safe }}
ingredients.forEach(function (cur, i) {
cur.delete = false
})
});
var data = ingredients
let data = ingredients;
var table = new Tabulator("#ingredients-table", {
let table = new Tabulator("#ingredients-table", {
index: "id",
layout: "fitColumns",
reactiveData: true,
@@ -85,26 +146,44 @@
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: "<i class='fas fa-sort'></i>",
rowHandle: true,
formatter: "handle",
headerSort: false,
frozen: true,
width: 36,
minWidth: 36
},
{
title: "{% trans 'Ingredient' %}",
field: "name",
field: "ingredient__name",
validator: "required",
editor: "input"
editor: select2IngredientEditor
},
{title: "{% trans 'Amount' %}", field: "amount", validator: "required", editor: "input"},
{title: "{% trans 'Unit' %}", field: "unit", validator: "required", editor: "input"},
{
title: "{% trans 'Delete' %}",
field: "delete",
title: "{% trans 'Unit' %}",
field: "unit__name",
validator: "required",
editor: select2UnitEditor
},
{
formatter: function (cell, formatterParams) {
return "<span style='color:red'><i class=\"fas fa-trash-alt\"></i></span>"
},
align: "center",
editor: true,
formatter: "tickCross"
title: "{% trans 'Delete' %}",
headerSort: false,
cellClick: function (e, cell) {
if (confirm('{% trans 'Are you sure that you want to delete this ingredient?' %}'))
cell.getRow().delete();
}
},
{title: "id", field: "id", visible: false}
],
dataEdited: function (data) {
$('#ingredients_data_input').val(JSON.stringify(data))
$('#id_ingredients').val(JSON.stringify(data))
data.forEach(function (cur, i) {
if (cur.delete) {
@@ -113,24 +192,36 @@
})
},
cellClick: function (e, cell) {
input = cell.getElement().childNodes[0]
input.focus()
input.select()
if (cell._cell.column.definition.editor === "input") {
input = cell.getElement().childNodes[0];
input.focus();
input.select();
}
},
});
// load initial value
$('#ingredients_data_input').val(JSON.stringify(data))
$('#id_ingredients').val(JSON.stringify(data))
document.getElementById("new_empty").addEventListener("click", function () {
function addIngredientRow() {
data.push({
name: "{% trans 'Ingredient' %}",
ingredient__name: "{% trans 'Ingredient' %}",
amount: "100",
unit: "g",
unit__name: "g",
id: Math.floor(Math.random() * 10000000),
delete: false,
});
});
}
document.onkeyup = function (e) {
if (e.shiftKey && e.ctrlKey && (e.which === 83 || e.keyCode === 83)) {
$('#id_form').submit()
} else if (e.ctrlKey && (e.which === 83 || e.keyCode === 32)) {
addIngredientRow();
}
};
document.getElementById("new_empty").addEventListener("click", addIngredientRow);
});
</script>

View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load crispy_forms_tags %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Cookbook" %}{% endblock %}
{% block extra_head %}
{{ units_form.media }}
{% endblock %}
{% block content %}
<h2><i class="fas fa-shopping-cart"></i> {% trans 'Edit Ingredients' %}</h2>
{% blocktrans %}
The following form can be used if, accidentally, two (or more) units or ingredients where created that should be
the same.
It merges two units or ingredients and updates all recipes using them.
{% endblocktrans %}
<br/>
<br/>
<h4>{% trans 'Units' %}</h4>
<form action="{% url 'edit_ingredient' %}" method="post">
{% csrf_token %}
{{ units_form|crispy }}
<button class="btn btn-danger" type="submit"
onclick="confirm('{% trans 'Are you sure that you want to merge these two units ?' %}')"><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
<h4>{% trans 'Ingredients' %}</h4>
<form action="{% url 'edit_ingredient' %}" method="post">
{% csrf_token %}
{{ ingredients_form|crispy }}
<button class="btn btn-danger" type="submit"
onclick="confirm('{% trans 'Are you sure that you want to merge these two ingredients ?' %}')"><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
{% endblock %}

View File

@@ -22,7 +22,7 @@
{{ form|crispy }}
<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"><i class="far fa-eye"></i> {% trans 'View' %}</a>
{% endif %}

View File

@@ -23,4 +23,15 @@
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
{% if default_recipe %}
<script type="text/javascript">
$(document).ready(function () {
$('#id_recipe').val({{ default_recipe.pk }}).trigger('change');
});
</script>
{% endif %}
{% endblock %}

View File

@@ -43,15 +43,12 @@
</div>
<script type="text/javascript">
function openRecipe(id, force_external = false) {
function openRecipe(id) {
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);
}
var url = "{% url 'api_get_external_file_link' recipe_id=12345 %}".replace(/12345/, id);
link.text("{% trans 'Open Recipe' %}");
$('#modal_recipe').modal('show');

View File

@@ -46,6 +46,9 @@
<div>
{{ filter.form.keywords | as_crispy_field }}
</div>
<div>
{{ filter.form.ingredients | as_crispy_field }}
</div>
</div>
</div>
</form>
@@ -60,6 +63,4 @@
</div>
{% endif %}
{% include 'include/recipe_open_modal.html' %}
{% endblock %}

View File

@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
{% block extra_head %}
{{ form.media }}
{% endblock %}
{% block content %}
<h3>
{% trans 'Meal-Plan' %} <a href="{% url 'new_plan' %}"><i class="fas fa-plus-circle"></i></a>
</h3>
<div class="row">
<div class="col-md-12" style="text-align: center">
<form action="{% url 'view_plan' %}" method="post">
{% csrf_token %}
<label>{% trans 'Week' %}
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary" id="btn_prev"
onclick="$('#id_week').val('{{ surrounding_weeks.prev }}'); document.forms[0].submit()">
<i class="fas fa-arrow-left"></i>
</button>
</div>
<input name="week" id="id_week" class="form-control" type="week"
onchange="document.forms[0].submit()" value="{{ js_week }}">
<div class="input-group-append">
<button class="btn btn-outline-secondary" id="btn_next"
onclick="$('#id_week').val('{{ surrounding_weeks.next }}'); document.forms[0].submit()">
<i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
</label>
</form>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 table-responsive">
<table class="table table-bordered">
<tr style="text-align: center">
{% for d in days %}
<th>{{ d | date:"l" }}<br/>{{ d }}</th>
{% endfor %}
</tr>
{% for plan_key, plan_value in plan.items %}
<tr>
<td colspan="7" style="text-align: center"><h5>{{ plan_value.type_name }}</h5></td>
</tr>
<tr>
{% for day_key, days_value in plan_value.days.items %}
<td>
{% for mp in days_value %}
<a href="{% url 'edit_plan' mp.pk %}"><i class="fas fa-edit"></i></a>
<a href="{% url 'view_recipe' mp.recipe.id %}">{{ mp.recipe.name }}</a><br/>
{% endfor %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}

View File

@@ -9,6 +9,19 @@
{% 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">
<!-- prevent weired character stuff escaping the pdf box -->
<style>
.textLayer > span {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
</style>
{% endblock %}
{% block content %}
@@ -20,6 +33,12 @@
<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>
{% if ingredients %}
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}"><i
class="fas fa-shopping-cart"></i></a>
{% endif %}
<a class="btn btn-info" href="{% url 'new_plan' %}?recipe={{ recipe.pk }}"><i
class="fas fa-calendar"></i></a>
</div>
</div>
@@ -93,7 +112,7 @@
</div>
</td>
<td style="font-size: large">{{ i.name }}</td>
<td style="font-size: large">{{ i.ingredient.name }}</td>
</tr>
{% endfor %}
</table>
@@ -113,40 +132,111 @@
</div>
{% endif %}
</div>
{% if recipe.ingredients or recipe.image %}
{% if ingredients or recipe.image %}
<br/>
<br/>
{% endif %}
{% if recipe.instructions %}
{{ recipe.instructions | markdown | safe }}
{% endif %}
<div style="font-size: large">
{% if recipe.instructions %}
{{ recipe.instructions | markdown | safe }}
{% endif %}
</div>
{% if recipe.storage %}
<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 %}
<div class="row">
{% if recipe.internal %}
<a href='#' onClick='openRecipe({{ recipe.id }})'
class="d-print-none">{% trans 'View external recipe' %} <i class="fas fa-external-link-alt"></i></a>
{% else %}
{% if not recipe.internal %}
<br/>
<br/>
<br/>
<div class="card border-info">
<div class="card-body text-info">
<h5 class="card-title">{% trans 'External recipe' %}</h5>
<p class="card-text">
{% blocktrans %}
This is an external recipe, which means you can only view it by opening the link above.
You can convert this recipe to a fancy recipe by pressing the convert button. The original file
will still be accessible.
{% endblocktrans %}.
<br/>
<br/>
<a href="{% url 'edit_convert_recipe' recipe.pk %}"
class="card-link btn btn-info">{% trans 'Convert now!' %}</a>
</p>
</div>
<div class="col col-12" style="margin-top: 2vh">
<div class="loader" id="id_loader"></div>
<div id="viewerContainer" class="border">
<div id="viewer" class="pdfViewer"></div>
</div>
<div class="alert alert-warning" role="alert" id="id_warning_no_preview" style="display: none">
{% trans 'Cloud not show a file preview. Maybe its not a PDF ?' %}
</div>
</div>
<div class="col col-12" style="margin-top: 2vh">
<div class="card border-info">
<div class="card-body text-info">
<h5 class="card-title">{% trans 'External recipe' %}</h5>
<p class="card-text">
{% blocktrans %}
This is an external recipe, which means you can only view it by opening the link
above.
You can convert this recipe to a fancy recipe by pressing the convert button. The
original
file
will still be accessible.
{% endblocktrans %}.
<br/>
<br/>
<a href="{% url 'edit_convert_recipe' recipe.pk %}"
class="card-link btn btn-info">{% trans 'Convert now!' %}</a>
<a href='#' onClick='openRecipe({{ recipe.id }})'
class="d-print-none btn btn-warning">{% trans 'View external recipe' %} <i
class="fas fa-external-link-alt"></i></a>
</p>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf.min.js"
integrity="sha256-J4Z8Fhj2MITUakMQatkqOVdtqodUlwHtQ/ey6fSsudE="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf_viewer.js"
integrity="sha256-JW7ackRikw8/UM/hHV6vKaZBYc+t2ZQ77sd3LWR8vh8="
crossorigin="anonymous"></script>
<script type="text/javascript">
var url = "{% url 'api_get_recipe_file' recipe_id=12345 %}".replace(/12345/, {{ recipe.id }});
$('#viewerContainer').hide();
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
var base64Pdf = atob(this.responseText);
$('#id_loader').hide();
$('#viewerContainer').show();
var container = document.getElementById("viewerContainer");
var pdfViewer = new pdfjsViewer.PDFViewer({
container: container,
});
document.addEventListener("pagesinit", function () {
// We can use pdfViewer now, e.g. let's change default scale.
pdfViewer.currentScaleValue = "page-width";
});
var loadingTask = pdfjsLib.getDocument({
data: base64Pdf
});
loadingTask.promise.then(function (pdfDocument) {
// Document loaded, specifying document for the viewer and
// the (optional) linkService.
pdfViewer.setDocument(pdfDocument);
});
}
};
xhttp.open("GET", url, true);
xhttp.send();
</script>
{% endif %}
</div>
{% endif %}

View File

@@ -41,7 +41,7 @@
{% endif %}
</div>
<input type="submit" class="btn btn-primary" value="login"/>
<input type="submit" class="btn btn-primary" value="{% trans 'Login' %}"/>
<input type="hidden" name="next" value="{{ next }}"/>
</div>
</form>

View File

@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load static %}
{% block title %}{% trans 'Settings' %}{% endblock %}
{% block content %}
<h3>
{% trans 'Settings' %}
</h3>
<br/>
<br/>
<h4><i class="fas fa-language"></i> {% trans 'Language' %}</h4>
<div class="row">
<div class="col-md-12">
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
<input class="form-control" name="next" type="hidden" value="{{ redirect_to }}">
<select name="language" class="form-control">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
<br/>
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
</div>
</div>
<br/>
<br/>
<h4><i class="fas fa-palette"></i>{% trans 'Style' %}</h4>
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load crispy_forms_tags %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Cookbook" %}{% endblock %}
{% block extra_head %}
{{ form.media }}
{% endblock %}
{% block content %}
<h2><i class="fas fa-shopping-cart"></i> {% trans 'Shopping List' %}</h2>
<form action="{% url 'view_shopping' %}" method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-sync-alt"></i> {% trans 'Load' %}</button>
</form>
<br/>
<br/>
<div class="row">
<div class="col col-md-12">
<!--// @formatter:off-->
<textarea id="id_list" class="form-control" rows="{{ ingredients|length|add:1 }}">{% for i in ingredients %}{% if markdown_format %}- [ ]{% endif %} {{ i.amount.normalize }} {{ i.unit }} {{ i.ingredient.name }}&#10;{% endfor %}</textarea>
<!--// @formatter:on-->
</div>
</div>
<br/>
<div class="row">
<div class="col col-md-12 text-center">
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
data-placement="top" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
class="far fa-copy"></i></button>
</div>
</div>
<script type="text/javascript">
function copy() {
let list = $('#id_list');
list.select();
$('#id_btn_copy').attr('data-original-title','{% trans 'Copied!' %}').tooltip('show');
document.execCommand("copy");
}
function resetTooltip() {
setTimeout(function () {
$('#id_btn_copy').attr('data-original-title','{% trans 'Copy list to clipboard' %}');
}, 300);
}
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}

View File

@@ -1,5 +1,7 @@
from django import template
import markdown as md
import bleach
from bleach_whitelist import markdown_tags, markdown_attrs
register = template.Library()
@@ -11,4 +13,7 @@ def get_class(value):
@register.filter()
def markdown(value):
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])
return bleach.clean(md.markdown(value, extensions=['markdown.extensions.fenced_code']), markdown_tags, markdown_attrs)

View File

@@ -0,0 +1,48 @@
from django import template
from django.templatetags.static import static
from cookbook.models import UserPreference
register = template.Library()
@register.simple_tag
def theme_url(request):
try:
themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
UserPreference.FLATLY: 'themes/flatly.min.css',
UserPreference.DARKLY: 'themes/darkly.min.css',
UserPreference.SUPERHERO: 'themes/superhero.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
except AttributeError:
return static('themes/flatly.min.css')
@register.simple_tag
def nav_color(request):
try:
return request.user.userpreference.nav_color
except AttributeError:
return 'primary'
@register.simple_tag
def tabulator_theme_url(request):
try:
themes = {
UserPreference.BOOTSTRAP: 'tabulator/tabulator_bootstrap4.min.css',
UserPreference.FLATLY: 'tabulator/tabulator_bootstrap4.min.css',
UserPreference.DARKLY: 'tabulator/tabulator_site.min.css',
UserPreference.SUPERHERO: 'tabulator/tabulator_site.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
except AttributeError:
return static('tabulator/tabulator_bootstrap4.min.css')

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@@ -0,0 +1,73 @@
from django.contrib import auth
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.urls import reverse
from cookbook.models import Recipe, RecipeIngredient
class TestViews(TestCase):
def setUp(self):
self.client = Client()
self.anonymous_client = Client()
self.client.force_login(User.objects.get_or_create(username='test')[0])
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated)
def test_index(self):
r = self.client.get(reverse('index'))
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(reverse('index'))
self.assertEqual(r.status_code, 200)
def test_books(self):
url = reverse('view_books')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_plan(self):
url = reverse('view_plan')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_shopping(self):
url = reverse('view_shopping')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
def test_internal_recipe_update(self):
recipe = Recipe.objects.create(
name='Test',
created_by=auth.get_user(self.client)
)
url = reverse('edit_internal_recipe', args=[recipe.pk])
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
r = self.client.post(url, {'name': 'Changed', 'working_time': 15, 'waiting_time': 15, 'ingredients': '[]'})
self.assertEqual(r.status_code, 200)
recipe = Recipe.objects.get(pk=recipe.pk)
self.assertEqual('Changed', recipe.name)
r = self.client.post(url,
{'name': 'Changed', 'working_time': 15, 'waiting_time': 15,
'ingredients': '[{"ingredient__name":"Tomato","unit__name":"g","amount":100,"delete":false},{"ingredient__name":"Egg","unit__name":"Piece","amount":2,"delete":false}]'})
self.assertEqual(r.status_code, 200)
self.assertEqual(2, RecipeIngredient.objects.filter(recipe=recipe).count())

View File

@@ -6,7 +6,10 @@ from cookbook.helper import dal
urlpatterns = [
path('', views.index, name='index'),
path('books', views.books, name='view_books'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('shopping/', views.shopping_list, name='view_shopping'),
path('settings/', views.settings, name='view_settings'),
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
@@ -15,6 +18,7 @@ urlpatterns = [
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('new/plan/', new.MealPlanCreate.as_view(), name='new_plan'),
path('list/keyword', lists.keyword, name='list_keyword'),
path('list/import_log', lists.sync_log, name='list_import_log'),
@@ -34,6 +38,8 @@ urlpatterns = [
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('edit/plan/<int:pk>/', edit.MealPlanUpdate.as_view(), name='edit_plan'),
path('edit/ingredient/', edit.edit_ingredients, name='edit_ingredient'),
path('redirect/delete/<slug:name>/<int:pk>/', edit.delete_redirect, name='redirect_delete'),
@@ -46,6 +52,7 @@ urlpatterns = [
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('delete/plan/<int:pk>/', edit.MealPlanDelete.as_view(), name='delete_plan'),
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'),
@@ -53,12 +60,12 @@ urlpatterns = [
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
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/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
path('api/sync_all/', api.sync_all, name='api_sync'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'),
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
]

View File

@@ -1,5 +1,5 @@
from django.contrib import messages
from django.http import HttpResponse
from django.http import HttpResponse, FileResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from django.contrib.auth.decorators import login_required
@@ -10,40 +10,40 @@ from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
@login_required
def get_file_link(request, recipe_id):
recipe = Recipe.objects.get(id=recipe_id)
def get_recipe_provider(recipe):
if recipe.storage.method == Storage.DROPBOX:
return Dropbox
elif recipe.storage.method == Storage.NEXTCLOUD:
return Nextcloud
else:
raise Exception('Provider not implemented')
if recipe.internal:
return HttpResponse(reverse('view_recipe', args=[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()
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)
def update_recipe_links(recipe):
if not recipe.link:
recipe.link = get_recipe_provider(recipe).get_share_link(recipe) # TODO response validation in apis
recipe.save()
@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()
if recipe.storage.method == Storage.NEXTCLOUD:
if recipe.link == "":
recipe.link = Nextcloud.get_share_link(recipe) # TODO response validation
recipe.save()
if not recipe.link:
update_recipe_links(recipe)
return HttpResponse(recipe.link)
@login_required
def get_recipe_file(request, recipe_id):
recipe = Recipe.objects.get(id=recipe_id)
if not recipe.cors_link:
update_recipe_links(recipe)
return HttpResponse(get_recipe_provider(recipe).get_base64_file(recipe))
@login_required
def sync_all(request):
monitors = Sync.objects.filter(active=True)

View File

@@ -7,15 +7,17 @@ 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.db.models import Value, CharField
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext as _, ngettext
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, RecipeBook, \
RecipeBookEntry
from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, InternalRecipeForm, CommentForm, \
MealPlanForm, UnitMergeForm, IngredientMergeForm
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredient, RecipeBook, \
RecipeBookEntry, MealPlan, Unit, Ingredient
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
@@ -41,9 +43,12 @@ def convert_recipe(request, pk):
@login_required
def internal_recipe_update(request, pk):
recipe_instance = get_object_or_404(Recipe, pk=pk)
status = 200
if request.method == "POST":
form = InternalRecipeForm(request.POST, request.FILES)
form.instance = recipe_instance
if form.is_valid():
recipe = recipe_instance
recipe.name = form.cleaned_data['name']
@@ -69,34 +74,50 @@ def internal_recipe_update(request, pk):
recipe.save()
form_ingredients = json.loads(form.data['ingredients'])
RecipeIngredients.objects.filter(recipe=recipe_instance).delete()
form_ingredients = json.loads(form.cleaned_data['ingredients'])
RecipeIngredient.objects.filter(recipe=recipe_instance).delete()
for i in form_ingredients:
ingredient = RecipeIngredients()
ingredient.recipe = recipe_instance
ingredient.name = i['name']
if isinstance(i['amount'], str):
ingredient.amount = float(i['amount'].replace(',', '.'))
recipe_ingredient = RecipeIngredient()
recipe_ingredient.recipe = recipe_instance
if Ingredient.objects.filter(name=i['ingredient__name']).exists():
recipe_ingredient.ingredient = Ingredient.objects.get(name=i['ingredient__name'])
else:
ingredient.amount = i['amount']
ingredient.unit = i['unit']
ingredient.save()
ingredient = Ingredient()
ingredient.name = i['ingredient__name']
ingredient.save()
recipe_ingredient.ingredient = ingredient
if isinstance(i['amount'], str):
recipe_ingredient.amount = float(i['amount'].replace(',', '.'))
else:
recipe_ingredient.amount = i['amount']
if Unit.objects.filter(name=i['unit__name']).exists():
recipe_ingredient.unit = Unit.objects.get(name=i['unit__name'])
else:
unit = Unit()
unit.name = i['unit__name']
unit.save()
recipe_ingredient.unit = unit
recipe_ingredient.save()
recipe.keywords.set(form.cleaned_data['keywords'])
messages.add_message(request, messages.SUCCESS, _('Recipe saved!'))
return HttpResponseRedirect(reverse('edit_internal_recipe', args=[pk]))
else:
messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!'))
messages.add_message(request, messages.ERROR, _('There was an error saving this recipe!'))
status = 403
else:
form = InternalRecipeForm(instance=recipe_instance)
ingredients = RecipeIngredients.objects.filter(recipe=recipe_instance)
ingredients = RecipeIngredient.objects.select_related('unit__name', 'ingredient__name').filter(recipe=recipe_instance).values('ingredient__name', 'unit__name', 'amount')
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)),
'view_url': reverse('view_recipe', args=[pk])}, status=status)
class SyncUpdate(LoginRequiredMixin, UpdateView):
@@ -225,6 +246,22 @@ class RecipeBookUpdate(LoginRequiredMixin, UpdateView):
return context
class MealPlanUpdate(LoginRequiredMixin, UpdateView):
template_name = "generic/edit_template.html"
model = MealPlan
form_class = MealPlanForm
# TODO add msg box
def get_success_url(self):
return reverse('view_plan')
def get_context_data(self, **kwargs):
context = super(MealPlanUpdate, self).get_context_data(**kwargs)
context['title'] = _("Meal-Plan")
return context
class RecipeUpdate(LoginRequiredMixin, UpdateView):
model = Recipe
form_class = ExternalRecipeForm
@@ -262,8 +299,47 @@ class RecipeUpdate(LoginRequiredMixin, UpdateView):
return context
# Generic Delete views
@login_required
def edit_ingredients(request):
if request.method == "POST":
success = False
units_form = UnitMergeForm(request.POST, prefix=UnitMergeForm.prefix)
if units_form.is_valid():
new_unit = units_form.cleaned_data['new_unit']
old_unit = units_form.cleaned_data['old_unit']
recipe_ingredients = RecipeIngredient.objects.filter(unit=old_unit).all()
for i in recipe_ingredients:
i.unit = new_unit
i.save()
old_unit.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
ingredients_form = IngredientMergeForm(request.POST, prefix=IngredientMergeForm.prefix)
if ingredients_form.is_valid():
new_ingredient = ingredients_form.cleaned_data['new_ingredient']
old_ingredient = ingredients_form.cleaned_data['old_ingredient']
recipe_ingredients = RecipeIngredient.objects.filter(ingredient=old_ingredient).all()
for i in recipe_ingredients:
i.ingredient = new_ingredient
i.save()
old_ingredient.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Ingredients merged!'))
if success:
units_form = UnitMergeForm()
ingredients_form = IngredientMergeForm()
else:
units_form = UnitMergeForm()
ingredients_form = IngredientMergeForm()
return render(request, 'forms/ingredients.html', {'units_form': units_form, 'ingredients_form': ingredients_form})
# Generic Delete views
def delete_redirect(request, name, pk):
return redirect(('delete_' + name), pk)
@@ -374,3 +450,14 @@ class RecipeBookEntryDelete(LoginRequiredMixin, DeleteView):
context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs)
context['title'] = _("Bookmarks")
return context
class MealPlanDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = MealPlan
success_url = reverse_lazy('view_plan')
def get_context_data(self, **kwargs):
context = super(MealPlanDelete, self).get_context_data(**kwargs)
context['title'] = _("Meal-Plan")
return context

View File

@@ -1,3 +1,5 @@
import re
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -8,8 +10,8 @@ from django.utils.translation import gettext as _
from django.views.generic import CreateView
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \
RecipeBookForm
from cookbook.models import Keyword, Recipe, RecipeBook
RecipeBookForm, MealPlanForm
from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan
class RecipeCreate(LoginRequiredMixin, CreateView):
@@ -109,3 +111,28 @@ class RecipeBookCreate(LoginRequiredMixin, CreateView):
context = super(RecipeBookCreate, self).get_context_data(**kwargs)
context['title'] = _("Recipe Book")
return context
class MealPlanCreate(LoginRequiredMixin, CreateView):
template_name = "generic/new_template.html"
model = MealPlan
form_class = MealPlanForm
success_url = reverse_lazy('view_plan')
def form_valid(self, form):
obj = form.save(commit=False)
obj.user = self.request.user
obj.save()
return HttpResponseRedirect(reverse('view_plan'))
def get_context_data(self, **kwargs):
context = super(MealPlanCreate, self).get_context_data(**kwargs)
context['title'] = _("Meal-Plan")
recipe = self.request.GET.get('recipe')
if recipe:
if re.match(r'^([0-9])+$', recipe):
if Recipe.objects.filter(pk=int(recipe)).exists():
context['default_recipe'] = Recipe.objects.get(pk=int(recipe))
return context

View File

@@ -1,8 +1,10 @@
import copy
import re
from datetime import datetime, timedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django_tables2 import RequestConfig
from django.utils.translation import gettext as _
@@ -26,7 +28,7 @@ def index(request):
@login_required
def recipe_view(request, pk):
recipe = get_object_or_404(Recipe, pk=pk)
ingredients = RecipeIngredients.objects.filter(recipe=recipe)
ingredients = RecipeIngredient.objects.filter(recipe=recipe)
comments = Comment.objects.filter(recipe=recipe)
if request.method == "POST":
@@ -69,3 +71,108 @@ def books(request):
book_list.append({'book': b, 'recipes': RecipeBookEntry.objects.filter(book=b).all()})
return render(request, 'books.html', {'book_list': book_list})
def get_start_end_from_week(p_year, p_week):
first_day_of_week = datetime.strptime(f'{p_year}-W{int(p_week) - 1}-1', "%Y-W%W-%w").date()
last_day_of_week = first_day_of_week + timedelta(days=6.9)
return first_day_of_week, last_day_of_week
def get_days_from_week(start, end):
delta = end - start
days = []
for i in range(delta.days + 1):
days.append(start + timedelta(days=i))
return days
@login_required()
def meal_plan(request):
js_week = datetime.now().strftime("%Y-W%V")
if request.method == "POST":
js_week = request.POST['week']
year, week = js_week.split('-')
first_day, last_day = get_start_end_from_week(year, week.replace('W', ''))
surrounding_weeks = {'next': (last_day + timedelta(3)).strftime("%Y-W%V"), 'prev': (first_day - timedelta(3)).strftime("%Y-W%V")}
days = get_days_from_week(first_day, last_day)
days_dict = {}
for d in days:
days_dict[d] = []
plan = {}
for t in MealPlan.MEAL_TYPES:
plan[t[0]] = {'type_name': t[1], 'days': copy.deepcopy(days_dict)}
for d in days:
plan_day = MealPlan.objects.filter(date=d).all()
for p in plan_day:
plan[p.meal]['days'][d].append(p)
return render(request, 'meal_plan.html', {'js_week': js_week, 'plan': plan, 'days': days, 'surrounding_weeks': surrounding_weeks})
@login_required
def shopping_list(request):
markdown_format = True
if request.method == "POST":
form = ShoppingForm(request.POST)
if form.is_valid():
recipes = form.cleaned_data['recipe']
markdown_format = form.cleaned_data['markdown_format']
else:
recipes = []
else:
raw_list = request.GET.getlist('r')
recipes = []
for r in raw_list:
if re.match(r'^([1-9])+$', r):
if Recipe.objects.filter(pk=int(r)).exists():
recipes.append(int(r))
form = ShoppingForm(initial={'recipe': recipes})
ingredients = []
for r in recipes:
for ri in RecipeIngredient.objects.filter(recipe=r).all():
index = None
for x, ig in enumerate(ingredients):
if ri.ingredient == ig.ingredient and ri.unit == ig.unit:
index = x
if index:
ingredients[index].amount = ingredients[index].amount + ri.amount
else:
ingredients.append(ri)
return render(request, 'shopping_list.html', {'ingredients': ingredients, 'recipes': recipes, 'form': form, 'markdown_format': markdown_format})
@login_required
def settings(request):
try:
up = request.user.userpreference
except UserPreference.DoesNotExist:
up = None
if request.method == "POST":
form = UserPreferenceForm(request.POST)
if form.is_valid():
if not up:
up = UserPreference(user=request.user)
up.theme = form.cleaned_data['theme']
up.nav_color = form.cleaned_data['nav_color']
up.save()
if up:
form = UserPreferenceForm(instance=up)
else:
form = UserPreferenceForm()
return render(request, 'settings.html', {'form': form})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
preview.xcf Normal file

Binary file not shown.

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-01-13 12:08+0100\n"
"POT-Creation-Date: 2020-02-18 23:20+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:136
#: .\recipes\settings.py:136
msgid "German"
msgstr "Deutsch"
#: recipes/settings.py:137
#: .\recipes\settings.py:137
msgid "English"
msgstr "Englisch"

View File

@@ -72,8 +72,7 @@ ROOT_URLCONF = 'recipes.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [

View File

@@ -22,6 +22,7 @@ urlpatterns = [
path('', include('cookbook.urls')),
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),
path('i18n/', include('django.conf.urls.i18n')),
]
if settings.DEBUG:

View File

@@ -7,12 +7,14 @@ djangorestframework
django-autocomplete-light
django-emoji-picker
django-cleanup
bleach
bleach-whitelist
six
requests
markdown
simplejson
lxml
webdavclient3
python-dotenv==0.10.3
python-dotenv
psycopg2-binary
gunicorn==19.7.1
gunicorn