Compare commits

..

50 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
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
vabene1111
2afec837a4 fixed missing locale middleware 2020-01-17 14:17:10 +01:00
vabene1111
127eb3181b updated readme 2020-01-17 09:51:05 +01:00
vabene1111
0590826742 Merge pull request #22 from ntindle/patch-3
Update README.md
2020-01-17 09:45:52 +01:00
Nicholas Tindle
ac2c13743c Update README.md 2020-01-16 17:27:17 -06:00
vabene1111
7d4da4c19b Merge pull request #17 from ntindle/patch-2
Update README.md
2020-01-14 06:49:23 +01:00
Nicholas Tindle
763a4a66a2 Update README.md
Minor changes
2020-01-13 21:08:57 -06:00
vabene1111
51fc05dda2 fixed and added german translations 2020-01-13 12:14:05 +01:00
vabene1111
3d695e3d0f added support for ingredients with 0 amout 2020-01-13 11:51:22 +01:00
vabene1111
22b8a4ac18 added help texts to storage form 2020-01-13 11:44:27 +01:00
vabene1111
2e26f9c84b Merge branch 'develop' of https://github.com/vabene1111/recipes into develop
merge
2020-01-13 11:31:03 +01:00
vabene1111
78105e28c8 improved ingredient table 2020-01-13 11:30:59 +01:00
vabene1111
202b92b156 readme 2020-01-01 22:15:14 +01:00
vabene1111
f11f6b7ed1 increased session lifetime 2020-01-01 21:57:21 +01:00
66 changed files with 2312 additions and 346 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}" />
</component>
</project>

View File

@@ -1,16 +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 ![CI](https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master)
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)
- :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 webinterface
- :person_with_blond_hair: Share recipes with friends and comment on them to suggest or remember changes you made
- :whale: Easy setup with Docker
- :package: **Sync** files with Dropbox and Nextcloud (more can easily be added)
- :mag: Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- :label: Create and search for **tags**, assign them in batch to all files matching certain filters
- :page_facing_up: **Create recipes** locally within a nice, standardized web interface
- :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**
- :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.
@@ -18,13 +23,13 @@ This application is meant for people with a collection of recipes they want to s
Most things should be straight forward but there are some more complicated things.
##### Storage Backends
A `Storage Backend` is a remote storage location where files are stored. To add a new backend klick on `Storage Data` and then on `Storage Backends`. There click the plus button.
A `Storage Backend` is a remote storage location where files are stored. To add a new backend click on `Storage Data` and then on `Storage Backends`. There click the plus button.
Enter a name (just a display name for you to identify it) and an API access Token for the account you want to use.
Dropboxes API tokens can be found on the [Dropboxes API explorer](https://dropbox.github.io/dropbox-api-v2-explorer/#auth_token/from_oauth1)
with the button on the top right. For Nextcloud you can use a App apssword created in the settings.
##### Adding Synced Path's
##### Adding Synced Paths
To add a new path from your Storage backend to the sync list, go to `Storage Data >> Configure Sync` and select the storage backend you want to use.
Then enter the path you want to monitor starting at the storage root (e.g. `/Folder/RecipesFolder`) and save it.
@@ -33,23 +38,23 @@ To sync the recipes app with the storage backends press `Sync now` under `Storag
##### Import Recipes
All files found by the sync can be found under `Manage Data >> Import recipes`. There you can either import all at once without modifying them or import one by one, adding tags while importing.
##### Batch Edit
If you have many untagged recipes you may want to edit them all at once. For this go to
If you have many untagged recipes, you may want to edit them all at once. To do so, go to
`Storage Data >> Batch Edit`. Enter a word which should be contained in the recipe name and select the tags you want to apply.
When clicking submit every recipe containing the word will be updated (tags are added).
When clicking submit, every recipe containing the word will be updated (tags are added).
> Currently the only option is word contains, maybe some more SQL like operators will be added later.
## Installation
### Docker-Compose
When cloning this repository a simple docker-compose file is included. It is made for setups already running an nginx-reverse proxy network with lets encrypt companion but can be changed easily. Copy `.env.template` to `.env` and fill in the missing values accordingly.
Now simply start the containers and run the `update.sh` script which will apply all migrations and collect static files.
When cloning this repository, a simple docker-compose file is included. It is made for setups already running an nginx-reverse proxy network with lets encrypt companion but can be changed easily. Copy `.env.template` to `.env` and fill in the missing values accordingly.
Now simply start the containers and run the `update.sh` script that will apply all migrations and collect static files.
Create a default user by executing into the container with `docker-compose exec web_recipes sh` and run `python3 manage.py createsuperuser`.
### Manual
Copy `.env.template` to `.env` and fill in the missing values accordingly.
You can leave out the docker specific variables (VIRTUAL_HOST, LETSENCRYPT_HOST, LETSENCRYPT_EMAIL).
Make sure all variables are available to whatever servers your application.
Make sure all variables are available to whatever serves your application.
Otherwise simply follow the instructions for any django based deployment
(for example this one http://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html).
@@ -62,7 +67,8 @@ To start developing:
5. Start development server with `manage.py runserver`
## Contributing
Pull Requests and ideas are welcome, feel free to contribute in any way.
## License
This project is licensed under the MIT license. Even though it is not required to publish derivatives i highly encourage pushing changes upstream and letting people profit from any work done on this project.
This project is licensed under the MIT license. Even though it is not required to publish derivatives, I highly encourage pushing changes upstream and letting people profit from any work done on this project.

View File

@@ -36,7 +36,7 @@ class RecipeFilter(django_filters.FilterSet):
class QuickRecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='contains')
name = django_filters.CharFilter(lookup_expr='icontains')
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
method='filter_keywords')

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,30 @@ class InternalRecipeForm(forms.ModelForm):
widgets = {'keywords': MultiSelectWidget}
class RecipeForm(forms.Form):
recipe = forms.ModelMultipleChoiceField(
queryset=Recipe.objects.all(),
widget=MultiSelectWidget
)
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 CommentForm(forms.ModelForm):
prefix = 'comment'
@@ -71,19 +122,20 @@ class KeywordForm(forms.ModelForm):
class StorageForm(forms.ModelForm):
username = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password'}), required=False)
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
required=False)
required=False,
help_text=_('Leave empty for dropbox and enter app password for nextcloud.'))
token = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
required=False)
required=False,
help_text=_('Leave empty for nextcloud and enter api token for dropbox.'))
class Meta:
model = Storage
fields = ('name', 'method', 'username', 'password', 'token', 'url')
class RecipeBookForm(forms.ModelForm):
class Meta:
model = RecipeBook
fields = ('name',)
help_texts = {
'url': _(
'Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'),
}
class RecipeBookEntryForm(forms.ModelForm):
@@ -118,3 +170,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-01 20:59+0100\n"
"POT-Creation-Date: 2020-01-13 12:08+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,174 +18,173 @@ 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:115
#: cookbook/forms.py:24 cookbook/forms.py:40 cookbook/forms.py:122
msgid "Name"
msgstr "Name"
#: .\cookbook\forms.py:25 .\cookbook\forms.py:41 .\cookbook\forms.py:116
#: .\cookbook\templates\stats.html:22
#: cookbook/forms.py:25 cookbook/forms.py:41 cookbook/forms.py:123
#: cookbook/templates/stats.html:22
msgid "Keywords"
msgstr "Schlagwörter"
#: .\cookbook\forms.py:26 .\cookbook\forms.py:43
#: cookbook/forms.py:26 cookbook/forms.py:43
msgid "Preparation time in minutes"
msgstr "Zubereitungszeit in Minuten"
#: .\cookbook\forms.py:27 .\cookbook\forms.py:44
#: cookbook/forms.py:27 cookbook/forms.py:44
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Wartezeit (kochen/backen) in Minuten"
#: .\cookbook\forms.py:28 .\cookbook\forms.py:117
#: cookbook/forms.py:28 cookbook/forms.py:124
msgid "Path"
msgstr "Pfad"
#: .\cookbook\forms.py:29
#: cookbook/forms.py:29
msgid "Storage UID"
msgstr "Speicher ID"
#: .\cookbook\forms.py:42
#: cookbook/forms.py:42
msgid "Instructions"
msgstr "Anleitung"
#: .\cookbook\forms.py:57
#: cookbook/forms.py:57
msgid "Add your comment: "
msgstr "Schreibe einen Kommentar:"
#: .\cookbook\forms.py:104
#: cookbook/forms.py:75
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr "Für Dropbox leer lassen, bei Nextcloud App-Passwort eingeben."
#: cookbook/forms.py:78
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Bei Nextcloud leer lassen, bei Dropbox API Token eingeben"
#: cookbook/forms.py:86
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
msgstr "Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote."
"php/webdav/</code> wird automatisch hinzugefügt)"
#: cookbook/forms.py:111
msgid "Search String"
msgstr "Such Wort"
#: .\cookbook\forms.py:118
#: cookbook/forms.py:125
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:97
#: .\cookbook\templates\generic\delete_template.html:5
#: .\cookbook\templates\generic\delete_template.html:13
#: .\cookbook\templates\generic\edit_template.html:25
#: cookbook/tables.py:75 cookbook/templates/forms/edit_internal_recipe.html:39
#: cookbook/templates/forms/edit_internal_recipe.html:98
#: cookbook/templates/generic/delete_template.html:5
#: cookbook/templates/generic/delete_template.html:13
#: cookbook/templates/generic/edit_template.html:25
msgid "Delete"
msgstr "Löschen"
#: .\cookbook\templates\base.html:64 .\cookbook\templates\base.html:72
#: .\cookbook\templates\index.html:7
#: cookbook/templates/base.html:64 cookbook/templates/base.html:72
#: cookbook/templates/index.html:7
msgid "Cookbook"
msgstr "Kochbuch"
#: .\cookbook\templates\base.html:76
#: cookbook/templates/base.html:76
msgid "Books"
msgstr "Bücher"
#: .\cookbook\templates\base.html:81
#: cookbook/templates/base.html:81
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:85 cookbook/views/edit.py:130
#: cookbook/views/edit.py:331 cookbook/views/lists.py:17
#: cookbook/views/new.py:44
msgid "Keyword"
msgstr "Schlagwort"
#: .\cookbook\templates\base.html:87
#: cookbook/templates/base.html:87
msgid "Batch Edit"
msgstr "Massenbearbeitung"
#: .\cookbook\templates\base.html:92
#, fuzzy
#| msgid "Manage Data"
#: cookbook/templates/base.html:92
msgid "Storage Data"
msgstr "Daten Verwalten"
msgstr "Datenquellen"
#: .\cookbook\templates\base.html:96
#: cookbook/templates/base.html:96
msgid "Storage Backends"
msgstr "Speicher Quellen"
#: .\cookbook\templates\base.html:98
#: cookbook/templates/base.html:98
msgid "Configure Sync"
msgstr "Sync Einstellen"
#: .\cookbook\templates\base.html:100
#, fuzzy
#| msgid "Import Recipe"
#: cookbook/templates/base.html:100
msgid "Import Recipes"
msgstr "Rezept Importieren"
msgstr "Importierte Rezepte"
#: .\cookbook\templates\base.html:102 .\cookbook\views\lists.py:25
#, fuzzy
#| msgid "Import Recipe"
#: cookbook/templates/base.html:102 cookbook/views/lists.py:25
msgid "Import Log"
msgstr "Rezept Importieren"
msgstr "Import Log"
#: .\cookbook\templates\base.html:104 .\cookbook\templates\stats.html:10
#: cookbook/templates/base.html:104 cookbook/templates/stats.html:10
msgid "Statistics"
msgstr "Statistiken"
#: .\cookbook\templates\base.html:112
#: cookbook/templates/base.html:112
msgid "Admin"
msgstr "Admin"
#: .\cookbook\templates\base.html:116
#: cookbook/templates/base.html:116
msgid "Logout"
msgstr "Ausloggen"
#: .\cookbook\templates\base.html:119
#: cookbook/templates/base.html:119
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
#, fuzzy
#| msgid ""
#| "Add the specified category and keywords to all recipes containing a word"
#: cookbook/templates/batch/edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
msgstr ""
"Ausgewählte Kategorie und Schlagwörtern zu allen Rezepten die das Suchwort "
"enthalten hinzufügen"
"Ausgewählte Schlagwörter zu allen Rezepten die das Suchwort enthalten "
"hinzufügen"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:114
#: cookbook/templates/batch/monitor.html:6 cookbook/views/edit.py:114
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
#, fuzzy
#| msgid ""
#| "On this Page you can manage all DropBox folder locations that should be "
#| "monitored and synced"
#: cookbook/templates/batch/monitor.html:14
msgid ""
"On this Page you can manage all storage folder locations that should be "
"monitored and synced"
msgstr ""
"Auf dieser Seite kannst du alle Dropbox Ordner verwalten die überwacht und "
"Auf dieser Seite kannst du alle Ordner verwalten die überwacht und "
"synchronisiert werden sollen"
#: .\cookbook\templates\batch\monitor.html:16
#: 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
#, fuzzy
#| msgid "Import Recipe"
#: cookbook/templates/batch/waiting.html:4
#: cookbook/templates/batch/waiting.html:10
msgid "Importing Recipes"
msgstr "Rezept Importieren"
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."
@@ -193,139 +192,120 @@ 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
#, fuzzy
#| msgid "Recipe"
#: 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
#, fuzzy
#| msgid "Import Recipe"
#: 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:201
#: cookbook/templates/forms/edit_import_recipe.html:14
#: cookbook/templates/forms/edit_internal_recipe.html:37
#: cookbook/templates/generic/edit_template.html:23
#: cookbook/templates/generic/new_template.html:23
#: cookbook/templates/recipe_view.html:207
msgid "Save"
msgstr "Speichern"
#: .\cookbook\templates\forms\edit_internal_recipe.html:7
#: .\cookbook\templates\forms\edit_internal_recipe.html:16
#, fuzzy
#| msgid "Recipe"
#: cookbook/templates/forms/edit_internal_recipe.html:7
#: cookbook/templates/forms/edit_internal_recipe.html:16
msgid "Edit Recipe"
msgstr "Rezept"
msgstr "Rezept bearbeiten"
#: .\cookbook\templates\forms\edit_internal_recipe.html:26
#: .\cookbook\templates\recipe_view.html:62
#: 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:41
#: .\cookbook\templates\generic\edit_template.html:27
#: .\cookbook\templates\recipe_view.html:7
#: cookbook/templates/forms/edit_internal_recipe.html:41
#: 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:45
#: cookbook/templates/generic/edit_template.html:30
msgid "Delete original file"
msgstr "Original löschen"
#: .\cookbook\templates\forms\edit_internal_recipe.html:89
#: .\cookbook\templates\forms\edit_internal_recipe.html:126
#: cookbook/templates/forms/edit_internal_recipe.html:90
#: cookbook/templates/forms/edit_internal_recipe.html:127
msgid "Ingredient"
msgstr "Zutat"
#: .\cookbook\templates\forms\edit_internal_recipe.html:94
#: cookbook/templates/forms/edit_internal_recipe.html:95
msgid "Amount"
msgstr "Menge"
#: .\cookbook\templates\forms\edit_internal_recipe.html:95
#: cookbook/templates/forms/edit_internal_recipe.html:96
msgid "Unit"
msgstr "Einheit"
#: .\cookbook\templates\generic\delete_template.html:18
#: 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 ""
#: .\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
#, fuzzy
#| msgid "Auto import all"
#: 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:258 cookbook/views/edit.py:278
#: cookbook/views/edit.py:298 cookbook/views/new.py:32
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
#, fuzzy
#| msgid ""
#| "\n"
#| " The <b>Password and Token</b> field are stored as <b>plain text</"
#| "b> inside the database.\n"
#| " This is necessary because they are needed to make API requests, "
#| "but it also increases the risk of\n"
#| " someone stealing it. <br/>\n"
#| " To limit the possible damage use read only tokens or accounts if "
#| "available or create separate accounts\n"
#| " with limited access (only to recipes).\n"
#: cookbook/templates/include/storage_backend_warning.html:5
msgid ""
"\n"
" The <b>Password and Token</b> field are stored as <b>plain text</b> "
@@ -333,9 +313,8 @@ msgid ""
" This is necessary because they are needed to make API requests, but "
"it also increases the risk of\n"
" someone stealing it. <br/>\n"
" To limit the possible damage use read only tokens or accounts if "
"available or create separate accounts\n"
" with limited access (only to recipes).\n"
" To limit the possible damage tokens or accounts with limited access "
"can be used.\n"
" "
msgstr ""
"\n"
@@ -345,67 +324,51 @@ msgstr ""
"anfragen zu machen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/"
">\n"
" Um das Risiko zu minimieren sollten, wenn möglich, Tokens benutzt "
"werden die keinen Schreibzugriff haben. Alternativ können vollständig "
"seperate Accounts mit limitiertem Zugriff genutzt werden\n"
"oder Accounts mit limitiertem Zugriff verwendet werden.\n"
" "
#: .\cookbook\templates\index.html:21
#, fuzzy
#| msgid "Search String"
#: cookbook/templates/index.html:21
msgid "Search recipe ..."
msgstr "Such Wort"
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:59
msgid "Log in to view Recipies"
msgstr "Bitte einloggen um Rezepte zu sehen"
#: .\cookbook\templates\recipe_view.html:26
#: cookbook/templates/recipe_view.html:27
msgid "in"
msgstr "in"
#: .\cookbook\templates\recipe_view.html:31
#: .\cookbook\templates\recipe_view.html:168
#: cookbook/templates/recipe_view.html:32
#: cookbook/templates/recipe_view.html:174
msgid "by"
msgstr "von"
#: .\cookbook\templates\recipe_view.html:42
#: cookbook/templates/recipe_view.html:43
msgid "Preparation time ca."
msgstr "Zubereitungszeit ca."
#: .\cookbook\templates\recipe_view.html:47
#, fuzzy
#| msgid "Preparation time ca."
#: cookbook/templates/recipe_view.html:48
msgid "Waiting time ca."
msgstr "Zubereitungszeit ca."
#: .\cookbook\templates\recipe_view.html:104
#, fuzzy
#| msgid "Recipe"
#: cookbook/templates/recipe_view.html:110
msgid "Recipe Image"
msgstr "Rezept"
msgstr "Rezept Bild"
#: .\cookbook\templates\recipe_view.html:120
#: cookbook/templates/recipe_view.html:126
msgid "View external recipe"
msgstr "Externes Rezept ansehen"
#: .\cookbook\templates\recipe_view.html:131
#, fuzzy
#| msgid "Open Recipe"
#: cookbook/templates/recipe_view.html:137
msgid "External recipe"
msgstr "Rezept öffnen"
msgstr "Externes Rezept"
#: .\cookbook\templates\recipe_view.html:133
#, fuzzy
#| msgid ""
#| "\n"
#| " This is an external recipe, which means you can "
#| "only view it by opening the link above.\n"
#| " You can convert this recipe to a fancy recipe by "
#| "pressing the convert button. The original file\n"
#| " will still be accessible.\n"
#: cookbook/templates/recipe_view.html:139
msgid ""
"\n"
" This is an external recipe, which means you can only "
@@ -420,144 +383,125 @@ msgstr ""
"nur durch klicken auf den link geöffnet werden kann.\n"
" Das Rezept kann durch drücken des Umwandeln Knopfes "
"in ein schickes lokales Rezept verwandelt werden. Die originale Datei "
"bleibt weiterhin verfügbar\n"
"bleibt weiterhin verfügbar.\n"
" "
#: .\cookbook\templates\recipe_view.html:141
#: cookbook/templates/recipe_view.html:147
msgid "Convert now!"
msgstr "Jetzt umwandeln!"
#: .\cookbook\templates\recipe_view.html:150
#: cookbook/templates/recipe_view.html:156
msgid "Comments"
msgstr "Kommentare"
#: .\cookbook\templates\recipe_view.html:159 .\cookbook\views\edit.py:191
#: .\cookbook\views\edit.py:353
#: cookbook/templates/recipe_view.html:165 cookbook/views/edit.py:191
#: cookbook/views/edit.py:353
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/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
#, fuzzy
#| msgid "Recipe"
#: cookbook/templates/stats.html:20
msgid "Recipes"
msgstr "Rezepte"
#: .\cookbook\templates\stats.html:24
#, fuzzy
#| msgid "Recipe"
#: 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
#, fuzzy, python-format
#| msgid "Batch edit done. %(count)d recipe where updated."
#| msgid_plural "Batch edit done. %(count)d Recipes where updated."
#: 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."
msgstr[0] "Massenbearbeitung erfolgreich. %(count)d Rezept wurde aktualisiert."
msgstr[1] ""
"Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
#: .\cookbook\views\edit.py:88
#, fuzzy
#| msgid "Recipe"
#: cookbook/views/edit.py:88
msgid "Recipe saved!"
msgstr "Rezept"
msgstr "Rezept gespeichert"
#: .\cookbook\views\edit.py:91 .\cookbook\views\new.py:87
#: 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:139 .\cookbook\views\edit.py:182
#: cookbook/views/edit.py:139 cookbook/views/edit.py:182
msgid "You cannot edit this comment!"
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
#: .\cookbook\views\edit.py:158
#, fuzzy
#| msgid "Changes saved!"
#: cookbook/views/edit.py:158
msgid "Storage saved!"
msgstr "Änderungen gespeichert"
msgstr "Speicherquelle gespeichert"
#: .\cookbook\views\edit.py:161
#: cookbook/views/edit.py:161
msgid "There was an error updating this storage backend.!"
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle"
#: .\cookbook\views\edit.py:208 .\cookbook\views\edit.py:309
#: .\cookbook\views\lists.py:34
#, fuzzy
#| msgid "Import Recipe"
#: cookbook/views/edit.py:208 cookbook/views/edit.py:309
#: 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
#, fuzzy
#| msgid "Recipe"
#: cookbook/views/edit.py:224 cookbook/views/edit.py:364
#: cookbook/views/new.py:110
msgid "Recipe Book"
msgstr "Rezept"
msgstr "Rezeptbuch"
#: .\cookbook\views\edit.py:246
#: cookbook/views/edit.py:246
msgid "Changes saved!"
msgstr "Änderungen gespeichert"
#: .\cookbook\views\edit.py:250
#: cookbook/views/edit.py:250
msgid "Error saving changes!"
msgstr "Fehler beim Speichern der Daten."
#: .\cookbook\views\edit.py:320
#: cookbook/views/edit.py:320
msgid "Monitor"
msgstr "Monitor"
#: .\cookbook\views\edit.py:342 .\cookbook\views\lists.py:42
#: .\cookbook\views\new.py:62
#: cookbook/views/edit.py:342 cookbook/views/lists.py:42
#: cookbook/views/new.py:62
msgid "Storage Backend"
msgstr "Speicher Quelle"
#: .\cookbook\views\edit.py:375
#: cookbook/views/edit.py:375
msgid "Bookmarks"
msgstr "Lesezeichen"
#: .\cookbook\views\new.py:84
#, fuzzy
#| msgid "Imported Recipes"
#: cookbook/views/new.py:84
msgid "Imported new recipe!"
msgstr "Importierte Rezepte"
msgstr "Importier neue Rezepte"
#: .\cookbook\views\views.py:42
#, fuzzy
#| msgid "Changes saved!"
#: cookbook/views/views.py:42
msgid "Comment saved!"
msgstr "Änderungen gespeichert"
msgstr "Kommentar gespeichert"
#: .\cookbook\views\views.py:52
#, fuzzy
#| msgid "Changes saved!"
#: cookbook/views/views.py:52
msgid "Bookmark saved!"
msgstr "Änderungen gespeichert"
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

@@ -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'
@@ -75,12 +101,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 +133,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 +162,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

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

@@ -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">
@@ -73,7 +79,12 @@
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>
<a class="nav-link" href="{% url 'view_books' %}"><i class="fas fa-bookmark"></i> {% trans 'Books' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'view_plan' %}"><i class="fas fa-calendar"></i> {% trans 'Meal-Plan' %}
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
@@ -102,15 +113,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>
<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 +161,8 @@
{% endblock %}
</div>
{% block script %}
{% endblock script %}
</body>
</html>

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,25 +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: "{% 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) {
@@ -112,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,34 @@
{% 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 '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 %}
{{ 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>
{% 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,14 @@
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<script type="text/javascript">
{% if default_recipe %}
$(document).ready(function () {
$('#id_recipe').val({{ default_recipe.pk }}).trigger('change');
});
{% endif %}
</script>
{% endblock %}

View File

@@ -6,7 +6,6 @@
The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.
This is necessary because they are needed to make API requests, but it also increases the risk of
someone stealing it. <br/>
To limit the possible damage use read only tokens or accounts if available or create separate accounts
with limited access (only to recipes).
To limit the possible damage tokens or accounts with limited access can be used.
{% endblocktrans %}</p>
</div>

View File

@@ -0,0 +1,72 @@
{% 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="#" onclick="openRecipe({{ mp.recipe.id }})">{{ mp.recipe.name }}</a><br/>
{% endfor %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
</div>
{% include 'include/recipe_open_modal.html' %}
{% endblock %}

View File

@@ -14,11 +14,16 @@
{% block content %}
<div class="row">
<div class="col col-md-9">
<h3>{{ recipe.name }} <a href="{% url 'edit_recipe' recipe.pk %}" class="d-print-none"><i class="fas fa-pencil-alt"></i></a></h3>
<h3>{{ recipe.name }} <a href="{% url 'edit_recipe' recipe.pk %}" class="d-print-none"><i
class="fas fa-pencil-alt"></i></a></h3>
</div>
<div class="col col-md-3 d-print-none" style="text-align: right">
<button class="btn btn-success" onclick="$('#bookmarkModal').modal({'show':true})"><i
class="fas fa-bookmark"></i></button>
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}"><i
class="fas fa-shopping-cart"></i></a>
<a class="btn btn-info" href="{% url 'new_plan' %}?recipe={{ recipe.pk }}"><i
class="fas fa-calendar"></i></a>
</div>
</div>
@@ -80,14 +85,19 @@
<div class="pretty p-default p-curve">
<input type="checkbox"/>
<div class="state p-success">
<label><span
id="ing_{{ i.pk }}">{{ i.amount.normalize }}</span> {{ i.unit }}
<label>
{% if i.amount != 0 %}
<span id="ing_{{ i.pk }}">{{ i.amount.normalize }}</span>
{{ i.unit }}
{% else %}
<span>&#x2063;</span>
{% endif %}
</label>
</div>
</div>
</td>
<td style="font-size: large">{{ i.name }}</td>
<td style="font-size: large">{{ i.ingredient.name }}</td>
</tr>
{% endfor %}
</table>
@@ -107,14 +117,17 @@
</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' %}

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,44 @@
{% 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/>
<button class="btn btn-success" onclick="copy()"><i class="far fa-copy"></i></button>
<div class="row">
<div class="col col-md-12">
<!--// @formatter:off-->
<textarea id="id_list" style="height: 50vh" class="form-control">{% for i in ingredients %}- [ ] {{ i.amount.normalize }} {{ i.unit }} {{ i.ingredient.name }}&#10;{% endfor %}</textarea>
<!--// @formatter:on-->
</div>
</div>
<script type="text/javascript">
function copy() {
let list = $('#id_list');
list.select();
document.execCommand("copy");
}
</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'),
@@ -56,9 +63,9 @@ urlpatterns = [
path('api/get_file_link/<int:recipe_id>/', api.get_file_link, name='api_get_file_link'),
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
path('api/sync_all/', api.sync_all, name='api_sync'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'),
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
]

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
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,29 @@ class RecipeUpdate(LoginRequiredMixin, UpdateView):
return context
# Generic Delete views
@login_required
def edit_ingredients(request):
if request.method == "POST":
form = UnitMergeForm(request.POST, prefix=UnitMergeForm.prefix)
if form.is_valid():
new_unit = form.cleaned_data['new_unit']
old_unit = form.cleaned_data['old_unit']
ingredients = RecipeIngredient.objects.filter(unit=old_unit).all()
for i in ingredients:
i.unit = new_unit
i.save()
old_unit.delete()
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
else:
messages.add_message(request, messages.WARNING, _('There was an error in your form.'))
else:
form = UnitMergeForm()
return render(request, 'forms/ingredients.html', {'form': form})
# Generic Delete views
def delete_redirect(request, name, pk):
return redirect(('delete_' + name), pk)
@@ -374,3 +432,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,97 @@ 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):
if request.method == "POST":
form = RecipeForm(request.POST)
if form.is_valid():
recipes = form.cleaned_data['recipe']
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 = RecipeForm(initial={'recipe': recipes})
ingredients = []
for r in recipes:
for i in RecipeIngredient.objects.filter(recipe=r).all():
ingredients.append(i)
return render(request, 'shopping_list.html', {'ingredients': ingredients, 'recipes': recipes, 'form': form})
@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-01 20:59+0100\n"
"POT-Creation-Date: 2020-01-13 12:08+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,10 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:136
#: recipes/settings.py:136
msgid "German"
msgstr "Deutsch"
#: .\recipes\settings.py:137
#: recipes/settings.py:137
msgid "English"
msgstr "Englisch"

View File

@@ -27,7 +27,7 @@ LOGIN_REDIRECT_URL = "index"
LOGOUT_REDIRECT_URL = "index"
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = 365 * 60 * 24
SESSION_COOKIE_AGE = 365 * 60 * 24 * 60
CRISPY_TEMPLATE_PACK = 'bootstrap4'
DJANGO_TABLES2_TEMPLATE = 'cookbook/templates/generic/table_template.html'
@@ -63,6 +63,7 @@ MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@@ -71,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': [
@@ -122,7 +122,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'de'
LANGUAGE_CODE = 'en'
TIME_ZONE = 'Europe/Berlin'

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