Compare commits

...

57 Commits
0.4.0 ... 0.6.2

Author SHA1 Message Date
vabene1111
4cf6a3b219 Merge pull request #45 from tourn/improved-meal-plan
Add buttons to add a meal plan to a specific point in time
2020-04-05 14:47:55 +02:00
tourn
de145b6b18 Add some styling 2020-04-05 11:10:39 +02:00
vabene1111
84a8308bf3 updated tabulator js to 4.6 2020-03-31 01:20:49 +02:00
vabene1111
8d191fa1a1 set recipe name as page title 2020-03-31 01:11:14 +02:00
vabene1111
b47a0197e2 imroved recipe printing and view 2020-03-31 01:10:30 +02:00
tourn
4e7c5f9495 Add buttons to add a meal plan to a specific point in time 2020-03-30 22:06:17 +02:00
vabene1111
d704ddacdd fixed shopping list format 2020-03-27 21:47:00 +01:00
vabene1111
2c3140248c fixed broken links 2020-03-27 21:41:55 +01:00
vabene1111
6d5ea31f8e re added mistakingly removed command 2020-03-26 18:55:35 +01:00
vabene1111
b538761746 run container as root for now
since i want to realease this we will for now continue to run this as root inside the containerr. this can be fixed later, PR's welcome
2020-03-26 18:20:44 +01:00
vabene1111
913d858473 updated boot script to fix permission 2020-03-25 22:30:44 +01:00
vabene1111
574d088cdd Create docker-publish.yml 2020-03-24 18:04:52 +01:00
vabene1111
ed360ca1c7 updated readme 2020-03-24 17:37:16 +01:00
vabene1111
08848da4a3 Merge branch 'feature/docker-rewrite' into develop 2020-03-24 17:23:04 +01:00
vabene1111
3f1f63d7e0 remove image tag as it is default 2020-03-24 17:22:51 +01:00
vabene1111
3bd6557e59 use stable image 2020-03-24 17:18:27 +01:00
vabene1111
23cb98f631 Create docker-release-publish.yml 2020-03-24 17:16:24 +01:00
vabene1111
6e91c30245 fixed tests 2020-03-24 17:03:52 +01:00
vabene1111
d7e0fa821b updated examples 2020-03-24 16:44:19 +01:00
vabene1111
c67342df26 wip changes 2020-03-24 12:57:45 +01:00
vabene1111
e3b71d47f4 Merge pull request #39 from h4llow3En/develop
Run as alpine docker image and server static files with gunicorn
2020-03-23 20:03:09 +01:00
h4llow3En
1e3e03e4af Simplify first user creation 2020-03-20 10:50:34 +01:00
h4llow3En
391ab5ddac Don't use "latest" images 2020-03-20 10:02:59 +01:00
h4llow3En
e0c560c2d7 Remove debug output from Dockerfile 2020-03-20 09:28:33 +01:00
h4llow3En
be942bcb79 Fix "Update" description in readme 2020-03-19 18:02:28 +01:00
h4llow3En
6b27f0c8ab Cleanup and simplify deployment 2020-03-19 15:08:53 +01:00
h4llow3En
cc931189e8 Run as alpine docker image and server static files with gunicorn 2020-03-19 10:13:49 +01:00
vabene1111
1b45121385 case insenstitive import 2020-03-18 16:58:27 +01:00
vabene1111
97e2593f72 fixed single import 2020-03-18 16:50:28 +01:00
vabene1111
00539b9d1b fixed theme switching 2020-03-18 13:08:31 +01:00
vabene1111
1cadb1e85e added password change form 2020-03-18 13:06:39 +01:00
vabene1111
9e524a8f22 added user name change 2020-03-18 12:51:13 +01:00
vabene1111
a8a7d4e0f4 complete bottom border "hack" 2020-03-18 12:30:35 +01:00
vabene1111
d0cf396f68 ingredient mobile friendly 2020-03-18 12:29:40 +01:00
vabene1111
e45f3f3343 updated translations 2020-03-18 12:20:45 +01:00
vabene1111
13ea2ecd7d display ingredient note 2020-03-18 12:12:03 +01:00
vabene1111
25ba62e87c improved ingredient editing 2020-03-18 12:11:15 +01:00
vabene1111
48107b918d added ingredient list page 2020-03-18 11:36:39 +01:00
vabene1111
0b56e22af9 nav improvements 2020-03-18 11:36:24 +01:00
vabene1111
47128fbb79 properly align nav icon vertically 2020-03-18 11:19:17 +01:00
vabene1111
12f6aa6df7 changed docker base image for python 3.8 2020-03-17 23:55:16 +01:00
vabene1111
c2dc038ac9 note optional 2020-03-17 22:49:53 +01:00
vabene1111
0c2b3d2d03 added ingredient notes + removed null constraints 2020-03-17 22:47:17 +01:00
vabene1111
1d562452df changed behavior of delete original 2020-03-17 18:54:44 +01:00
vabene1111
4c90664aa2 fixed translation mistaek 2020-03-17 18:47:24 +01:00
vabene1111
90dbc36402 added ability to link recipes to ingredients 2020-03-17 18:44:11 +01:00
vabene1111
a60b09e491 fixed confirm message on unit/ingredeitn merge 2020-03-17 18:28:53 +01:00
vabene1111
deeda425a8 added tests for storage edit 2020-03-17 18:22:13 +01:00
vabene1111
6fcbc9f0cd some basic testing for external recipe edits 2020-03-17 17:48:23 +01:00
vabene1111
7518d8c6b1 fixed several rewrite issues 2020-03-17 17:36:05 +01:00
vabene1111
eb25a9163f fixed import log badge 2020-03-17 17:31:26 +01:00
vabene1111
e2f6e07e42 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2020-03-17 17:23:45 +01:00
vabene1111
17b9519fa9 refactor generic url creation 2020-03-17 17:23:39 +01:00
vabene1111
adcef1d887 Update main.yml 2020-03-17 16:41:21 +01:00
vabene1111
7398304d16 Update main.yml 2020-03-17 16:40:59 +01:00
vabene1111
09ff7e82f1 django admin cleanup 2020-03-17 16:16:04 +01:00
vabene1111
47072763ee cleand up search ui 2020-03-17 15:34:17 +01:00
59 changed files with 1060 additions and 486 deletions

View File

@@ -7,4 +7,12 @@ docker-compose*
.gitignore
README.md
LICENSE
.vscode
.vscode
.env
.env.template
.github
.idea
LICENSE.md
docs
nginx
update.sh

View File

@@ -1,7 +1,3 @@
VIRTUAL_HOST=
LETSENCRYPT_HOST=
LETSENCRYPT_EMAIL=
DEBUG=1
ALLOWED_HOSTS=*
SECRET_KEY=

13
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Publish Docker
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@2.13
with:
name: vabene1111/recipes
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -0,0 +1,18 @@
name: Deploy Docker Image
on:
push:
tags:
- '*'
jobs:
build-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: AhnSeongHyun/action-tag-docker-build-push@v1.0.0
with:
repo_name: vabene1111/recipes
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -9,18 +9,19 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.7]
python-version: [3.8]
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
python3 manage.py collectstatic --noinput
- name: Django Testing project
run: |
python3 manage.py test

2
.gitignore vendored
View File

@@ -76,4 +76,4 @@ staticfiles/
postgresql/
/docker-compose.yml
/docker-compose.override.yml

2
.idea/recipes.iml generated
View File

@@ -16,7 +16,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.7 (recipes)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.8 (recipes)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jquery-3.4.1" level="application" />
<orderEntry type="library" name="pretty-checkbox" level="application" />

View File

@@ -1,24 +1,18 @@
FROM ubuntu:18.04
RUN mkdir /Recipes
WORKDIR /Recipes
ADD . /Recipes/
RUN apt-get update
RUN apt-get -y upgrade
RUN apt-get install -y \
python3 \
python3-pip \
postgresql-client \
gettext
RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt
RUN apt-get autoremove -y
FROM python:3.8-alpine
RUN apk add --no-cache postgresql-libs gettext zlib libjpeg libxml2-dev libxslt-dev
ENV PYTHONUNBUFFERED 1
EXPOSE 8080
EXPOSE 8080
RUN mkdir /opt/recipes
WORKDIR /opt/recipes
COPY . ./
RUN chmod +x boot.sh setup.sh
RUN ln -s /opt/recipes/setup.sh /usr/local/bin/createsuperuser
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev && \
python -m venv venv && \
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@@ -19,11 +19,38 @@ Recipes is a Django application to manage, tag and search recipes using either b
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.
# Installation
The docker image (`vabene1111/recipes`) simply exposes the application on port `8080`. You may choose any preferred installation method, the following are just examples to make it easier.
### Docker-Compose
2. Choose one of the included configurations [here](https://github.com/vabene1111/recipes/tree/develop/docs/docker).
2. Download the environment (config) file template and fill it out `wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env `
3. Start the container `docker-compose up -d`
4. Create a default user by running `docker-compose exec web_recipes createsuperuser`.
### Manual
Copy `.env.template` to `.env` and fill in the missing values accordingly.
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)).
## Updating
While intermediate updates can be skipped when updating please make sure to **read the release notes** in case some special action is required to update.
0. Before updating it is recommended to **create a backup!**
1. Stop the container using `docker-compose down`
2. Pull the latest image using `docker-compose pull`
3. Start the container again using `docker-compose up -d`
# Documentation
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 click on `Storage Data` and then on `Storage Backends`. There click the plus button.
A `Storage Backend` is a remote storage location where PDF files are read from. To add a new backend click on `Storage Data` and then on `Storage Backends`. There click the plus button.
Enter a name (just a display name for you to identify it) and an API access Token for the account you want to use.
Dropboxes API tokens can be found on the [Dropboxes API explorer](https://dropbox.github.io/dropbox-api-v2-explorer/#auth_token/from_oauth1)
@@ -44,29 +71,6 @@ When clicking submit, every recipe containing the word will be updated (tags are
> Currently the only option is word contains, maybe some more SQL like operators will be added later.
## Installation
### Docker-Compose
1. Clone this repository to your desired install location
2. Choose one of the included `docker-compose.yml` files [here](https://github.com/vabene1111/recipes/tree/develop/docs/docker).
3. Copy it to the root directory (where this readme is)
4. Start the container (`docker-compose up -d`)
5. This time and **on each update** run `update.sh` to apply migrations and collect static files
6. 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.
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)).
## Updating
0. Before updating it is recommended to **backup your database**
1. Stop the container using `docker-compose down`.
2. Pull the project files and start the container again using `docker-compose up -d --build`.
3. Run `update.sh`
## Contributing
Pull Requests and ideas are welcome, feel free to contribute in any way.
For any questions on how to work with django please refer to their excellent [documentation](https://www.djangoproject.com/start/).

9
boot.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
source venv/bin/activate
echo "Updating database"
python manage.py migrate
python manage.py collectstatic --noinput
echo "Done"
exec gunicorn -b :8080 --access-logfile - --error-logfile - recipes.wsgi

View File

@@ -2,10 +2,103 @@ from django.contrib import admin
from .models import *
admin.site.register(Recipe)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color')
@staticmethod
def name(obj):
return obj.user.get_user_name()
admin.site.register(UserPreference, UserPreferenceAdmin)
class StorageAdmin(admin.ModelAdmin):
list_display = ('name', 'method')
admin.site.register(Storage, StorageAdmin)
class SyncAdmin(admin.ModelAdmin):
list_display = ('storage', 'path', 'active', 'last_checked')
admin.site.register(Sync, SyncAdmin)
class SyncLogAdmin(admin.ModelAdmin):
list_display = ('sync', 'status', 'msg', 'created_at')
admin.site.register(SyncLog, SyncLogAdmin)
admin.site.register(Keyword)
admin.site.register(Sync)
admin.site.register(SyncLog)
admin.site.register(RecipeImport)
admin.site.register(Storage)
class RecipeAdmin(admin.ModelAdmin):
list_display = ('name', 'internal', 'created_by', 'storage')
@staticmethod
def created_by(obj):
return obj.created_by.get_user_name()
admin.site.register(Recipe, RecipeAdmin)
admin.site.register(Unit)
admin.site.register(Ingredient)
class RecipeIngredientAdmin(admin.ModelAdmin):
list_display = ('recipe', 'ingredient', 'amount', 'unit')
admin.site.register(RecipeIngredient, RecipeIngredientAdmin)
class CommentAdmin(admin.ModelAdmin):
list_display = ('recipe', 'name', 'created_at')
@staticmethod
def name(obj):
return obj.created_by.get_user_name()
admin.site.register(Comment, CommentAdmin)
class RecipeImportAdmin(admin.ModelAdmin):
list_display = ('name', 'storage', 'file_path')
admin.site.register(RecipeImport, RecipeImportAdmin)
class RecipeBookAdmin(admin.ModelAdmin):
list_display = ('name', 'user_name')
@staticmethod
def user_name(obj):
return obj.user.get_user_name()
admin.site.register(RecipeBook, RecipeBookAdmin)
class RecipeBookEntryAdmin(admin.ModelAdmin):
list_display = ('book', 'recipe')
admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin)
class MealPlanAdmin(admin.ModelAdmin):
list_display = ('user', 'recipe', 'meal', 'date')
@staticmethod
def user(obj):
return obj.user.get_user_name()
admin.site.register(MealPlan, MealPlanAdmin)

View File

@@ -44,3 +44,11 @@ class RecipeFilter(django_filters.FilterSet):
class Meta:
model = Recipe
fields = ['name', 'keywords', 'ingredients', 'internal']
class IngredientFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Ingredient
fields = ['name']

View File

@@ -27,6 +27,8 @@ class DateWidget(forms.DateInput):
class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'
class Meta:
model = UserPreference
fields = ('theme', 'nav_color')
@@ -36,6 +38,18 @@ class UserPreferenceForm(forms.ModelForm):
}
class UserNameForm(forms.ModelForm):
prefix = 'name'
class Meta:
model = User
fields = ('first_name', 'last_name')
help_texts = {
'first_name': _('Both fields are optional. If none are given the username will be displayed instead')
}
class ExternalRecipeForm(forms.ModelForm):
file_path = forms.CharField(disabled=True, required=False)
storage = forms.ModelChoiceField(queryset=Storage.objects.all(), disabled=True, required=False)
@@ -141,6 +155,13 @@ class KeywordForm(forms.ModelForm):
widgets = {'icon': EmojiPickerTextInput}
class IngredientForm(forms.ModelForm):
class Meta:
model = Ingredient
fields = ('name', 'recipe')
widgets = {'recipe': SelectWidget}
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'}),

View File

@@ -3,25 +3,25 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-18 23:20+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"POT-Creation-Date: 2020-03-18 12:13+0100\n"
"PO-Revision-Date: 2020-03-18 12:19+0100\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
#: .\cookbook\filters.py:15
#: .\cookbook\filters.py:15 .\cookbook\templates\base.html:98
#: .\cookbook\templates\forms\edit_internal_recipe.html:28
#: .\cookbook\templates\forms\ingredients.html:33
#: .\cookbook\templates\recipe_view.html:67
#: .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\recipe_view.html:104 .\cookbook\views\lists.py:45
msgid "Ingredients"
msgstr "Zutaten"
@@ -30,13 +30,14 @@ msgid ""
"Color of the top navigation bar. Not all colors work with all themes, just "
"try them out!"
msgstr ""
"Farbe der oberen Navigationsleiste. Nicht alle Farben passen, daher einfach mal ausprobieren!"
"Farbe der oberen Navigationsleiste. Nicht alle Farben passen, daher einfach "
"mal ausprobieren!"
#: .\cookbook\forms.py:49 .\cookbook\forms.py:67 .\cookbook\forms.py:189
#: .\cookbook\forms.py:49 .\cookbook\forms.py:67 .\cookbook\forms.py:196
msgid "Name"
msgstr "Name"
#: .\cookbook\forms.py:50 .\cookbook\forms.py:68 .\cookbook\forms.py:190
#: .\cookbook\forms.py:50 .\cookbook\forms.py:68 .\cookbook\forms.py:197
#: .\cookbook\templates\stats.html:22
msgid "Keywords"
msgstr "Schlagwörter"
@@ -49,7 +50,7 @@ msgstr "Zubereitungszeit in Minuten"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Wartezeit (kochen/backen) in Minuten"
#: .\cookbook\forms.py:53 .\cookbook\forms.py:191
#: .\cookbook\forms.py:53 .\cookbook\forms.py:198
msgid "Path"
msgstr "Pfad"
@@ -66,8 +67,8 @@ msgid ""
"Include <code>- [ ]</code> in list for easier usage in markdown based "
"documents."
msgstr ""
"Füge <code>- [ ]</code> vor den Zutaten ein um sie besser in einem Markdown Dokument "
"zu verwenden."
"Füge <code>- [ ]</code> vor den Zutaten ein um sie besser in einem Markdown "
"Dokument zu verwenden."
#: .\cookbook\forms.py:94
msgid "New Unit"
@@ -105,15 +106,15 @@ msgstr "Zutat die ersetzt werden soll."
msgid "Add your comment: "
msgstr "Schreibe einen Kommentar:"
#: .\cookbook\forms.py:148
#: .\cookbook\forms.py:155
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:151
#: .\cookbook\forms.py:158
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Bei Nextcloud leer lassen, bei Dropbox API Token eingeben"
msgstr "Bei Nextcloud leer lassen, bei Dropbox API Token eingeben."
#: .\cookbook\forms.py:159
#: .\cookbook\forms.py:166
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -121,33 +122,33 @@ msgstr ""
"Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote.php/"
"webdav/</code> wird automatisch hinzugefügt)"
#: .\cookbook\forms.py:178
#: .\cookbook\forms.py:185
msgid "Search String"
msgstr "Such Wort"
#: .\cookbook\forms.py:192
#: .\cookbook\forms.py:199
msgid "File ID"
msgstr "Datei ID"
#: .\cookbook\models.py:172
#: .\cookbook\models.py:190
msgid "Breakfast"
msgstr "Frühstück"
#: .\cookbook\models.py:172
#: .\cookbook\models.py:190
msgid "Lunch"
msgstr "Mittagessen"
#: .\cookbook\models.py:172
#: .\cookbook\models.py:190
msgid "Dinner"
msgstr "Abendessen"
#: .\cookbook\models.py:172
#: .\cookbook\models.py:190
msgid "Other"
msgstr "Andere"
#: .\cookbook\tables.py:75
#: .\cookbook\tables.py:83
#: .\cookbook\templates\forms\edit_internal_recipe.html:49
#: .\cookbook\templates\forms\edit_internal_recipe.html:176
#: .\cookbook\templates\forms\edit_internal_recipe.html:160
#: .\cookbook\templates\generic\delete_template.html:5
#: .\cookbook\templates\generic\delete_template.html:13
#: .\cookbook\templates\generic\edit_template.html:25
@@ -160,78 +161,80 @@ msgstr "Löschen"
msgid "Cookbook"
msgstr "Kochbuch"
#: .\cookbook\templates\base.html:82
#: .\cookbook\templates\base.html:85
msgid "Utensils"
msgstr "Utensilien"
#: .\cookbook\templates\base.html:89
msgid "Books"
msgstr "Bücher"
#: .\cookbook\templates\base.html:86 .\cookbook\templates\meal_plan.html:4
#: .\cookbook\templates\meal_plan.html:13 .\cookbook\views\edit.py:261
#: .\cookbook\views\edit.py:462 .\cookbook\views\new.py:130
#: .\cookbook\templates\base.html:92 .\cookbook\templates\meal_plan.html:4
#: .\cookbook\templates\meal_plan.html:13 .\cookbook\views\delete.py:136
#: .\cookbook\views\edit.py:283 .\cookbook\views\new.py:130
msgid "Meal-Plan"
msgstr "Plan"
#: .\cookbook\templates\base.html:90
#, fuzzy
#| msgid "Shopping List"
#: .\cookbook\templates\base.html:95
msgid "Shopping"
msgstr "Einkaufsliste"
#: .\cookbook\templates\base.html:96
#: .\cookbook\templates\base.html:105
msgid "Tags"
msgstr "Schlagwörter"
#: .\cookbook\templates\base.html:100 .\cookbook\views\edit.py:151
#: .\cookbook\views\edit.py:407 .\cookbook\views\lists.py:17
#: .\cookbook\templates\base.html:109 .\cookbook\views\delete.py:70
#: .\cookbook\views\edit.py:159 .\cookbook\views\lists.py:18
#: .\cookbook\views\new.py:46
msgid "Keyword"
msgstr "Schlagwort"
#: .\cookbook\templates\base.html:102
#: .\cookbook\templates\base.html:111
msgid "Batch Edit"
msgstr "Massenbearbeitung"
#: .\cookbook\templates\base.html:107
#: .\cookbook\templates\base.html:116
msgid "Storage Data"
msgstr "Datenquellen"
#: .\cookbook\templates\base.html:111
#: .\cookbook\templates\base.html:120
msgid "Storage Backends"
msgstr "Speicher Quellen"
#: .\cookbook\templates\base.html:113
#: .\cookbook\templates\base.html:122
msgid "Configure Sync"
msgstr "Sync Einstellen"
#: .\cookbook\templates\base.html:115
#: .\cookbook\templates\base.html:124
msgid "Import Recipes"
msgstr "Importierte Rezepte"
#: .\cookbook\templates\base.html:117 .\cookbook\views\lists.py:25
#: .\cookbook\templates\base.html:126 .\cookbook\views\lists.py:26
msgid "Import Log"
msgstr "Import Log"
#: .\cookbook\templates\base.html:119 .\cookbook\templates\stats.html:10
#: .\cookbook\templates\base.html:128 .\cookbook\templates\stats.html:10
msgid "Statistics"
msgstr "Statistiken"
#: .\cookbook\templates\base.html:121
#: .\cookbook\templates\base.html:130
msgid "Units & Ingredients"
msgstr "Einheiten & Zutaten"
#: .\cookbook\templates\base.html:130 .\cookbook\templates\settings.html:6
#: .\cookbook\templates\base.html:145 .\cookbook\templates\settings.html:6
#: .\cookbook\templates\settings.html:11
msgid "Settings"
msgstr "Einstellungen"
#: .\cookbook\templates\base.html:135
#: .\cookbook\templates\base.html:148
msgid "Admin"
msgstr "Admin"
#: .\cookbook\templates\base.html:140
#: .\cookbook\templates\base.html:152
msgid "Logout"
msgstr "Ausloggen"
#: .\cookbook\templates\base.html:143
#: .\cookbook\templates\base.html:157
#: .\cookbook\templates\registration\login.html:44
msgid "Login"
msgstr "Einloggen"
@@ -250,7 +253,7 @@ msgstr ""
"Ausgewählte Schlagwörter zu allen Rezepten die das Suchwort enthalten "
"hinzufügen"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:135
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:143
msgid "Sync"
msgstr "Synchronisieren"
@@ -308,7 +311,7 @@ msgstr "Rezept Importieren"
#: .\cookbook\templates\forms\edit_internal_recipe.html:47
#: .\cookbook\templates\generic\edit_template.html:23
#: .\cookbook\templates\generic\new_template.html:23
#: .\cookbook\templates\recipe_view.html:214
#: .\cookbook\templates\recipe_view.html:340
#: .\cookbook\templates\settings.html:33 .\cookbook\templates\settings.html:47
msgid "Save"
msgstr "Speichern"
@@ -323,6 +326,8 @@ msgid ""
"Use <b>Ctrl</b>+<b>Space</b> to insert new Ingredient!<br/>You can also save "
"the recipe using <b>Ctrl</b>+<b>Shift</b>+<b>S</b>."
msgstr ""
"Benutze <b>Strg</b>+<b>Leertaste</b> um eine neue Zutat einzufügen!<br/"
">Rezepte können mit<b>Strg</b>+<b>Shift</b>+<b>S</b> gespeichert werden."
#: .\cookbook\templates\forms\edit_internal_recipe.html:51
#: .\cookbook\templates\generic\edit_template.html:27
@@ -335,20 +340,25 @@ msgstr "Angucken"
msgid "Delete original file"
msgstr "Original löschen"
#: .\cookbook\templates\forms\edit_internal_recipe.html:159
#: .\cookbook\templates\forms\edit_internal_recipe.html:208
#: .\cookbook\templates\forms\edit_internal_recipe.html:142
#: .\cookbook\templates\forms\edit_internal_recipe.html:189
#: .\cookbook\views\delete.py:81 .\cookbook\views\edit.py:175
msgid "Ingredient"
msgstr "Zutat"
#: .\cookbook\templates\forms\edit_internal_recipe.html:164
#: .\cookbook\templates\forms\edit_internal_recipe.html:147
msgid "Amount"
msgstr "Menge"
#: .\cookbook\templates\forms\edit_internal_recipe.html:166
#: .\cookbook\templates\forms\edit_internal_recipe.html:149
msgid "Unit"
msgstr "Einheit"
#: .\cookbook\templates\forms\edit_internal_recipe.html:179
#: .\cookbook\templates\forms\edit_internal_recipe.html:154
msgid "Note"
msgstr "Notiz "
#: .\cookbook\templates\forms\edit_internal_recipe.html:163
msgid "Are you sure that you want to delete this ingredient?"
msgstr "Bist du sicher das du diese Zutat löschen willst?"
@@ -367,27 +377,27 @@ msgid ""
" "
msgstr ""
"\n"
" Dieses Formular kann genutzt werden wenn versehentlich zwei (oder mehr) Einheiten"
"oder Zutaten erstellt wurden die eigentlich identisch\n"
" Dieses Formular kann genutzt werden wenn versehentlich zwei (oder "
"mehr) Einheitenoder Zutaten erstellt wurden die eigentlich identisch\n"
" sein sollen.\n"
" Es vereint zwei Zutaten oder Einheiten und aktualisiert alle entsprechenden "
"Rezepte.\n"
" Es vereint zwei Zutaten oder Einheiten und aktualisiert alle "
"entsprechenden Rezepte.\n"
" "
#: .\cookbook\templates\forms\ingredients.html:24
msgid "Units"
msgstr "Einheiten"
#: .\cookbook\templates\forms\ingredients.html:29
#: .\cookbook\templates\forms\ingredients.html:26
msgid "Are you sure that you want to merge these two units ?"
msgstr "Bist du sicher diese beiden Einheiten zusammengeführt werden sollen ?"
#: .\cookbook\templates\forms\ingredients.html:30
#: .\cookbook\templates\forms\ingredients.html:39
#: .\cookbook\templates\forms\ingredients.html:31
#: .\cookbook\templates\forms\ingredients.html:40
msgid "Merge"
msgstr "Zusammenführen"
#: .\cookbook\templates\forms\ingredients.html:38
#: .\cookbook\templates\forms\ingredients.html:36
msgid "Are you sure that you want to merge these two ingredients ?"
msgstr "Bist du sicher diese beiden Zutaten zusammengeführt werden sollen ?"
@@ -410,7 +420,11 @@ msgstr "Bearbeiten"
msgid "List"
msgstr "Liste"
#: .\cookbook\templates\generic\list_template.html:19
#: .\cookbook\templates\generic\list_template.html:25
msgid "Filter"
msgstr "Filter"
#: .\cookbook\templates\generic\list_template.html:30
msgid "Import all"
msgstr "Alle importieren"
@@ -428,8 +442,8 @@ msgid "next"
msgstr "nächste"
#: .\cookbook\templates\include\recipe_open_modal.html:28
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:354
#: .\cookbook\views\edit.py:374 .\cookbook\views\new.py:34
#: .\cookbook\views\delete.py:21 .\cookbook\views\edit.py:315
#: .\cookbook\views\new.py:34
msgid "Recipe"
msgstr "Rezept"
@@ -437,7 +451,7 @@ msgstr "Rezept"
msgid "Close"
msgstr "Schließen"
#: .\cookbook\templates\include\recipe_open_modal.html:56
#: .\cookbook\templates\include\recipe_open_modal.html:53
msgid "Open Recipe"
msgstr "Rezept öffnen"
@@ -467,15 +481,23 @@ msgstr ""
"oder Accounts mit limitiertem Zugriff verwendet werden.\n"
" "
#: .\cookbook\templates\index.html:21
#: .\cookbook\templates\index.html:27
msgid "Search recipe ..."
msgstr "Suche Rezept ..."
#: .\cookbook\templates\index.html:40
#: .\cookbook\templates\index.html:41
msgid "New Recipe"
msgstr "Neues Rezept"
#: .\cookbook\templates\index.html:46
msgid "Advanced Search"
msgstr "Erweiterte Suche"
#: .\cookbook\templates\index.html:62
#: .\cookbook\templates\index.html:50
msgid "Reset Search"
msgstr "Suche zurücksetzen"
#: .\cookbook\templates\index.html:78
msgid "Log in to view Recipies"
msgstr "Bitte einloggen um Rezepte zu sehen"
@@ -483,44 +505,54 @@ msgstr "Bitte einloggen um Rezepte zu sehen"
msgid "Week"
msgstr "Woche"
#: .\cookbook\templates\recipe_view.html:31
#: .\cookbook\templates\recipe_view.html:67
msgid "in"
msgstr "in"
#: .\cookbook\templates\recipe_view.html:36
#: .\cookbook\templates\recipe_view.html:181
#: .\cookbook\templates\recipe_view.html:72
#: .\cookbook\templates\recipe_view.html:293
msgid "by"
msgstr "von"
#: .\cookbook\templates\recipe_view.html:47
#: .\cookbook\templates\recipe_view.html:84
msgid "Preparation time ca."
msgstr "Zubereitungszeit ca."
#: .\cookbook\templates\recipe_view.html:52
#: .\cookbook\templates\recipe_view.html:89
msgid "Waiting time ca."
msgstr "Zubereitungszeit ca."
msgstr "Wartezeit ca."
#: .\cookbook\templates\recipe_view.html:114
#: .\cookbook\templates\recipe_view.html:170
msgid "Recipe Image"
msgstr "Rezept Bild"
#: .\cookbook\templates\recipe_view.html:133
#: .\cookbook\templates\recipe_view.html:193
#: .\cookbook\templates\recipe_view.html:227
msgid "View external recipe"
msgstr "Externes Rezept ansehen"
#: .\cookbook\templates\recipe_view.html:144
#: .\cookbook\templates\recipe_view.html:205
msgid "Cloud not show a file preview. Maybe its not a PDF ?"
msgstr ""
"Datei konnte nicht angezeigt werden. Direkte anzeige funktioniert nur mit "
"PDF Dateien."
#: .\cookbook\templates\recipe_view.html:212
msgid "External recipe"
msgstr "Externes Rezept"
#: .\cookbook\templates\recipe_view.html:146
#: .\cookbook\templates\recipe_view.html:214
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"
" "
" This is an external recipe, which means "
"you can only view it by opening the link\n"
" above.\n"
" You can convert this recipe to a fancy "
"recipe by pressing the convert button. The\n"
" original\n"
" file\n"
" will still be accessible.\n"
" "
msgstr ""
"\n"
" Dies ist ein externes Rezept. Das bedeutet das es "
@@ -530,16 +562,16 @@ msgstr ""
"bleibt weiterhin verfügbar.\n"
" "
#: .\cookbook\templates\recipe_view.html:154
#: .\cookbook\templates\recipe_view.html:225
msgid "Convert now!"
msgstr "Jetzt umwandeln!"
#: .\cookbook\templates\recipe_view.html:163
#: .\cookbook\templates\recipe_view.html:289
msgid "Comments"
msgstr "Kommentare"
#: .\cookbook\templates\recipe_view.html:172 .\cookbook\views\edit.py:212
#: .\cookbook\views\edit.py:429
#: .\cookbook\templates\recipe_view.html:309 .\cookbook\views\delete.py:103
#: .\cookbook\views\edit.py:234
msgid "Comment"
msgstr "Kommentar"
@@ -612,65 +644,69 @@ msgstr[0] "Massenbearbeitung erfolgreich. %(count)d Rezept wurde aktualisiert."
msgstr[1] ""
"Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
#: .\cookbook\views\edit.py:109
msgid "Recipe saved!"
msgstr "Rezept gespeichert"
#: .\cookbook\views\edit.py:111
msgid "There was an error saving this recipe!"
msgstr "Es gab einen Fehler beim Speichern des Rezepts"
#: .\cookbook\views\edit.py:160 .\cookbook\views\edit.py:203
msgid "You cannot edit this comment!"
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
#: .\cookbook\views\edit.py:179
msgid "Storage saved!"
msgstr "Speicherquelle gespeichert"
#: .\cookbook\views\edit.py:182
msgid "There was an error updating this storage backend.!"
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle"
#: .\cookbook\views\edit.py:229 .\cookbook\views\edit.py:385
#: .\cookbook\views\lists.py:34
#: .\cookbook\views\delete.py:48 .\cookbook\views\edit.py:251
#: .\cookbook\views\lists.py:35
msgid "Import"
msgstr "Rezept Importieren"
#: .\cookbook\views\edit.py:245 .\cookbook\views\edit.py:440
#: .\cookbook\views\new.py:112
msgid "Recipe Book"
msgstr "Rezeptbuch"
#: .\cookbook\views\edit.py:283
msgid "Changes saved!"
msgstr "Änderungen gespeichert"
#: .\cookbook\views\edit.py:287
msgid "Error saving changes!"
msgstr "Fehler beim Speichern der Daten."
#: .\cookbook\views\edit.py:317
msgid "Units merged!"
msgstr "Einheiten zusammengeführt"
#: .\cookbook\views\edit.py:330
msgid "Ingredients merged!"
msgstr "Zutaten zusammengeführt"
#: .\cookbook\views\edit.py:396
#: .\cookbook\views\delete.py:59
msgid "Monitor"
msgstr "Monitor"
#: .\cookbook\views\edit.py:418 .\cookbook\views\lists.py:42
#: .\cookbook\views\delete.py:92 .\cookbook\views\lists.py:53
#: .\cookbook\views\new.py:64
msgid "Storage Backend"
msgstr "Speicher Quelle"
#: .\cookbook\views\edit.py:451
#: .\cookbook\views\delete.py:114 .\cookbook\views\edit.py:267
#: .\cookbook\views\new.py:112
msgid "Recipe Book"
msgstr "Rezeptbuch"
#: .\cookbook\views\delete.py:125
msgid "Bookmarks"
msgstr "Lesezeichen"
#: .\cookbook\views\edit.py:117
msgid "Recipe saved!"
msgstr "Rezept gespeichert"
#: .\cookbook\views\edit.py:119
msgid "There was an error saving this recipe!"
msgstr "Es gab einen Fehler beim Speichern des Rezepts"
#: .\cookbook\views\edit.py:184
msgid "You cannot edit this storage!"
msgstr "Du kannst diese Speicherquelle nicht bearbeiten!"
#: .\cookbook\views\edit.py:203
msgid "Storage saved!"
msgstr "Speicherquelle gespeichert"
#: .\cookbook\views\edit.py:205
msgid "There was an error updating this storage backend.!"
msgstr "Es gab einen Fehler beim aktualisierung dieser Speicher Quelle"
#: .\cookbook\views\edit.py:225
msgid "You cannot edit this comment!"
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
#: .\cookbook\views\edit.py:303
msgid "Changes saved!"
msgstr "Änderungen gespeichert"
#: .\cookbook\views\edit.py:307
msgid "Error saving changes!"
msgstr "Fehler beim Speichern der Daten."
#: .\cookbook\views\edit.py:337
msgid "Units merged!"
msgstr "Einheiten zusammengeführt"
#: .\cookbook\views\edit.py:350
msgid "Ingredients merged!"
msgstr "Zutaten zusammengeführt"
#: .\cookbook\views\new.py:86
msgid "Imported new recipe!"
msgstr "Importier neue Rezepte"

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.4 on 2020-03-17 17:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0026_auto_20200219_1605'),
]
operations = [
migrations.AddField(
model_name='ingredient',
name='recipe',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.Recipe'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.4 on 2020-03-17 18:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0027_ingredient_recipe'),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='ingredient',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.Ingredient'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.0.4 on 2020-03-17 18:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0028_auto_20200317_1901'),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='unit',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.Unit'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-03-17 18:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0029_auto_20200317_1901'),
]
operations = [
migrations.AddField(
model_name='recipeingredient',
name='note',
field=models.CharField(blank=True, max_length=64, null=True),
),
]

View File

@@ -1,8 +1,25 @@
import re
from django.contrib import auth
from django.contrib.auth.models import User
from django.utils.translation import gettext as _
from django.db import models
def get_user_name(self):
if not (name := f"{self.first_name} {self.last_name}") == " ":
return name
else:
return self.username
auth.models.User.add_to_class('get_user_name', get_user_name)
def get_model_name(model):
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
class UserPreference(models.Model):
# Themes
BOOTSTRAP = 'BOOTSTRAP'
@@ -28,6 +45,9 @@ class UserPreference(models.Model):
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
def __str__(self):
return self.user
class Storage(models.Model):
DROPBOX = 'DB'
@@ -64,6 +84,9 @@ class SyncLog(models.Model):
msg = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.created_at}:{self.sync} - {self.status}"
class Keyword(models.Model):
name = models.CharField(max_length=64, unique=True)
@@ -115,16 +138,18 @@ class Unit(models.Model):
class Ingredient(models.Model):
name = models.CharField(unique=True, max_length=128)
recipe = models.ForeignKey(Recipe, null=True, blank=True, on_delete=models.SET_NULL)
def __str__(self):
return self.name
class RecipeIngredient(models.Model):
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT, null=True)
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT)
amount = models.DecimalField(default=0, decimal_places=2, max_digits=16)
note = models.CharField(max_length=64, null=True, blank=True)
def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.ingredient)

View File

@@ -35,7 +35,7 @@ class Dropbox(Provider):
import_count = 0
for recipe in recipes['entries']: # TODO check if has_more is set and import that as well
path = recipe['path_lower']
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(
if not Recipe.objects.filter(file_path__iexact=path).exists() and not RecipeImport.objects.filter(
file_path=path).exists():
name = os.path.splitext(recipe['name'])[0]
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage, file_uid=recipe['id'])

View File

@@ -32,7 +32,7 @@ class Nextcloud(Provider):
import_count = 0
for file in files:
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path=path).exists() and not RecipeImport.objects.filter(
if not Recipe.objects.filter(file_path__iexact=path).exists() and not RecipeImport.objects.filter(
file_path=path).exists():
name = os.path.splitext(file)[0]
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage)

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,15 @@ class KeywordTable(tables.Table):
fields = ('id', 'icon', 'name')
class IngredientTable(tables.Table):
id = tables.LinkColumn('edit_ingredient', args=[A('id')])
class Meta:
model = Keyword
template_name = 'generic/table_template.html'
fields = ('id', 'name')
class StorageTable(tables.Table):
id = tables.LinkColumn('edit_storage', args=[A('id')])
@@ -44,7 +53,7 @@ class ImportLogTable(tables.Table):
if value == 'SUCCESS':
return format_html('<span class="badge badge-success">%s</span>' % value)
else:
return format_html('<span class="badge badge-error">%s</span>' % value)
return format_html('<span class="badge badge-danger">%s</span>' % value)
class Meta:
model = SyncLog
@@ -71,7 +80,7 @@ class SyncTable(tables.Table):
class RecipeImportTable(tables.Table):
id = tables.LinkColumn('new_recipe_import', args=[A('id')])
delete = tables.TemplateColumn("<a href='{% url 'delete_import' record.id %}' >" + _('Delete') + "</a>")
delete = tables.TemplateColumn("<a href='{% url 'delete_recipe_import' record.id %}' >" + _('Delete') + "</a>")
class Meta:
model = RecipeImport

View File

@@ -74,77 +74,90 @@
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item {% if request.resolver_match.url_name == "index" %}active{% endif %}">
<li class="nav-item {% if request.resolver_match.url_name in 'index,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
<a class="nav-link" href="{% url 'index' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
class="sr-only">(current)</span></a>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_books" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_books' %}"><i class="fas fa-bookmark"></i> {% trans 'Books' %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_books,view_plan,view_shopping,list_ingredient' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-mortar-pestle"></i> {% trans 'Utensils' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'view_books' %}"><i
class="fas fa-bookmark fa-fw"></i> {% trans 'Books' %}
</a>
<a class="dropdown-item" href="{% url 'view_plan' %}"><i
class="fas fa-calendar fa-fw"></i> {% trans 'Meal-Plan' %}
</a>
<a class="dropdown-item" href="{% url 'view_shopping' %}"><i
class="fas fa-shopping-cart fa-fw"></i> {% trans 'Shopping' %}
</a>
<a class="dropdown-item" href="{% url 'list_ingredient' %}"><i
class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %}
</a>
</div>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_plan" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_plan' %}"><i class="fas fa-calendar"></i> {% trans 'Meal-Plan' %}
</a>
</li>
<li class="nav-item {% if request.resolver_match.url_name == "view_shopping" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_shopping' %}"><i class="fas fa-shopping-cart"></i> {% trans 'Shopping' %}
</a>
</li>
<li class="nav-item dropdown">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_keyword,data_batch_edit' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-tags"></i> {% trans 'Tags' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'list_keyword' %}"><i
class="fas fa-tags"></i> {% trans 'Keyword' %}</a>
class="fas fa-tags fa-fw"></i> {% trans 'Keyword' %}</a>
<a class="dropdown-item" href="{% url 'data_batch_edit' %}"><i
class="fas fa-edit"></i> {% trans 'Batch Edit' %}</a>
class="fas fa-edit fa-fw"></i> {% trans 'Batch Edit' %}</a>
</div>
</li>
<li class="nav-item dropdown">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_storage,data_sync,list_recipe_import,list_sync_log,data_stats,edit_ingredient' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i class="fas fa-database"></i> {% trans 'Storage Data' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'list_storage' %}"><i
class="fas fa-database"></i> {% trans 'Storage Backends' %}</a>
class="fas fa-database fa-fw"></i> {% trans 'Storage Backends' %}</a>
<a class="dropdown-item" href="{% url 'data_sync' %}"><i
class="fas fa-sync-alt"></i> {% trans 'Configure Sync' %}</a>
<a class="dropdown-item" href="{% url 'list_import' %}"><i
class="far fa-file-alt"></i> {% trans 'Import Recipes' %}</a>
<a class="dropdown-item" href="{% url 'list_import_log' %}"><i
class="fas fa-history"></i> {% trans 'Import Log' %}</a>
class="fas fa-sync-alt fa-fw"></i> {% trans 'Configure Sync' %}</a>
<a class="dropdown-item" href="{% url 'list_recipe_import' %}"><i
class="far fa-file-alt fa-fw"></i> {% trans 'Import Recipes' %}</a>
<a class="dropdown-item" href="{% url 'list_sync_log' %}"><i
class="fas fa-history fa-fw"></i> {% trans 'Import Log' %}</a>
<a class="dropdown-item" href="{% url 'data_stats' %}"><i
class="fas fa-chart-line"></i> {% trans 'Statistics' %}</a>
class="fas fa-chart-line fa-fw"></i> {% trans 'Statistics' %}</a>
<a class="dropdown-item" href="{% url 'edit_ingredient' %}"><i
class="fas fa-balance-scale"></i> {% trans 'Units & Ingredients' %}</a>
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units & Ingredients' %}</a>
</div>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item {% if request.resolver_match.url_name == "view_settings" %}active{% endif %}">
<a class="nav-link" href="{% url 'view_settings' %}"><i
class="fas fa-user-cog"></i> {% trans 'Settings' %}</a>
</li>
{% if user.is_superuser %}
{% if user.is_authenticated %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i class="fas fa-user-alt"></i> {{ user.get_user_name }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'view_settings' %}"><i
class="fas fa-user-cog fa-fw"></i> {% trans 'Settings' %}</a>
{% if user.is_superuser %}
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
{% endif %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'logout' %}"><i
class="fas fa-sign-out-alt fa-fw"></i> {% trans 'Logout' %}</a>
</div>
</li>
{% else %}
<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 'login' %}">{% trans 'Login' %} <i class="fas fa-sign-in-alt"></i></a>
</li>
{% endif %}
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'logout' %}">{% trans 'Logout' %} {{ user.get_username }} <i
class="fas fa-sign-out-alt"></i></a>
{% else %}
<a class="nav-link" href="{% url 'login' %}">{% trans 'Login' %} <i class="fas fa-sign-in-alt"></i></a>
{% endif %}
</li>
</ul>
</div>
</nav>
<br/>

View File

@@ -10,7 +10,7 @@
<h2>{% trans 'Recipe Books' %}</h2>
</div>
<div class="col col-md-3" style="text-align: right">
<a href="{% url 'new_book' %}" class="btn btn-success"><i
<a href="{% url 'new_recipe_book' %}" class="btn btn-success"><i
class="fas fa-plus-circle"></i> {% trans 'New Book' %}</a>
</div>
</div>

View File

@@ -45,7 +45,7 @@
<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 %}"
<a href="{% delete_url form.instance|get_class form.instance.pk %}"
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>
@@ -151,6 +151,7 @@
validator: "required",
editor: select2UnitEditor
},
{title: "{% trans 'Note' %}", field: "note", editor: "input"},
{
formatter: function (cell, formatterParams) {
return "<span style='color:red'><i class=\"fas fa-trash-alt\"></i></span>"
@@ -181,16 +182,20 @@
});
// load initial value
$('#id_ingredients').val(JSON.stringify(data))
$('#id_ingredients').val(JSON.stringify(data));
function addIngredientRow() {
data.push({
ingredient__name: "{% trans 'Ingredient' %}",
amount: "100",
unit__name: "g",
note: "",
id: Math.floor(Math.random() * 10000000),
delete: false,
});
input = table.rowManager.rows[((table.rowManager.rows).length) - 1].cells[1].getElement()
input.focus();
input.select();
}
document.onkeyup = function (e) {

View File

@@ -22,21 +22,22 @@
<br/>
<h4>{% trans 'Units' %}</h4>
<form action="{% url 'edit_ingredient' %}" method="post">
<form action="{% url 'edit_ingredient' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two units ?' %}')">
{% csrf_token %}
{{ units_form|crispy }}
<button class="btn btn-danger" type="submit"
onclick="confirm('{% trans 'Are you sure that you want to merge these two units ?' %}')"><i
><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
<h4>{% trans 'Ingredients' %}</h4>
<form action="{% url 'edit_ingredient' %}" method="post">
<form action="{% url 'edit_ingredient' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two ingredients ?' %}')">
{% csrf_token %}
{{ ingredients_form|crispy }}
<button class="btn btn-danger" type="submit"
onclick="confirm('{% trans 'Are you sure that you want to merge these two ingredients ?' %}')"><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
<button class="btn btn-danger" type="submit">
<i class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
{% endblock %}

View File

@@ -13,7 +13,7 @@
<h3>{% trans 'Edit' %} {{ title }}</h3>
{% if form.Meta.model|get_class == 'Storage' %}
{% if form.Meta.model|get_class_name == 'Storage' %}
{% include 'include/storage_backend_warning.html' %}
{% endif %}
@@ -21,7 +21,7 @@
{% csrf_token %}
{{ 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 %}"
<a href="{% delete_url form.instance|get_class form.instance.pk %}"
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>

View File

@@ -15,6 +15,17 @@
</a>
{% endif %}
</h3>
{% if filter %}
<br/>
<br/>
<form action="." method="get">
{% csrf_token %}
{{ filter.form|crispy }}
<button type="submit" class="btn btn-success">{% trans 'Filter' %}</button>
</form>
{% endif %}
{% if import_btn %}
<a href="{% url 'data_batch_import' %}" class="btn btn-warning">{% trans 'Import all' %}</a>
<br/>

View File

@@ -13,7 +13,7 @@
<h3>{% trans 'New' %} {{ title }} </h3>
{% if form.Meta.model|get_class == 'Storage' %}
{% if form.Meta.model|get_class_name == 'Storage' %}
{% include 'include/storage_backend_warning.html' %}
{% endif %}

View File

@@ -8,6 +8,12 @@
{% block extra_head %}
{{ filter.form.media }}
<style>
.dropdown-toggle-no-arrow::after {
display: none;
}
</style>
{% endblock %}
{% block content %}
@@ -21,29 +27,36 @@
<input type="text" class="form-control" placeholder="{% trans 'Search recipe ...' %}"
id="{{ filter.form.name.id_for_label }}" name="{{ filter.form.name.name }}"
aria-describedby="button-addon4">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-primary" type="submit"><i class="fas fa-search"></i></button>
<button class="btn btn-warning" type="button" onclick="window.location = window.location.pathname;"><i class="fas fa-backspace"></i></button>
<button class="btn btn-success" type="button" onclick="location.href='{% url 'new_recipe' %}'"><i class="fas fa-plus-circle"></i></button>
<div class="input-group-append">
<button class="btn btn-primary" type="submit"><i class="fas fa-search"></i></button>
<button type="button" class="btn btn-light dropdown-toggle dropdown-toggle-split dropdown-toggle-no-arrow"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v"></i>
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<button class="dropdown-item" type="button"
onclick="location.href='{% url 'new_recipe' %}'"><i
class="fas fa-plus-circle fa-fw"></i> {% trans 'New Recipe' %}</button>
<button data-toggle="collapse" href="#collapse_adv_search"
role="button" class="dropdown-item"
aria-expanded="false" type="button"
aria-controls="collapse_adv_search"><i
class="fas fa-search-plus fa-fw"></i> {% trans 'Advanced Search' %}
</button>
<button class="dropdown-item" type="button"
onclick="window.location = window.location.pathname;"><i
class="fas fa-sync fa-fw"></i> {% trans 'Reset Search' %}</button>
</div>
</div>
</div>
</div>
</div>
<div class="row ">
<div class="col-md-2 offset-md-10" style="text-align: right">
<a class="" data-toggle="collapse" href="#collapse_adv_search" role="button"
aria-expanded="false"
aria-controls="collapse_adv_search"><i
class="fas fa-search"></i> {% trans 'Advanced Search' %}</a>
</div>
</div>
<div class="row">
<div class="collapse col-md-12" id="collapse_adv_search">
<div>
<div style="margin-top: 1vh">
{{ filter.form.keywords | as_crispy_field }}
</div>
<div>

View File

@@ -8,9 +8,28 @@
{% endblock %}
{% block content %}
<style>
.mealplan-cell .mealplan-add-button{
text-align: center;
display: block;
}
@media (hover: hover) {
.mealplan-cell .mealplan-add-button{
visibility: hidden;
float: right;
display: inline;
}
.mealplan-cell:hover .mealplan-add-button{
visibility: initial;
}
}
</style>
<h3>
{% trans 'Meal-Plan' %} <a href="{% url 'new_plan' %}"><i class="fas fa-plus-circle"></i></a>
{% trans 'Meal-Plan' %} <a href="{% url 'new_meal_plan' %}"><i class="fas fa-plus-circle"></i></a>
</h3>
<div class="row">
@@ -53,9 +72,10 @@
</tr>
<tr>
{% for day_key, days_value in plan_value.days.items %}
<td>
<td class="mealplan-cell">
<a class="mealplan-add-button" href="{% url 'new_meal_plan' %}?date={{ day_key|date:'Y-m-d' }}&meal={{ plan_key }}"><i class="fas fa-plus"></i></a>
{% for mp in days_value %}
<a href="{% url 'edit_plan' mp.pk %}"><i class="fas fa-edit"></i></a>
<a href="{% url 'edit_meal_plan' mp.pk %}"><i class="fas fa-edit"></i></a>
<a href="{% url 'view_recipe' mp.recipe.id %}">{{ mp.recipe.name }}</a><br/>
{% endfor %}
</td>

View File

@@ -4,7 +4,7 @@
{% load l10n %}
{% load custom_tags %}
{% block title %}{% trans 'View' %}{% endblock %}
{% block title %}{{ recipe.name }}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pretty-checkbox@3.0/dist/pretty-checkbox.min.css"
@@ -58,8 +58,10 @@
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}"><i
class="fas fa-shopping-cart"></i></a>
{% endif %}
<a class="btn btn-info" href="{% url 'new_plan' %}?recipe={{ recipe.pk }}"><i
<a class="btn btn-info" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}"><i
class="fas fa-calendar"></i></a>
<a class="btn btn-light" onclick="window.print()"><i
class="fas fa-print"></i></a>
</div>
</div>
@@ -69,7 +71,7 @@
{% endif %}
{% if recipe.internal %}
<small>{% trans 'by' %} {{ recipe.created_by.username }}<br/></small>
<small>{% trans 'by' %} {{ recipe.created_by.get_user_name }}<br/></small>
{% endif %}
{% if recipe.keywords %}
@@ -81,12 +83,14 @@
{% endif %}
{% if recipe.working_time and recipe.working_time != 0 %}
<span class="badge badge-secondary">{% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
<span class="badge badge-secondary"><i
class="fas fa-user-clock"></i> {% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
{% endif %}
{% if recipe.waiting_time and recipe.waiting_time != 0 %}
<span
class="badge badge-secondary">{% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
class="badge badge-secondary"><i
class="far fa-clock"></i> {% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
{% endif %}
{% if recipe.waiting_time and recipe.waiting_time != 0 or recipe.working_time and recipe.working_time != 0 %}
@@ -115,10 +119,10 @@
</div>
</div>
<br/>
<table class="">
<table class="table table-sm">
{% for i in ingredients %}
<tr>
<td style="font-size: large">
<td style="vertical-align: middle!important;">
<div class="pretty p-default p-curve">
<input type="checkbox"/>
<div class="state p-success">
@@ -134,11 +138,40 @@
</div>
</td>
<td style="font-size: large">{{ i.ingredient.name }}</td>
<td style="vertical-align: middle!important;">
{% if i.ingredient.recipe %}
<a href="{% url 'view_recipe' i.ingredient.recipe.pk %}" target="_blank">
{% endif %}
{{ i.ingredient.name }}
{% if i.ingredient.recipe %}
</a>
{% endif %}
</td>
<td style="vertical-align: middle!important;">
{% if i.note %}
<button class="btn btn-light btn-sm d-print-none" type="button"
data-container="body"
data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
data-content="{{ i.note }}">
<i class="fas fa-info"></i>
</button>
<div class="d-none d-print-block">
<i class="far fa-comment-alt"></i> {{ i.note }}
</div>
{% endif %}
</td>
</tr>
{% endfor %}
<!-- Bottom border -->
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<br/>
</div>
</div>
@@ -146,7 +179,7 @@
{% endif %}
{% if recipe.image %}
<div class="col-md-6 order-md-2 col-sm-12 order-sm-1 col-12 order-1 " style="text-align: center">
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2" style="text-align: center">
<img class="img img-fluid rounded" src="{{ recipe.image.url }}" style="max-height: 30vh;"
alt="{% trans 'Recipe Image' %}">
<br/>
@@ -267,7 +300,19 @@
<br/>
<br/>
<h5>{% trans 'Comments' %}</h5>
<h5 {% if not comments %}class="d-print-none" {% endif %}><i class="far fa-comments"></i> {% trans 'Comments' %}
</h5>
{% for c in comments %}
<div class="card">
<div class="card-body">
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i class="fas fa-pencil-alt"></i></a><br/>
{{ c.text }}
</div>
</div>
<br/>
{% endfor %}
<div class="d-print-none">
<form method="POST" class="post-form">
@@ -282,17 +327,6 @@
</form>
</div>
{% for c in comments %}
<div class="card">
<div class="card-body">
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i class="fas fa-pencil-alt"></i></a><br/>
{{ c.text }}
</div>
</div>
<br/>
{% endfor %}
{% if recipe.storage %}
{% include 'include/recipe_open_modal.html' %}
{% endif %}
@@ -327,6 +361,14 @@
<script type="text/javascript">
$(function () {
$('[data-toggle="popover"]').popover()
});
$('.popover-dismiss').popover({
trigger: 'focus'
});
function reloadIngredients() {
factor = Number($('#in_factor').val());
ingredients = {

View File

@@ -14,7 +14,24 @@
<br/>
<br/>
<h4><i class="fas fa-language"></i> {% trans 'Language' %}</h4>
<h4><i class="fas fa-user-edit fa-fw"></i> {% trans 'Account' %}</h4>
<form action="." method="post">
{% csrf_token %}
{{ user_name_form|crispy }}
<button class="btn btn-success" type="submit" name="user_name_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<form action="." method="post">
{% csrf_token %}
{{ password_form|crispy }}
<button class="btn btn-success" type="submit" name="password_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<br/>
<br/>
<h4><i class="fas fa-language fa-fw"></i> {% trans 'Language' %}</h4>
<div class="row">
<div class="col-md-12">
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
@@ -39,12 +56,12 @@
<br/>
<br/>
<h4><i class="fas fa-palette"></i>{% trans 'Style' %}</h4>
<h4><i class="fas fa-palette fa-fw"></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>
{{ preference_form|crispy }}
<button class="btn btn-success" type="submit" name="preference_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>

View File

@@ -2,19 +2,31 @@ from django import template
import markdown as md
import bleach
from bleach_whitelist import markdown_tags, markdown_attrs, all_styles, print_attrs
from django.urls import reverse
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.models import get_model_name
register = template.Library()
@register.filter(name='get_class')
def get_class(value):
@register.filter()
def get_class_name(value):
return value.__class__.__name__
@register.filter()
def get_class(value):
return value.__class__
@register.simple_tag
def delete_url(model, pk):
return reverse(f'delete_{get_model_name(model)}', args=[pk])
@register.filter()
def markdown(value):
tags = markdown_tags + ['pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead']
parsed_md = md.markdown(value, extensions=['markdown.extensions.fenced_code', 'tables', MarkdownFormatExtension()])
return bleach.clean(parsed_md, tags, print_attrs, markdown_attrs)
return bleach.clean(parsed_md, tags, markdown_attrs)

View File

@@ -1,7 +1,7 @@
from django.contrib import auth
from django.urls import reverse
from cookbook.models import Recipe, RecipeIngredient, Ingredient, Unit
from cookbook.models import Recipe, RecipeIngredient, Ingredient, Unit, Storage
from cookbook.tests.views.test_views import TestViews
@@ -97,3 +97,32 @@ class TestEditsRecipe(TestViews):
with open('cookbook/tests/resources/image.png', 'rb') as file:
r = self.client.post(url, {'name': "Changed", 'working_time': 15, 'waiting_time': 15, 'image': file})
self.assertEqual(r.status_code, 200)
def test_external_recipe_update(self):
storage = Storage.objects.create(
name='TestStorage',
method=Storage.DROPBOX,
created_by=auth.get_user(self.client),
token='test',
username='test',
password='test',
)
recipe = Recipe.objects.create(
name='Test',
created_by=auth.get_user(self.client),
storage=storage,
)
url = reverse('edit_external_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': 'Test', 'working_time': 15, 'waiting_time': 15, })
recipe.refresh_from_db()
self.assertEqual(recipe.working_time, 15)
self.assertEqual(recipe.waiting_time, 15)

View File

@@ -0,0 +1,39 @@
from django.contrib import auth
from django.urls import reverse
from cookbook.models import Storage
from cookbook.tests.views.test_views import TestViews
class TestEditsRecipe(TestViews):
def test_edit_storage(self):
storage = Storage.objects.create(
name='TestStorage',
method=Storage.DROPBOX,
created_by=auth.get_user(self.client),
token='test',
username='test',
password='test',
)
url = reverse('edit_storage', args=[storage.pk])
r = self.anonymous_client.get(url)
self.assertEqual(r.status_code, 302)
r = self.another_client.get(url)
self.assertEqual(r.status_code, 302)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.superuser_client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url, {'name': 'NewStorage', 'password': '1234_pw', 'token': '1234_token', 'method': Storage.DROPBOX})
storage.refresh_from_db()
self.assertEqual(storage.password, '1234_pw')
self.assertEqual(storage.token, '1234_token')
r = self.client.post(url, {'name': 'NewStorage', 'password': '1234_pw', 'token': '1234_token', 'method': 'not_a_valid_method'})
self.assertFormError(r, 'form', 'method', ['Select a valid choice. not_a_valid_method is not one of the available choices.'])

View File

@@ -6,8 +6,19 @@ from django.test import TestCase, Client
class TestBase(TestCase):
def setUp(self):
self.client = Client()
self.anonymous_client = Client()
self.client.force_login(User.objects.get_or_create(username='test')[0])
self.client = Client()
self.client.force_login(User.objects.get_or_create(username='client')[0])
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated)
self.another_client = Client()
self.another_client.force_login(User.objects.get_or_create(username='another_client')[0])
user = auth.get_user(self.another_client)
self.assertTrue(user.is_authenticated)
self.superuser_client = Client()
self.superuser_client.force_login(User.objects.get_or_create(username='superuser_client', is_superuser=True)[0])
user = auth.get_user(self.superuser_client)
self.assertTrue(user.is_authenticated)

View File

@@ -1,3 +1,5 @@
from pydoc import locate
from django.urls import path
from .views import *
@@ -13,46 +15,17 @@ urlpatterns = [
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
path('new/recipe/', new.RecipeCreate.as_view(), name='new_recipe'),
path('new/recipe_import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
path('new/keyword/', new.KeywordCreate.as_view(), name='new_keyword'),
path('new/storage/', new.StorageCreate.as_view(), name='new_storage'),
path('new/book/', new.RecipeBookCreate.as_view(), name='new_book'),
path('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'),
path('list/import', lists.recipe_import, name='list_import'),
path('list/storage', lists.storage, name='list_storage'),
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'),
# for internal use only
path('edit/recipe/external/<int:pk>/', edit.RecipeUpdate.as_view(), name='edit_external_recipe'),
# for internal use only
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only
path('edit/recipe/external/<int:pk>/', edit.ExternalRecipeUpdate.as_view(), name='edit_external_recipe'), # for internal use only
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'), # for internal use only
path('edit/keyword/<int:pk>/', edit.KeywordUpdate.as_view(), name='edit_keyword'),
path('edit/sync/<int:pk>/', edit.SyncUpdate.as_view(), name='edit_sync'),
path('edit/import/<int:pk>/', edit.ImportUpdate.as_view(), name='edit_import'),
path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
path('edit/comment/<int:pk>/', edit.CommentUpdate.as_view(), name='edit_comment'),
path('edit/recipe-book/<int:pk>/', edit.RecipeBookUpdate.as_view(), name='edit_recipe_book'),
path('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>/', delete.delete_redirect, name='redirect_delete'),
path('delete/recipe/<int:pk>/', delete.RecipeDelete.as_view(), name='delete_recipe'),
path('delete/recipe-source/<int:pk>/', delete.RecipeSourceDelete.as_view(), name='delete_recipe_source'),
path('delete/keyword/<int:pk>/', delete.KeywordDelete.as_view(), name='delete_keyword'),
path('delete/sync/<int:pk>/', delete.MonitorDelete.as_view(), name='delete_sync'),
path('delete/import/<int:pk>/', delete.ImportDelete.as_view(), name='delete_import'),
path('delete/storage/<int:pk>/', delete.StorageDelete.as_view(), name='delete_storage'),
path('delete/comment/<int:pk>/', delete.CommentDelete.as_view(), name='delete_comment'),
path('delete/recipe-book/<int:pk>/', delete.RecipeBookDelete.as_view(), name='delete_recipe_book'),
path('delete/recipe-book-entry/<int:pk>/', delete.RecipeBookEntryDelete.as_view(), name='delete_recipe_book_entry'),
path('delete/plan/<int:pk>/', delete.MealPlanDelete.as_view(), name='delete_plan'),
path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'),
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'),
@@ -69,3 +42,21 @@ urlpatterns = [
path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'),
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
]
generic_models = (Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Comment, RecipeBookEntry, Keyword, Ingredient)
for m in generic_models:
py_name = get_model_name(m)
url_name = py_name.replace('_', '-')
if c := locate(f'cookbook.views.new.{m.__name__}Create'):
urlpatterns.append(path(f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'))
if c := locate(f'cookbook.views.edit.{m.__name__}Update'):
urlpatterns.append(path(f'edit/{url_name}/<int:pk>/', c.as_view(), name=f'edit_{py_name}'))
if c := getattr(lists, py_name, None):
urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}'))
if c := locate(f'cookbook.views.delete.{m.__name__}Delete'):
urlpatterns.append(path(f'delete/{url_name}/<int:pk>/', c.as_view(), name=f'delete_{py_name}'))

View File

@@ -61,7 +61,7 @@ def sync_all(request):
if not error:
messages.add_message(request, messages.SUCCESS, _('Sync successful!'))
return redirect('list_import')
return redirect('list_recipe_import')
else:
messages.add_message(request, messages.ERROR, _('Error synchronizing with Storage'))
return redirect('list_import')
return redirect('list_recipe_import')

View File

@@ -44,7 +44,7 @@ def batch_import(request):
recipe.save()
new_recipe.delete()
return redirect('list_import')
return redirect('list_recipe_import')
@login_required

View File

@@ -1,20 +1,16 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext as _
from django.views.generic import DeleteView
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeBook, \
RecipeBookEntry, MealPlan
RecipeBookEntry, MealPlan, Ingredient
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
# Generic Delete views
def delete_redirect(request, name, pk):
return redirect(('delete_' + name), pk)
class RecipeDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = Recipe
@@ -26,44 +22,40 @@ class RecipeDelete(LoginRequiredMixin, DeleteView):
return context
class RecipeSourceDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = Recipe
success_url = reverse_lazy('index')
def delete_recipe_source(request, pk):
recipe = get_object_or_404(Recipe, pk=pk)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.storage.method == Storage.DROPBOX:
Dropbox.delete_file(self.object) # TODO central location to handle storage type switches
if self.object.storage.method == Storage.NEXTCLOUD:
Nextcloud.delete_file(self.object)
if recipe.storage.method == Storage.DROPBOX:
Dropbox.delete_file(recipe) # TODO central location to handle storage type switches
if recipe.storage.method == Storage.NEXTCLOUD:
Nextcloud.delete_file(recipe)
return super(RecipeSourceDelete, self).delete(request, *args, **kwargs)
recipe.storage = None
recipe.file_path = ''
recipe.file_uid = ''
recipe.save()
def get_context_data(self, **kwargs):
context = super(RecipeSourceDelete, self).get_context_data(**kwargs)
context['title'] = _("Recipe")
return context
return HttpResponseRedirect(reverse('edit_recipe', args=[recipe.pk]))
class ImportDelete(LoginRequiredMixin, DeleteView):
class RecipeImportDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = RecipeImport
success_url = reverse_lazy('list_import')
success_url = reverse_lazy('list_recipe_import')
def get_context_data(self, **kwargs):
context = super(ImportDelete, self).get_context_data(**kwargs)
context = super(RecipeImportDelete, self).get_context_data(**kwargs)
context['title'] = _("Import")
return context
class MonitorDelete(LoginRequiredMixin, DeleteView):
class SyncDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = Sync
success_url = reverse_lazy('data_sync')
def get_context_data(self, **kwargs):
context = super(MonitorDelete, self).get_context_data(**kwargs)
context = super(SyncDelete, self).get_context_data(**kwargs)
context['title'] = _("Monitor")
return context
@@ -79,6 +71,17 @@ class KeywordDelete(LoginRequiredMixin, DeleteView):
return context
class IngredientDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = Ingredient
success_url = reverse_lazy('list_ingredient')
def get_context_data(self, **kwargs):
context = super(IngredientDelete, self).get_context_data(**kwargs)
context['title'] = _("Ingredient")
return context
class StorageDelete(LoginRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = Storage

View File

@@ -15,7 +15,7 @@ from django.utils.translation import gettext as _
from django.views.generic import UpdateView
from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, InternalRecipeForm, CommentForm, \
MealPlanForm, UnitMergeForm, IngredientMergeForm
MealPlanForm, UnitMergeForm, IngredientMergeForm, IngredientForm
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredient, RecipeBook, \
MealPlan, Unit, Ingredient
from cookbook.provider.dropbox import Dropbox
@@ -86,6 +86,9 @@ def internal_recipe_update(request, pk):
recipe_ingredient = RecipeIngredient()
recipe_ingredient.recipe = recipe_instance
if 'note' in i:
recipe_ingredient.note = i['note']
if Ingredient.objects.filter(name=i['ingredient__name']).exists():
recipe_ingredient.ingredient = Ingredient.objects.get(name=i['ingredient__name'])
else:
@@ -118,7 +121,7 @@ def internal_recipe_update(request, pk):
else:
form = InternalRecipeForm(instance=recipe_instance)
ingredients = RecipeIngredient.objects.select_related('unit__name', 'ingredient__name').filter(recipe=recipe_instance).values('ingredient__name', 'unit__name', 'amount')
ingredients = RecipeIngredient.objects.select_related('unit__name', 'ingredient__name').filter(recipe=recipe_instance).values('ingredient__name', 'unit__name', 'amount', 'note')
return render(request, 'forms/edit_internal_recipe.html',
{'form': form, 'ingredients': json.dumps(list(ingredients)),
@@ -157,16 +160,32 @@ class KeywordUpdate(LoginRequiredMixin, UpdateView):
return context
class IngredientUpdate(LoginRequiredMixin, UpdateView):
template_name = "generic/edit_template.html"
model = Ingredient
form_class = IngredientForm
# TODO add msg box
def get_success_url(self):
return reverse('edit_ingredient', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(IngredientUpdate, self).get_context_data(**kwargs)
context['title'] = _("Ingredient")
return context
@login_required
def edit_storage(request, pk):
instance = get_object_or_404(Storage, pk=pk)
if not (instance.created_by == request.user or request.user.is_superuser):
messages.add_message(request, messages.ERROR, _('You cannot edit this comment!'))
messages.add_message(request, messages.ERROR, _('You cannot edit this storage!'))
return HttpResponseRedirect(reverse('list_storage'))
if request.method == "POST":
form = StorageForm(request.POST)
form = StorageForm(request.POST, instance=instance)
if form.is_valid():
instance.name = form.cleaned_data['name']
instance.method = form.cleaned_data['method']
@@ -182,7 +201,6 @@ def edit_storage(request, pk):
instance.save()
messages.add_message(request, messages.SUCCESS, _('Storage saved!'))
return HttpResponseRedirect(reverse('edit_storage', args=[pk]))
else:
messages.add_message(request, messages.ERROR, _('There was an error updating this storage backend.!'))
else:
@@ -191,8 +209,7 @@ def edit_storage(request, pk):
pseudo_instance.token = '__NO__CHANGE__'
form = StorageForm(instance=pseudo_instance)
return render(request, 'generic/edit_template.html',
{'form': form, 'view_url': reverse('view_recipe', args=[pk])})
return render(request, 'generic/edit_template.html', {'form': form})
class CommentUpdate(LoginRequiredMixin, UpdateView):
@@ -267,7 +284,7 @@ class MealPlanUpdate(LoginRequiredMixin, UpdateView):
return context
class RecipeUpdate(LoginRequiredMixin, UpdateView):
class ExternalRecipeUpdate(LoginRequiredMixin, UpdateView):
model = Recipe
form_class = ExternalRecipeForm
template_name = "generic/edit_template.html"
@@ -277,26 +294,24 @@ class RecipeUpdate(LoginRequiredMixin, UpdateView):
old_recipe = Recipe.objects.get(pk=self.object.pk)
if not old_recipe.name == self.object.name:
if self.object.storage.method == Storage.DROPBOX:
Dropbox.rename_file(old_recipe,
self.object.name) # TODO central location to handle storage type switches
Dropbox.rename_file(old_recipe, self.object.name) # TODO central location to handle storage type switches
if self.object.storage.method == Storage.NEXTCLOUD:
Nextcloud.rename_file(old_recipe, self.object.name)
self.object.file_path = os.path.dirname(self.object.file_path) + '/' + self.object.name + \
os.path.splitext(self.object.file_path)[1]
self.object.file_path = os.path.dirname(self.object.file_path) + '/' + self.object.name + os.path.splitext(self.object.file_path)[1]
messages.add_message(self.request, messages.SUCCESS, _('Changes saved!'))
return super(RecipeUpdate, self).form_valid(form)
return super(ExternalRecipeUpdate, self).form_valid(form)
def form_invalid(self, form):
messages.add_message(self.request, messages.ERROR, _('Error saving changes!'))
return super(RecipeUpdate, self).form_valid(form)
return super(ExternalRecipeUpdate, self).form_valid(form)
def get_success_url(self):
return reverse('edit_recipe', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(RecipeUpdate, self).get_context_data(**kwargs)
context = super(ExternalRecipeUpdate, self).get_context_data(**kwargs)
context['title'] = _("Recipe")
context['view_url'] = reverse('view_recipe', args=[self.object.pk])
if self.object.storage:
@@ -342,4 +357,3 @@ def edit_ingredients(request):
ingredients_form = IngredientMergeForm()
return render(request, 'forms/ingredients.html', {'units_form': units_form, 'ingredients_form': ingredients_form})

View File

@@ -5,8 +5,9 @@ from django.urls import reverse_lazy
from django_tables2 import RequestConfig
from django.utils.translation import gettext as _
from cookbook.models import Keyword, SyncLog, RecipeImport, Storage
from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable
from cookbook.filters import IngredientFilter
from cookbook.models import Keyword, SyncLog, RecipeImport, Storage, Ingredient
from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable, IngredientTable
@login_required
@@ -34,6 +35,16 @@ def recipe_import(request):
return render(request, 'generic/list_template.html', {'title': _("Import"), 'table': table, 'import_btn': True})
@login_required
def ingredient(request):
f = IngredientFilter(request.GET, queryset=Ingredient.objects.all().order_by('pk'))
table = IngredientTable(f.qs)
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'generic/list_template.html', {'title': _("Ingredients"), 'table': table, 'filter': f})
@login_required
def storage(request):
table = StorageTable(Storage.objects.all())

View File

@@ -1,4 +1,5 @@
import re
from datetime import datetime
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@@ -76,6 +77,7 @@ def create_new_external_recipe(request, import_id):
recipe.name = form.cleaned_data['name']
recipe.file_path = form.cleaned_data['file_path']
recipe.file_uid = form.cleaned_data['file_uid']
recipe.created_by = request.user
recipe.save()
@@ -84,7 +86,7 @@ def create_new_external_recipe(request, import_id):
RecipeImport.objects.get(id=import_id).delete()
messages.add_message(request, messages.SUCCESS, _('Imported new recipe!'))
return redirect('list_import')
return redirect('list_recipe_import')
else:
messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!'))
else:
@@ -119,6 +121,12 @@ class MealPlanCreate(LoginRequiredMixin, CreateView):
form_class = MealPlanForm
success_url = reverse_lazy('view_plan')
def get_initial(self):
return dict(
meal=self.request.GET['meal'] if 'meal' in self.request.GET else None,
date=datetime.strptime(self.request.GET['date'], '%Y-%m-%d') if 'date' in self.request.GET else None
)
def form_valid(self, form):
obj = form.save(commit=False)
obj.user = self.request.user

View File

@@ -3,7 +3,9 @@ import re
from datetime import datetime, timedelta
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.shortcuts import render, get_object_or_404
from django_tables2 import RequestConfig
from django.utils.translation import gettext as _
@@ -135,7 +137,8 @@ def shopping_list(request):
if Recipe.objects.filter(pk=int(r)).exists():
recipes.append(int(r))
form = ShoppingForm(initial={'recipe': recipes})
markdown_format = False
form = ShoppingForm(initial={'recipe': recipes, 'markdown_format': False})
ingredients = []
@@ -161,18 +164,35 @@ def settings(request):
except UserPreference.DoesNotExist:
up = None
user_name_form = UserNameForm(instance=request.user)
password_form = PasswordChangeForm(request.user)
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 'preference_form' in request.POST:
form = UserPreferenceForm(request.POST, prefix='preference')
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 'user_name_form' in request.POST:
user_name_form = UserNameForm(request.POST, prefix='name')
if user_name_form.is_valid():
request.user.first_name = user_name_form.cleaned_data['first_name']
request.user.last_name = user_name_form.cleaned_data['last_name']
request.user.save()
if 'password_form' in request.POST:
password_form = PasswordChangeForm(request.user, request.POST)
if password_form.is_valid():
user = password_form.save()
update_session_auth_hash(request, user)
if up:
form = UserPreferenceForm(instance=up)
preference_form = UserPreferenceForm(instance=up)
else:
form = UserPreferenceForm()
preference_form = UserPreferenceForm()
return render(request, 'settings.html', {'form': form})
return render(request, 'settings.html', {'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form})

View File

@@ -2,3 +2,10 @@ This is a docker compose example when using [jwilder's nginx reverse proxy](http
in combination with [jrcs's letsencrypt companion](https://hub.docker.com/r/jrcs/letsencrypt-nginx-proxy-companion/).
Please refer to the appropriate documentation on how to setup the reverse proxy and networks.
Remember to add the appropriate environment variables to `.env` file:
```
VIRTUAL_HOST=
LETSENCRYPT_HOST=
LETSENCRYPT_EMAIL=
```

View File

@@ -2,42 +2,42 @@ version: "3"
services:
db_recipes:
restart: always
image: "postgres:11-alpine"
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
- ./.env
networks:
- default
- default
web_recipes:
build: .
image: vabene1111/recipes
restart: always
env_file:
- ./.env
command: "gunicorn --bind 0.0.0.0:8080 recipes.wsgi"
- ./.env
volumes:
- .:/Recipes
- ./staticfiles:/opt/recipes/staticfiles
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
- db_recipes
networks:
- default
- default
nginx_recipes:
image: "nginx"
restart: always
env_file:
- ./.env
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
networks:
- default
- nginx-proxy
nginx_recipes:
image: nginx:mainline-alpine
restart: always
env_file:
- ./.env
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
networks:
- default
- nginx-proxy
networks:
default:
nginx-proxy:
external:
name: nginx-proxy
name: nginx-proxy

View File

@@ -0,0 +1,5 @@
This is the most basic configuration to run this image with docker compose.
> **NOTE**: There is no proxy included in this configuration and gunicorn is directly exposed as the webserver which is
> not recommended by according to the [gunicorn devs](https://serverfault.com/questions/331256/why-do-i-need-nginx-and-something-like-gunicorn).
> It is higly recommended to configure an additional proxy (nginx, ...) in front of this.

View File

@@ -2,39 +2,28 @@ version: "3"
services:
db_recipes:
restart: always
image: "postgres:11-alpine"
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
- ./.env
networks:
- default
- default
web_recipes:
build: .
image: vabene1111/recipes
restart: always
env_file:
- ./.env
command: "gunicorn --bind 0.0.0.0:8080 recipes.wsgi"
- ./.env
volumes:
- .:/Recipes
depends_on:
- db_recipes
networks:
- default
nginx_recipes:
image: "nginx"
restart: always
env_file:
- ./.env
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
- ./staticfiles:/opt/recipes/staticfiles
- ./mediafiles:/opt/recipes/mediafiles
ports:
- 80:80
networks:
- default
- 80:8080
depends_on:
- web_recipes
- db_recipes
networks:
- default
networks:
default:

View File

@@ -2,39 +2,34 @@ version: "3"
services:
db_recipes:
restart: always
image: "postgres:11-alpine"
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
- ./.env
networks:
- default
web_recipes:
build: .
image: vabene1111/recipes
restart: always
env_file:
- ./.env
command: "gunicorn --bind 0.0.0.0:8080 recipes.wsgi"
- ./.env
volumes:
- .:/Recipes
- ./staticfiles:/opt/recipes/staticfiles
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
nginx_recipes:
image: "nginx"
restart: always
env_file:
- ./.env
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./staticfiles:/static
- ./mediafiles:/media
labels:
- db_recipes
labels: # This lables are only examples!
- "traefik.enable=true"
- "traefik.http.routers.recipes.rule=Host(`recipes.mydomain.com`, `recipes.myotherdomain.com`)"
- "traefik.http.routers.recipes.entrypoints=web_secure"
- "traefik.http.routers.recipes.tls.certresolver=le_resolver"
networks:
- default
- traefik
networks:
default:
external:
name: traefik
default:
traefik: # This is you external traefic network
external: true

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-18 23:20+0100\n"
"POT-Creation-Date: 2020-03-18 12:13+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"

View File

@@ -58,6 +58,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -133,8 +134,8 @@ USE_L10N = True
USE_TZ = True
LANGUAGES = [
('de', _('German')),
('en', _('English')),
('de', _('German')),
('en', _('English')),
]
# Static files (CSS, JavaScript, Images)
@@ -145,3 +146,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles")
# Serve static files with gzip
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

View File

@@ -17,4 +17,5 @@ lxml
webdavclient3
python-dotenv
psycopg2-binary
whitenoise
gunicorn

5
setup.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
source venv/bin/activate
echo "Creating Superuser."
python manage.py createsuperuser
echo "Done"

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker-compose run web_recipes python3 manage.py migrate
docker-compose run web_recipes python3 manage.py collectstatic --noinput