mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 11:19:39 -05:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d5ea31f8e | ||
|
|
b538761746 | ||
|
|
913d858473 | ||
|
|
574d088cdd | ||
|
|
ed360ca1c7 | ||
|
|
08848da4a3 | ||
|
|
3f1f63d7e0 | ||
|
|
3bd6557e59 | ||
|
|
23cb98f631 | ||
|
|
6e91c30245 | ||
|
|
d7e0fa821b | ||
|
|
c67342df26 | ||
|
|
e3b71d47f4 | ||
|
|
1e3e03e4af | ||
|
|
391ab5ddac | ||
|
|
e0c560c2d7 | ||
|
|
be942bcb79 | ||
|
|
6b27f0c8ab | ||
|
|
cc931189e8 | ||
|
|
1b45121385 | ||
|
|
97e2593f72 | ||
|
|
00539b9d1b | ||
|
|
1cadb1e85e | ||
|
|
9e524a8f22 | ||
|
|
a8a7d4e0f4 | ||
|
|
d0cf396f68 | ||
|
|
e45f3f3343 | ||
|
|
13ea2ecd7d | ||
|
|
25ba62e87c | ||
|
|
48107b918d | ||
|
|
0b56e22af9 | ||
|
|
47128fbb79 | ||
|
|
12f6aa6df7 | ||
|
|
c2dc038ac9 | ||
|
|
0c2b3d2d03 | ||
|
|
1d562452df | ||
|
|
4c90664aa2 | ||
|
|
90dbc36402 | ||
|
|
a60b09e491 | ||
|
|
deeda425a8 | ||
|
|
6fcbc9f0cd | ||
|
|
7518d8c6b1 | ||
|
|
eb25a9163f | ||
|
|
e2f6e07e42 | ||
|
|
17b9519fa9 | ||
|
|
adcef1d887 | ||
|
|
7398304d16 | ||
|
|
09ff7e82f1 | ||
|
|
47072763ee | ||
|
|
86f2c9d89c | ||
|
|
c8eaa2a349 | ||
|
|
9853cecabb | ||
|
|
7b65252d47 | ||
|
|
f62ec51c91 | ||
|
|
9f8b93732f | ||
|
|
bf07fc7437 | ||
|
|
08837032ce | ||
|
|
abd655ce62 | ||
|
|
e755068a31 | ||
|
|
b5e35115fa | ||
|
|
c8cc140a78 | ||
|
|
8c7a171d56 | ||
|
|
df62717806 | ||
|
|
a1e6bd5441 | ||
|
|
034e2c612b | ||
|
|
45c85b9de8 | ||
|
|
a9952b8f57 | ||
|
|
b8f16b50a7 | ||
|
|
752df5a1d2 | ||
|
|
fe6e351349 | ||
|
|
8cc9273268 | ||
|
|
0c1763b347 | ||
|
|
88dc713683 | ||
|
|
fc1cc70870 | ||
|
|
42e09fcae9 | ||
|
|
4843568d10 | ||
|
|
2e7e4b23dd | ||
|
|
8192a8dc8f | ||
|
|
c98dbd065e | ||
|
|
43d03ed17d | ||
|
|
8ba34414a1 | ||
|
|
46dffe2f63 | ||
|
|
04cbe6cb2c | ||
|
|
8fd6dcc81c | ||
|
|
b89e96476a | ||
|
|
67b4ec8215 | ||
|
|
db2e67dd71 | ||
|
|
de355abd19 | ||
|
|
41bfa95cb2 | ||
|
|
ad9944dd01 | ||
|
|
a1160c310c | ||
|
|
0444286d11 | ||
|
|
7d4630e3af | ||
|
|
f77aa7c8f0 | ||
|
|
81677a74bb | ||
|
|
2cc385ceac | ||
|
|
b4cdc92207 | ||
|
|
ffdcbff540 | ||
|
|
cc7422a503 | ||
|
|
c08e30c5a9 | ||
|
|
60477cdb9e | ||
|
|
bc066d29f6 | ||
|
|
c96159e15c | ||
|
|
2d70680214 | ||
|
|
00fdab1678 | ||
|
|
6ccafe3c2f | ||
|
|
4080301dbc | ||
|
|
e7227f84ca | ||
|
|
6753a2c0b5 | ||
|
|
56e841879b | ||
|
|
19f5b44e50 | ||
|
|
305a4949fb | ||
|
|
07502fecc0 | ||
|
|
4da1293898 | ||
|
|
ab2ce26d9d | ||
|
|
a2348f531b | ||
|
|
227d90d49d | ||
|
|
6a61c934cd | ||
|
|
6f4a40acdd | ||
|
|
becdcdc6a4 | ||
|
|
afa69c647d | ||
|
|
7449380434 | ||
|
|
2afec837a4 | ||
|
|
127eb3181b | ||
|
|
0590826742 | ||
|
|
ac2c13743c | ||
|
|
7d4da4c19b | ||
|
|
763a4a66a2 |
@@ -7,4 +7,12 @@ docker-compose*
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
.vscode
|
||||
.env
|
||||
.env.template
|
||||
.github
|
||||
.idea
|
||||
LICENSE.md
|
||||
docs
|
||||
nginx
|
||||
update.sh
|
||||
@@ -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
13
.github/workflows/docker-publish.yml
vendored
Normal 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 }}
|
||||
18
.github/workflows/docker-release-publish.yml
vendored
Normal file
18
.github/workflows/docker-release-publish.yml
vendored
Normal 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 }}
|
||||
27
.github/workflows/main.yml
vendored
Normal file
27
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Continous Integration
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
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
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -63,35 +63,17 @@ venv/
|
||||
|
||||
mediafiles/
|
||||
|
||||
*.sqlite3
|
||||
*.sqlite3*
|
||||
|
||||
\.idea/workspace\.xml
|
||||
|
||||
\.idea/misc\.xml
|
||||
|
||||
\.idea/recipes\.iml
|
||||
|
||||
# Deployment
|
||||
|
||||
\.env
|
||||
staticfiles/
|
||||
postgresql/
|
||||
postgresql/
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
/docker-compose.override.yml
|
||||
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
2
.idea/jsLibraryMappings.xml
generated
2
.idea/jsLibraryMappings.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="file://$PROJECT_DIR$" libraries="{pretty-checkbox}" />
|
||||
<file url="file://$PROJECT_DIR$" libraries="{jquery-3.4.1, pdf, pdf_viewer, pretty-checkbox}" />
|
||||
</component>
|
||||
</project>
|
||||
35
.idea/recipes.iml
generated
Normal file
35
.idea/recipes.iml
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="django" name="Django">
|
||||
<configuration>
|
||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||
<option name="settingsModule" value="recipes/settings.py" />
|
||||
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
||||
<option name="environment" value="<map/>" />
|
||||
<option name="doNotUseTestRunner" value="false" />
|
||||
<option name="trackFilePattern" value="migrations" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<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" />
|
||||
<orderEntry type="library" name="pdf" level="application" />
|
||||
<orderEntry type="library" name="pdf_viewer" level="application" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/templates" />
|
||||
<option value="$MODULE_DIR$/cookbook/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
36
Dockerfile
36
Dockerfile
@@ -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"]
|
||||
74
README.md
74
README.md
@@ -1,30 +1,62 @@
|
||||
# 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 
|
||||
Recipes is a Django application to manage, tag and search recipes using either built in models or external storage providers hosting PDF's, Images or other files.
|
||||
|
||||

|
||||

|
||||
|
||||
### Features
|
||||
|
||||
- :package: **Sync** files with Dropbox and Nextcloud (more can easily be added)
|
||||
- :mag: Powerful **search** with djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- :mag: Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- :label: Create and search for **tags**, assign them in batch to all files matching certain filters
|
||||
- :page_facing_up: **Create recipes** locally within a nice, standardized web interface
|
||||
- :iphone: Optimized for use on **mobile** devices like phones and tablets
|
||||
- :shopping_cart: Generate **shopping** lists from recipes
|
||||
- :calendar: Create a **Plan** on what to eat when
|
||||
- :person_with_blond_hair: **Share** recipes with friends and comment on them to suggest or remember changes you made
|
||||
- :whale: Easy setup with **Docker**
|
||||
- :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.
|
||||
|
||||
# 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 klick 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)
|
||||
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,37 +65,15 @@ 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.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
To start developing:
|
||||
1. Clone the repository using your preferred method
|
||||
2. Install requirements from `requirements.txt` either globally or in a virtual environment
|
||||
3. Run migrations with `manage.py migrate`
|
||||
4. Create a first user with `manage.py createsuperuser`
|
||||
5. Start development server with `manage.py runserver`
|
||||
|
||||
## 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/).
|
||||
|
||||
## 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.
|
||||
|
||||
9
boot.sh
Normal file
9
boot.sh
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -2,14 +2,17 @@ import django_filters
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.db.models import Q
|
||||
from cookbook.forms import MultiSelectWidget
|
||||
from cookbook.models import Recipe, Keyword
|
||||
from cookbook.models import Recipe, Keyword, Ingredient
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class RecipeFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(method='filter_name')
|
||||
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
|
||||
method='filter_keywords')
|
||||
ingredients = django_filters.ModelMultipleChoiceFilter(queryset=Ingredient.objects.all(), widget=MultiSelectWidget,
|
||||
method='filter_ingredients', label=_('Ingredients'))
|
||||
|
||||
@staticmethod
|
||||
def filter_keywords(queryset, name, value):
|
||||
@@ -19,6 +22,14 @@ class RecipeFilter(django_filters.FilterSet):
|
||||
queryset = queryset.filter(keywords=x)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def filter_ingredients(queryset, name, value):
|
||||
if not name == 'ingredients':
|
||||
return queryset
|
||||
for x in value:
|
||||
queryset = queryset.filter(recipeingredient__ingredient=x).distinct()
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def filter_name(queryset, name, value):
|
||||
if not name == 'name':
|
||||
@@ -32,14 +43,12 @@ class RecipeFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ['name', 'keywords']
|
||||
fields = ['name', 'keywords', 'ingredients', 'internal']
|
||||
|
||||
|
||||
class QuickRecipeFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr='contains')
|
||||
keywords = django_filters.ModelMultipleChoiceFilter(queryset=Keyword.objects.all(), widget=MultiSelectWidget,
|
||||
method='filter_keywords')
|
||||
class IngredientFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ['name', 'keywords']
|
||||
model = Ingredient
|
||||
fields = ['name']
|
||||
|
||||
@@ -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,49 @@ 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):
|
||||
prefix = 'preference'
|
||||
|
||||
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 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)
|
||||
@@ -32,6 +71,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 +87,52 @@ class InternalRecipeForm(forms.ModelForm):
|
||||
widgets = {'keywords': MultiSelectWidget}
|
||||
|
||||
|
||||
class ShoppingForm(forms.Form):
|
||||
recipe = forms.ModelMultipleChoiceField(
|
||||
queryset=Recipe.objects.filter(internal=True).all(),
|
||||
widget=MultiSelectWidget
|
||||
)
|
||||
markdown_format = forms.BooleanField(
|
||||
help_text=_('Include <code>- [ ]</code> in list for easier usage in markdown based documents.'),
|
||||
required=False,
|
||||
initial=False
|
||||
)
|
||||
|
||||
|
||||
class UnitMergeForm(forms.Form):
|
||||
prefix = 'unit'
|
||||
|
||||
new_unit = forms.ModelChoiceField(
|
||||
queryset=Unit.objects.all(),
|
||||
widget=SelectWidget,
|
||||
label=_('New Unit'),
|
||||
help_text=_('New unit that other gets replaced by.'),
|
||||
)
|
||||
old_unit = forms.ModelChoiceField(
|
||||
queryset=Unit.objects.all(),
|
||||
widget=SelectWidget,
|
||||
label=_('Old Unit'),
|
||||
help_text=_('Unit that should be replaced.'),
|
||||
)
|
||||
|
||||
|
||||
class IngredientMergeForm(forms.Form):
|
||||
prefix = 'ingredient'
|
||||
|
||||
new_ingredient = forms.ModelChoiceField(
|
||||
queryset=Ingredient.objects.all(),
|
||||
widget=SelectWidget,
|
||||
label=_('New Ingredient'),
|
||||
help_text=_('New ingredient that other gets replaced by.'),
|
||||
)
|
||||
old_ingredient = forms.ModelChoiceField(
|
||||
queryset=Ingredient.objects.all(),
|
||||
widget=SelectWidget,
|
||||
label=_('Old Ingredient'),
|
||||
help_text=_('Ingredient that should be replaced.'),
|
||||
)
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
prefix = 'comment'
|
||||
|
||||
@@ -68,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'}),
|
||||
@@ -87,12 +181,6 @@ class StorageForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class RecipeBookForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = RecipeBook
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class RecipeBookEntryForm(forms.ModelForm):
|
||||
prefix = 'bookmark'
|
||||
|
||||
@@ -125,3 +213,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}
|
||||
|
||||
@@ -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
|
||||
|
||||
24
cookbook/helper/mdx_attributes.py
Normal file
24
cookbook/helper/mdx_attributes.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import markdown
|
||||
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
|
||||
class StyleTreeprocessor(Treeprocessor):
|
||||
|
||||
def run_processor(self, node):
|
||||
for child in node:
|
||||
if child.tag == "table":
|
||||
child.set("class", "table table-bordered")
|
||||
if child.tag == "img":
|
||||
child.set("class", "img-fluid")
|
||||
self.run_processor(child)
|
||||
return node
|
||||
|
||||
def run(self, root):
|
||||
self.run_processor(root)
|
||||
return root
|
||||
|
||||
|
||||
class MarkdownFormatExtension(markdown.Extension):
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
md.treeprocessors.register(StyleTreeprocessor(), 'StyleTreeprocessor', 10)
|
||||
Binary file not shown.
@@ -3,167 +3,265 @@
|
||||
# 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-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"
|
||||
"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/forms.py:24 cookbook/forms.py:40 cookbook/forms.py:122
|
||||
#: .\cookbook\filters.py:15 .\cookbook\templates\base.html:98
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:28
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\recipe_view.html:104 .\cookbook\views\lists.py:45
|
||||
msgid "Ingredients"
|
||||
msgstr "Zutaten"
|
||||
|
||||
#: .\cookbook\forms.py:35
|
||||
msgid ""
|
||||
"Color of the top navigation bar. Not all colors work with all themes, just "
|
||||
"try them out!"
|
||||
msgstr ""
|
||||
"Farbe der oberen Navigationsleiste. Nicht alle Farben passen, daher einfach "
|
||||
"mal ausprobieren!"
|
||||
|
||||
#: .\cookbook\forms.py:49 .\cookbook\forms.py:67 .\cookbook\forms.py:196
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: cookbook/forms.py:25 cookbook/forms.py:41 cookbook/forms.py:123
|
||||
#: cookbook/templates/stats.html:22
|
||||
#: .\cookbook\forms.py:50 .\cookbook\forms.py:68 .\cookbook\forms.py:197
|
||||
#: .\cookbook\templates\stats.html:22
|
||||
msgid "Keywords"
|
||||
msgstr "Schlagwörter"
|
||||
|
||||
#: cookbook/forms.py:26 cookbook/forms.py:43
|
||||
#: .\cookbook\forms.py:51 .\cookbook\forms.py:70
|
||||
msgid "Preparation time in minutes"
|
||||
msgstr "Zubereitungszeit in Minuten"
|
||||
|
||||
#: cookbook/forms.py:27 cookbook/forms.py:44
|
||||
#: .\cookbook\forms.py:52 .\cookbook\forms.py:71
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr "Wartezeit (kochen/backen) in Minuten"
|
||||
|
||||
#: cookbook/forms.py:28 cookbook/forms.py:124
|
||||
#: .\cookbook\forms.py:53 .\cookbook\forms.py:198
|
||||
msgid "Path"
|
||||
msgstr "Pfad"
|
||||
|
||||
#: cookbook/forms.py:29
|
||||
#: .\cookbook\forms.py:54
|
||||
msgid "Storage UID"
|
||||
msgstr "Speicher ID"
|
||||
|
||||
#: cookbook/forms.py:42
|
||||
#: .\cookbook\forms.py:69
|
||||
msgid "Instructions"
|
||||
msgstr "Anleitung"
|
||||
|
||||
#: cookbook/forms.py:57
|
||||
#: .\cookbook\forms.py:82
|
||||
msgid ""
|
||||
"Include <code>- [ ]</code> in list for easier usage in markdown based "
|
||||
"documents."
|
||||
msgstr ""
|
||||
"Füge <code>- [ ]</code> vor den Zutaten ein um sie besser in einem Markdown "
|
||||
"Dokument zu verwenden."
|
||||
|
||||
#: .\cookbook\forms.py:94
|
||||
msgid "New Unit"
|
||||
msgstr "Neue Einheit"
|
||||
|
||||
#: .\cookbook\forms.py:95
|
||||
msgid "New unit that other gets replaced by."
|
||||
msgstr "Neue Einheit die die alte ersetzt."
|
||||
|
||||
#: .\cookbook\forms.py:100
|
||||
msgid "Old Unit"
|
||||
msgstr "Alte Einheit"
|
||||
|
||||
#: .\cookbook\forms.py:101
|
||||
msgid "Unit that should be replaced."
|
||||
msgstr "Einheit die ersetzt werden soll."
|
||||
|
||||
#: .\cookbook\forms.py:111
|
||||
msgid "New Ingredient"
|
||||
msgstr "Neue Zutat"
|
||||
|
||||
#: .\cookbook\forms.py:112
|
||||
msgid "New ingredient that other gets replaced by."
|
||||
msgstr "Neue Zutat die die alte ersetzt."
|
||||
|
||||
#: .\cookbook\forms.py:117
|
||||
msgid "Old Ingredient"
|
||||
msgstr "Alte Zutat"
|
||||
|
||||
#: .\cookbook\forms.py:118
|
||||
msgid "Ingredient that should be replaced."
|
||||
msgstr "Zutat die ersetzt werden soll."
|
||||
|
||||
#: .\cookbook\forms.py:130
|
||||
msgid "Add your comment: "
|
||||
msgstr "Schreibe einen Kommentar:"
|
||||
|
||||
#: cookbook/forms.py:75
|
||||
#: .\cookbook\forms.py: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:78
|
||||
#: .\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:86
|
||||
#: .\cookbook\forms.py:166
|
||||
msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
msgstr "Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote."
|
||||
"php/webdav/</code> wird automatisch hinzugefügt)"
|
||||
msgstr ""
|
||||
"Bei Dropbox leer lassen, bei Nextcloud Server URL angeben (<code>/remote.php/"
|
||||
"webdav/</code> wird automatisch hinzugefügt)"
|
||||
|
||||
#: cookbook/forms.py:111
|
||||
#: .\cookbook\forms.py:185
|
||||
msgid "Search String"
|
||||
msgstr "Such Wort"
|
||||
|
||||
#: cookbook/forms.py:125
|
||||
#: .\cookbook\forms.py:199
|
||||
msgid "File ID"
|
||||
msgstr "Datei ID"
|
||||
|
||||
#: cookbook/tables.py:75 cookbook/templates/forms/edit_internal_recipe.html:39
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:98
|
||||
#: cookbook/templates/generic/delete_template.html:5
|
||||
#: cookbook/templates/generic/delete_template.html:13
|
||||
#: cookbook/templates/generic/edit_template.html:25
|
||||
#: .\cookbook\models.py:190
|
||||
msgid "Breakfast"
|
||||
msgstr "Frühstück"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
msgid "Lunch"
|
||||
msgstr "Mittagessen"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
msgid "Dinner"
|
||||
msgstr "Abendessen"
|
||||
|
||||
#: .\cookbook\models.py:190
|
||||
msgid "Other"
|
||||
msgstr "Andere"
|
||||
|
||||
#: .\cookbook\tables.py:83
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:49
|
||||
#: .\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
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: cookbook/templates/base.html:64 cookbook/templates/base.html:72
|
||||
#: cookbook/templates/index.html:7
|
||||
#: .\cookbook\templates\base.html:70 .\cookbook\templates\base.html:78
|
||||
#: .\cookbook\templates\forms\ingredients.html:7
|
||||
#: .\cookbook\templates\index.html:7 .\cookbook\templates\shopping_list.html:7
|
||||
msgid "Cookbook"
|
||||
msgstr "Kochbuch"
|
||||
|
||||
#: cookbook/templates/base.html:76
|
||||
#: .\cookbook\templates\base.html:85
|
||||
msgid "Utensils"
|
||||
msgstr "Utensilien"
|
||||
|
||||
#: .\cookbook\templates\base.html:89
|
||||
msgid "Books"
|
||||
msgstr "Bücher"
|
||||
|
||||
#: cookbook/templates/base.html:81
|
||||
#: .\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:95
|
||||
msgid "Shopping"
|
||||
msgstr "Einkaufsliste"
|
||||
|
||||
#: .\cookbook\templates\base.html:105
|
||||
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: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:87
|
||||
#: .\cookbook\templates\base.html:111
|
||||
msgid "Batch Edit"
|
||||
msgstr "Massenbearbeitung"
|
||||
|
||||
#: cookbook/templates/base.html:92
|
||||
#: .\cookbook\templates\base.html:116
|
||||
msgid "Storage Data"
|
||||
msgstr "Datenquellen"
|
||||
|
||||
#: cookbook/templates/base.html:96
|
||||
#: .\cookbook\templates\base.html:120
|
||||
msgid "Storage Backends"
|
||||
msgstr "Speicher Quellen"
|
||||
|
||||
#: cookbook/templates/base.html:98
|
||||
#: .\cookbook\templates\base.html:122
|
||||
msgid "Configure Sync"
|
||||
msgstr "Sync Einstellen"
|
||||
|
||||
#: cookbook/templates/base.html:100
|
||||
#: .\cookbook\templates\base.html:124
|
||||
msgid "Import Recipes"
|
||||
msgstr "Importierte Rezepte"
|
||||
|
||||
#: cookbook/templates/base.html:102 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:104 cookbook/templates/stats.html:10
|
||||
#: .\cookbook\templates\base.html:128 .\cookbook\templates\stats.html:10
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiken"
|
||||
|
||||
#: cookbook/templates/base.html:112
|
||||
#: .\cookbook\templates\base.html:130
|
||||
msgid "Units & Ingredients"
|
||||
msgstr "Einheiten & Zutaten"
|
||||
|
||||
#: .\cookbook\templates\base.html:145 .\cookbook\templates\settings.html:6
|
||||
#: .\cookbook\templates\settings.html:11
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: .\cookbook\templates\base.html:148
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: cookbook/templates/base.html:116
|
||||
#: .\cookbook\templates\base.html:152
|
||||
msgid "Logout"
|
||||
msgstr "Ausloggen"
|
||||
|
||||
#: cookbook/templates/base.html:119
|
||||
#: .\cookbook\templates\base.html:157
|
||||
#: .\cookbook\templates\registration\login.html:44
|
||||
msgid "Login"
|
||||
msgstr "Einloggen"
|
||||
|
||||
#: cookbook/templates/batch/edit.html:6
|
||||
#: .\cookbook\templates\batch\edit.html:6
|
||||
msgid "Batch edit Category"
|
||||
msgstr "Kategorie massenbearbeitung"
|
||||
|
||||
#: cookbook/templates/batch/edit.html:15
|
||||
#: .\cookbook\templates\batch\edit.html:15
|
||||
msgid "Batch edit Recipes"
|
||||
msgstr "Rezept massenbearbeitung"
|
||||
|
||||
#: cookbook/templates/batch/edit.html:20
|
||||
#: .\cookbook\templates\batch\edit.html:20
|
||||
msgid "Add the specified keywords to all recipes containing a word"
|
||||
msgstr ""
|
||||
"Ausgewählte Schlagwörter zu allen Rezepten die das Suchwort enthalten "
|
||||
"hinzufügen"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:6 cookbook/views/edit.py:114
|
||||
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:143
|
||||
msgid "Sync"
|
||||
msgstr "Synchronisieren"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:10
|
||||
#: .\cookbook\templates\batch\monitor.html:10
|
||||
msgid "Manage watched Folders"
|
||||
msgstr "Überwachte Ordner verwalten"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:14
|
||||
#: .\cookbook\templates\batch\monitor.html:14
|
||||
msgid ""
|
||||
"On this Page you can manage all storage folder locations that should be "
|
||||
"monitored and synced"
|
||||
@@ -171,20 +269,20 @@ msgstr ""
|
||||
"Auf dieser Seite kannst du alle Ordner verwalten die überwacht und "
|
||||
"synchronisiert werden sollen"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:16
|
||||
#: .\cookbook\templates\batch\monitor.html:16
|
||||
msgid "The path must be in the following format"
|
||||
msgstr "Der Pfad muss in folgendem Format sein"
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:27
|
||||
#: .\cookbook\templates\batch\monitor.html:27
|
||||
msgid "Sync Now!"
|
||||
msgstr "Jetzt Synchronisieren!"
|
||||
|
||||
#: cookbook/templates/batch/waiting.html:4
|
||||
#: cookbook/templates/batch/waiting.html:10
|
||||
#: .\cookbook\templates\batch\waiting.html:4
|
||||
#: .\cookbook\templates\batch\waiting.html:10
|
||||
msgid "Importing Recipes"
|
||||
msgstr "Rezept werden importiert"
|
||||
|
||||
#: cookbook/templates/batch/waiting.html:23
|
||||
#: .\cookbook\templates\batch\waiting.html:23
|
||||
msgid ""
|
||||
"This can take a few minutes, depending on the number of recipes in sync, "
|
||||
"please wait."
|
||||
@@ -192,120 +290,176 @@ msgstr ""
|
||||
"Abhängig von der Anzahl der Rezepte kann dieser Vorgang einige Minuten "
|
||||
"dauern, bitte warten."
|
||||
|
||||
#: cookbook/templates/books.html:4 cookbook/templates/books.html:10
|
||||
#: .\cookbook\templates\books.html:4 .\cookbook\templates\books.html:10
|
||||
msgid "Recipe Books"
|
||||
msgstr "Rezept Bücher"
|
||||
|
||||
#: cookbook/templates/books.html:14
|
||||
#: .\cookbook\templates\books.html:14
|
||||
msgid "New Book"
|
||||
msgstr "Neues Buch"
|
||||
|
||||
#: cookbook/templates/books.html:53
|
||||
#: .\cookbook\templates\books.html:53
|
||||
msgid "There are no recipes in this book yet."
|
||||
msgstr "In diesem Buch sind bisher keine Rezepte."
|
||||
|
||||
#: cookbook/templates/forms/edit_import_recipe.html:5
|
||||
#: cookbook/templates/forms/edit_import_recipe.html:9
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:5
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:9
|
||||
msgid "Import new Recipe"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: cookbook/templates/forms/edit_import_recipe.html:14
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:37
|
||||
#: cookbook/templates/generic/edit_template.html:23
|
||||
#: cookbook/templates/generic/new_template.html:23
|
||||
#: cookbook/templates/recipe_view.html:207
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:14
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:47
|
||||
#: .\cookbook\templates\generic\edit_template.html:23
|
||||
#: .\cookbook\templates\generic\new_template.html:23
|
||||
#: .\cookbook\templates\recipe_view.html:340
|
||||
#: .\cookbook\templates\settings.html:33 .\cookbook\templates\settings.html:47
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:7
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:16
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:8
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:18
|
||||
msgid "Edit Recipe"
|
||||
msgstr "Rezept bearbeiten"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:26
|
||||
#: cookbook/templates/recipe_view.html:63
|
||||
msgid "Ingredients"
|
||||
msgstr "Zutaten"
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:37
|
||||
msgid ""
|
||||
"Use <b>Ctrl</b>+<b>Space</b> to insert new Ingredient!<br/>You can also save "
|
||||
"the recipe using <b>Ctrl</b>+<b>Shift</b>+<b>S</b>."
|
||||
msgstr ""
|
||||
"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:41
|
||||
#: cookbook/templates/generic/edit_template.html:27
|
||||
#: cookbook/templates/recipe_view.html:7
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:51
|
||||
#: .\cookbook\templates\generic\edit_template.html:27
|
||||
#: .\cookbook\templates\recipe_view.html:7
|
||||
msgid "View"
|
||||
msgstr "Angucken"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:45
|
||||
#: cookbook/templates/generic/edit_template.html:30
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:55
|
||||
#: .\cookbook\templates\generic\edit_template.html:30
|
||||
msgid "Delete original file"
|
||||
msgstr "Original löschen"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:90
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:127
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html: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:95
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:147
|
||||
msgid "Amount"
|
||||
msgstr "Menge"
|
||||
|
||||
#: cookbook/templates/forms/edit_internal_recipe.html:96
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:149
|
||||
msgid "Unit"
|
||||
msgstr "Einheit"
|
||||
|
||||
#: cookbook/templates/generic/delete_template.html:18
|
||||
#: .\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?"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:15
|
||||
msgid "Edit Ingredients"
|
||||
msgstr "Zutaten Bearbeiten"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:16
|
||||
msgid ""
|
||||
"\n"
|
||||
" The following form can be used if, accidentally, two (or more) units "
|
||||
"or ingredients where created that should be\n"
|
||||
" the same.\n"
|
||||
" It merges two units or ingredients and updates all recipes using "
|
||||
"them.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Dieses Formular kann genutzt werden wenn versehentlich zwei (oder "
|
||||
"mehr) Einheitenoder Zutaten erstellt wurden die eigentlich identisch\n"
|
||||
" sein sollen.\n"
|
||||
" Es vereint zwei Zutaten oder Einheiten und aktualisiert alle "
|
||||
"entsprechenden Rezepte.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:24
|
||||
msgid "Units"
|
||||
msgstr "Einheiten"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html: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:31
|
||||
#: .\cookbook\templates\forms\ingredients.html:40
|
||||
msgid "Merge"
|
||||
msgstr "Zusammenführen"
|
||||
|
||||
#: .\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 ?"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:18
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
|
||||
msgstr "Bist du sicher das %(title)s: <b>%(object)s</b> gelöscht werden soll"
|
||||
|
||||
#: cookbook/templates/generic/delete_template.html:21
|
||||
#: .\cookbook\templates\generic\delete_template.html:21
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
msgstr "Bestätigen"
|
||||
|
||||
#: cookbook/templates/generic/edit_template.html:6
|
||||
#: cookbook/templates/generic/edit_template.html:14
|
||||
#: .\cookbook\templates\generic\edit_template.html:6
|
||||
#: .\cookbook\templates\generic\edit_template.html:14
|
||||
msgid "Edit"
|
||||
msgstr "Bearbeiten"
|
||||
|
||||
#: cookbook/templates/generic/list_template.html:6
|
||||
#: cookbook/templates/generic/list_template.html:12
|
||||
#: .\cookbook\templates\generic\list_template.html:6
|
||||
#: .\cookbook\templates\generic\list_template.html:12
|
||||
msgid "List"
|
||||
msgstr "Liste"
|
||||
|
||||
#: cookbook/templates/generic/list_template.html:19
|
||||
#: .\cookbook\templates\generic\list_template.html:25
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:30
|
||||
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\delete.py:21 .\cookbook\views\edit.py:315
|
||||
#: .\cookbook\views\new.py:34
|
||||
msgid "Recipe"
|
||||
msgstr "Rezept"
|
||||
|
||||
#: cookbook/templates/include/recipe_open_modal.html:39
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:39
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: cookbook/templates/include/recipe_open_modal.html:56
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:53
|
||||
msgid "Open Recipe"
|
||||
msgstr "Rezept öffnen"
|
||||
|
||||
#: cookbook/templates/include/storage_backend_warning.html:4
|
||||
#: .\cookbook\templates\include\storage_backend_warning.html:4
|
||||
msgid "Security Warning"
|
||||
msgstr "Sicherheitswarnung"
|
||||
|
||||
#: cookbook/templates/include/storage_backend_warning.html:5
|
||||
#: .\cookbook\templates\include\storage_backend_warning.html:5
|
||||
msgid ""
|
||||
"\n"
|
||||
" The <b>Password and Token</b> field are stored as <b>plain text</b> "
|
||||
@@ -327,56 +481,78 @@ 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:59
|
||||
#: .\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"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:27
|
||||
#: .\cookbook\templates\meal_plan.html:20
|
||||
msgid "Week"
|
||||
msgstr "Woche"
|
||||
|
||||
#: .\cookbook\templates\recipe_view.html:67
|
||||
msgid "in"
|
||||
msgstr "in"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:32
|
||||
#: cookbook/templates/recipe_view.html:174
|
||||
#: .\cookbook\templates\recipe_view.html:72
|
||||
#: .\cookbook\templates\recipe_view.html:293
|
||||
msgid "by"
|
||||
msgstr "von"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:43
|
||||
#: .\cookbook\templates\recipe_view.html:84
|
||||
msgid "Preparation time ca."
|
||||
msgstr "Zubereitungszeit ca."
|
||||
|
||||
#: cookbook/templates/recipe_view.html:48
|
||||
#: .\cookbook\templates\recipe_view.html:89
|
||||
msgid "Waiting time ca."
|
||||
msgstr "Zubereitungszeit ca."
|
||||
msgstr "Wartezeit ca."
|
||||
|
||||
#: cookbook/templates/recipe_view.html:110
|
||||
#: .\cookbook\templates\recipe_view.html:170
|
||||
msgid "Recipe Image"
|
||||
msgstr "Rezept Bild"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:126
|
||||
#: .\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:137
|
||||
#: .\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:139
|
||||
#: .\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 "
|
||||
@@ -386,56 +562,81 @@ msgstr ""
|
||||
"bleibt weiterhin verfügbar.\n"
|
||||
" "
|
||||
|
||||
#: cookbook/templates/recipe_view.html:147
|
||||
#: .\cookbook\templates\recipe_view.html:225
|
||||
msgid "Convert now!"
|
||||
msgstr "Jetzt umwandeln!"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:156
|
||||
#: .\cookbook\templates\recipe_view.html:289
|
||||
msgid "Comments"
|
||||
msgstr "Kommentare"
|
||||
|
||||
#: cookbook/templates/recipe_view.html:165 cookbook/views/edit.py:191
|
||||
#: cookbook/views/edit.py:353
|
||||
#: .\cookbook\templates\recipe_view.html:309 .\cookbook\views\delete.py:103
|
||||
#: .\cookbook\views\edit.py:234
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#: cookbook/templates/registration/login.html:8
|
||||
#: .\cookbook\templates\registration\login.html:8
|
||||
msgid "Your username and password didn't match. Please try again."
|
||||
msgstr "Nutzername oder Passwort falsch. Bitte versuch es erneut."
|
||||
|
||||
#: cookbook/templates/stats.html:4
|
||||
#: .\cookbook\templates\settings.html:17
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: .\cookbook\templates\settings.html:42
|
||||
msgid "Style"
|
||||
msgstr "Stil"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:15
|
||||
msgid "Shopping List"
|
||||
msgstr "Einkaufsliste"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:20
|
||||
msgid "Load"
|
||||
msgstr "Laden"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:37
|
||||
#: .\cookbook\templates\shopping_list.html:55
|
||||
msgid "Copy list to clipboard"
|
||||
msgstr "Kopiere Liste in Zwischenablage"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:48
|
||||
msgid "Copied!"
|
||||
msgstr "Kopiert!"
|
||||
|
||||
#: .\cookbook\templates\stats.html:4
|
||||
msgid "Stats"
|
||||
msgstr "Statistiken"
|
||||
|
||||
#: cookbook/templates/stats.html:17
|
||||
#: .\cookbook\templates\stats.html:17
|
||||
msgid "Number of objects"
|
||||
msgstr "Anzahl der Objekte"
|
||||
|
||||
#: cookbook/templates/stats.html:20
|
||||
#: .\cookbook\templates\stats.html:20
|
||||
msgid "Recipes"
|
||||
msgstr "Rezepte"
|
||||
|
||||
#: cookbook/templates/stats.html:24
|
||||
#: .\cookbook\templates\stats.html:24
|
||||
msgid "Recipe Imports"
|
||||
msgstr "Rezept Importe"
|
||||
|
||||
#: cookbook/templates/stats.html:32
|
||||
#: .\cookbook\templates\stats.html:32
|
||||
msgid "Objects stats"
|
||||
msgstr "Objekt Statistiken"
|
||||
|
||||
#: cookbook/templates/stats.html:35
|
||||
#: .\cookbook\templates\stats.html:35
|
||||
msgid "Recipes without Keywords"
|
||||
msgstr "Rezepte ohne Schlagwort"
|
||||
|
||||
#: cookbook/views/api.py:63
|
||||
#: .\cookbook\views\api.py:63
|
||||
msgid "Sync successful!"
|
||||
msgstr "Synchronisation erfolgreich!"
|
||||
|
||||
#: cookbook/views/api.py:66
|
||||
#: .\cookbook\views\api.py:66
|
||||
msgid "Error synchronizing with Storage"
|
||||
msgstr "Fehler beim Synchronisieren"
|
||||
|
||||
#: cookbook/views/data.py:71
|
||||
#: .\cookbook\views\data.py:71
|
||||
#, python-format
|
||||
msgid "Batch edit done. %(count)d recipe was updated."
|
||||
msgid_plural "Batch edit done. %(count)d Recipes where updated."
|
||||
@@ -443,65 +644,81 @@ msgstr[0] "Massenbearbeitung erfolgreich. %(count)d Rezept wurde aktualisiert."
|
||||
msgstr[1] ""
|
||||
"Massenbearbeitung erfolgreich. %(count)d Rezepte wurden aktualisiert."
|
||||
|
||||
#: cookbook/views/edit.py:88
|
||||
msgid "Recipe saved!"
|
||||
msgstr "Rezept gespeichert"
|
||||
|
||||
#: cookbook/views/edit.py:91 cookbook/views/new.py:87
|
||||
msgid "There was an error importing this recipe!"
|
||||
msgstr "Beim importieren des Rezeptes ist ein Fehler aufgetreten"
|
||||
|
||||
#: cookbook/views/edit.py:139 cookbook/views/edit.py:182
|
||||
msgid "You cannot edit this comment!"
|
||||
msgstr "Du kannst diesen Kommentar nicht bearbeiten!"
|
||||
|
||||
#: cookbook/views/edit.py:158
|
||||
msgid "Storage saved!"
|
||||
msgstr "Speicherquelle gespeichert"
|
||||
|
||||
#: 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
|
||||
#: .\cookbook\views\delete.py:48 .\cookbook\views\edit.py:251
|
||||
#: .\cookbook\views\lists.py:35
|
||||
msgid "Import"
|
||||
msgstr "Rezept Importieren"
|
||||
|
||||
#: cookbook/views/edit.py:224 cookbook/views/edit.py:364
|
||||
#: cookbook/views/new.py:110
|
||||
msgid "Recipe Book"
|
||||
msgstr "Rezeptbuch"
|
||||
|
||||
#: cookbook/views/edit.py:246
|
||||
msgid "Changes saved!"
|
||||
msgstr "Änderungen gespeichert"
|
||||
|
||||
#: cookbook/views/edit.py:250
|
||||
msgid "Error saving changes!"
|
||||
msgstr "Fehler beim Speichern der Daten."
|
||||
|
||||
#: cookbook/views/edit.py:320
|
||||
#: .\cookbook\views\delete.py:59
|
||||
msgid "Monitor"
|
||||
msgstr "Monitor"
|
||||
|
||||
#: cookbook/views/edit.py:342 cookbook/views/lists.py:42
|
||||
#: cookbook/views/new.py:62
|
||||
#: .\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:375
|
||||
#: .\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/new.py:84
|
||||
#: .\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"
|
||||
|
||||
#: cookbook/views/views.py:42
|
||||
#: .\cookbook\views\new.py:89
|
||||
msgid "There was an error importing this recipe!"
|
||||
msgstr "Beim importieren des Rezeptes ist ein Fehler aufgetreten"
|
||||
|
||||
#: .\cookbook\views\views.py:44
|
||||
msgid "Comment saved!"
|
||||
msgstr "Kommentar gespeichert"
|
||||
|
||||
#: cookbook/views/views.py:52
|
||||
#: .\cookbook\views\views.py:54
|
||||
msgid "Bookmark saved!"
|
||||
msgstr "Lesezeichen gespeichert"
|
||||
|
||||
27
cookbook/migrations/0008_mealplan.py
Normal file
27
cookbook/migrations/0008_mealplan.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
27
cookbook/migrations/0009_auto_20200130_1056.py
Normal file
27
cookbook/migrations/0009_auto_20200130_1056.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
28
cookbook/migrations/0010_auto_20200130_1059.py
Normal file
28
cookbook/migrations/0010_auto_20200130_1059.py
Normal 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),
|
||||
]
|
||||
17
cookbook/migrations/0011_remove_recipeingredients_unit.py
Normal file
17
cookbook/migrations/0011_remove_recipeingredients_unit.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0012_auto_20200130_1116.py
Normal file
18
cookbook/migrations/0012_auto_20200130_1116.py
Normal 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',
|
||||
),
|
||||
]
|
||||
24
cookbook/migrations/0013_userpreference.py
Normal file
24
cookbook/migrations/0013_userpreference.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
26
cookbook/migrations/0014_auto_20200213_2332.py
Normal file
26
cookbook/migrations/0014_auto_20200213_2332.py
Normal 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),
|
||||
),
|
||||
]
|
||||
21
cookbook/migrations/0015_auto_20200213_2334.py
Normal file
21
cookbook/migrations/0015_auto_20200213_2334.py
Normal 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),
|
||||
),
|
||||
]
|
||||
25
cookbook/migrations/0016_auto_20200213_2335.py
Normal file
25
cookbook/migrations/0016_auto_20200213_2335.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0017_auto_20200216_2257.py
Normal file
18
cookbook/migrations/0017_auto_20200216_2257.py
Normal 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),
|
||||
),
|
||||
]
|
||||
17
cookbook/migrations/0018_auto_20200216_2303.py
Normal file
17
cookbook/migrations/0018_auto_20200216_2303.py
Normal 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',
|
||||
),
|
||||
]
|
||||
20
cookbook/migrations/0019_ingredient.py
Normal file
20
cookbook/migrations/0019_ingredient.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0020_recipeingredient_ingredient.py
Normal file
19
cookbook/migrations/0020_recipeingredient_ingredient.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
26
cookbook/migrations/0021_auto_20200216_2309.py
Normal file
26
cookbook/migrations/0021_auto_20200216_2309.py
Normal 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),
|
||||
]
|
||||
17
cookbook/migrations/0022_remove_recipeingredient_name.py
Normal file
17
cookbook/migrations/0022_remove_recipeingredient_name.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0023_auto_20200216_2311.py
Normal file
18
cookbook/migrations/0023_auto_20200216_2311.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0024_auto_20200216_2313.py
Normal file
18
cookbook/migrations/0024_auto_20200216_2313.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0025_userpreference_nav_color.py
Normal file
18
cookbook/migrations/0025_userpreference_nav_color.py
Normal 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),
|
||||
),
|
||||
]
|
||||
23
cookbook/migrations/0026_auto_20200219_1605.py
Normal file
23
cookbook/migrations/0026_auto_20200219_1605.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.2 on 2020-02-19 15:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0025_userpreference_nav_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='recipe',
|
||||
name='cors_link',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recipe',
|
||||
name='link',
|
||||
field=models.CharField(blank=True, max_length=512, null=True),
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0027_ingredient_recipe.py
Normal file
19
cookbook/migrations/0027_ingredient_recipe.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0028_auto_20200317_1901.py
Normal file
19
cookbook/migrations/0028_auto_20200317_1901.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0029_auto_20200317_1901.py
Normal file
19
cookbook/migrations/0029_auto_20200317_1901.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0030_recipeingredient_note.py
Normal file
18
cookbook/migrations/0030_recipeingredient_note.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,54 @@
|
||||
from django.contrib.auth.models import User
|
||||
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'
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.user
|
||||
|
||||
|
||||
class Storage(models.Model):
|
||||
DROPBOX = 'DB'
|
||||
NEXTCLOUD = 'NEXTCLOUD'
|
||||
@@ -38,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)
|
||||
@@ -48,7 +97,10 @@ class Keyword(models.Model):
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{0} {1}".format(self.icon, self.name)
|
||||
if self.icon:
|
||||
return f"{self.icon} {self.name}"
|
||||
else:
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class Recipe(models.Model):
|
||||
@@ -58,7 +110,8 @@ class Recipe(models.Model):
|
||||
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
|
||||
file_uid = models.CharField(max_length=256, default="")
|
||||
file_path = models.CharField(max_length=512, default="")
|
||||
link = models.CharField(max_length=512, default="")
|
||||
link = models.CharField(max_length=512, null=True, blank=True)
|
||||
cors_link = models.CharField(max_length=1024, null=True, blank=True)
|
||||
keywords = models.ManyToManyField(Keyword, blank=True)
|
||||
working_time = models.IntegerField(default=0)
|
||||
waiting_time = models.IntegerField(default=0)
|
||||
@@ -72,14 +125,34 @@ class Recipe(models.Model):
|
||||
|
||||
@property
|
||||
def all_tags(self):
|
||||
return ' '.join([(x.icon + x.name) for x in self.keywords.all()])
|
||||
return ' '.join([(str(x)) 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)
|
||||
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)
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
unit = models.CharField(max_length=128)
|
||||
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)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
@@ -89,6 +162,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 +191,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)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
@@ -34,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'])
|
||||
@@ -88,6 +89,16 @@ class Dropbox(Provider):
|
||||
response = Dropbox.create_share_link(recipe)
|
||||
return response['url']
|
||||
|
||||
@staticmethod
|
||||
def get_base64_file(recipe):
|
||||
if not recipe.link:
|
||||
recipe.link = Dropbox.get_share_link(recipe)
|
||||
recipe.save()
|
||||
|
||||
response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
|
||||
|
||||
return base64.b64encode(response.content)
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
url = "https://api.dropboxapi.com/2/files/move_v2"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import base64
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
import webdav3.client as wc
|
||||
import requests
|
||||
|
||||
from io import BytesIO
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from cookbook.models import Recipe, RecipeImport, SyncLog
|
||||
@@ -30,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)
|
||||
@@ -81,6 +83,20 @@ class Nextcloud(Provider):
|
||||
|
||||
return Nextcloud.create_share_link(recipe)
|
||||
|
||||
@staticmethod
|
||||
def get_base64_file(recipe):
|
||||
client = Nextcloud.get_client(recipe.storage)
|
||||
|
||||
tmp_file_path = tempfile.gettempdir() + '/' + recipe.name + '.pdf'
|
||||
|
||||
client.download_file(remote_path=recipe.file_path, local_path=tmp_file_path)
|
||||
|
||||
val = base64.b64encode(open(tmp_file_path, 'rb').read())
|
||||
|
||||
os.remove(tmp_file_path)
|
||||
|
||||
return val
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
client = Nextcloud.get_client(recipe.storage)
|
||||
|
||||
@@ -11,10 +11,14 @@ class Provider:
|
||||
def get_share_link(recipe):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
@staticmethod
|
||||
def get_base64_file(recipe):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
@staticmethod
|
||||
def delete_file(recipe, new_name):
|
||||
def delete_file(recipe):
|
||||
raise Exception('Method not implemented in storage provider')
|
||||
|
||||
3
cookbook/static/custom/js/form_select.js
Normal file
3
cookbook/static/custom/js/form_select.js
Normal file
@@ -0,0 +1,3 @@
|
||||
$(document).ready(function () {
|
||||
$('.selectwidget').select2();
|
||||
});
|
||||
3
cookbook/static/tabulator/tabulator_midnight.min.css
vendored
Normal file
3
cookbook/static/tabulator/tabulator_midnight.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
cookbook/static/tabulator/tabulator_modern.min.css
vendored
Normal file
3
cookbook/static/tabulator/tabulator_modern.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
cookbook/static/tabulator/tabulator_simple.min.css
vendored
Normal file
3
cookbook/static/tabulator/tabulator_simple.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
cookbook/static/tabulator/tabulator_site.min.css
vendored
Normal file
3
cookbook/static/tabulator/tabulator_site.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
cookbook/static/themes/bootstrap.min.css
vendored
Normal file
7
cookbook/static/themes/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
cookbook/static/themes/darkly.min.css
vendored
Normal file
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
12
cookbook/static/themes/flatly.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
722
cookbook/static/themes/select2-bootstrap-theme.css
Normal file
722
cookbook/static/themes/select2-bootstrap-theme.css
Normal 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;
|
||||
}
|
||||
}
|
||||
12
cookbook/static/themes/superhero.min.css
vendored
Normal file
12
cookbook/static/themes/superhero.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -8,8 +8,7 @@ from .models import *
|
||||
|
||||
class RecipeTable(tables.Table):
|
||||
id = tables.LinkColumn('edit_recipe', args=[A('id')])
|
||||
name = tables.TemplateColumn(
|
||||
"<a href='#' onClick='openRecipe({{record.id}})'>{{record.name}}</a>")
|
||||
name = tables.LinkColumn('view_recipe', args=[A('id')])
|
||||
all_tags = tables.Column(
|
||||
attrs={'td': {'class': 'd-none d-lg-table-cell'}, 'th': {'class': 'd-none d-lg-table-cell'}})
|
||||
|
||||
@@ -28,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')])
|
||||
|
||||
@@ -45,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
|
||||
@@ -72,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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load theming_tags %}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
@@ -18,11 +19,11 @@
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||
|
||||
<!-- Bootstrap 4 -->
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
|
||||
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
|
||||
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.js"
|
||||
integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
|
||||
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
|
||||
crossorigin="anonymous"></script>
|
||||
@@ -31,13 +32,18 @@
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Select2 for use with django autocomplete light -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/css/select2.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/js/select2.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.min.js"></script>
|
||||
|
||||
<!-- Bootstrap theme for select2 -->
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/select2-bootstrap-theme/0.1.0-beta.10/select2-bootstrap.css"
|
||||
integrity="sha256-zFnNbsU+u3l0K+MaY92RvJI6AdAVAxK3/QrBApHvlH8=" crossorigin="anonymous"/>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'themes/select2-bootstrap-theme.css' %}"
|
||||
crossorigin="anonymous"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
$.fn.select2.defaults.set("theme", "bootstrap");
|
||||
</script>
|
||||
@@ -60,7 +66,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}" id="id_main_nav">
|
||||
<!--<a class="navbar-brand" href="{% url 'index' %}">{% trans 'Cookbook' %}</a>-->
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
@@ -68,59 +74,90 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<li class="nav-item {% if request.resolver_match.url_name 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">
|
||||
<a class="nav-link" href="{% url 'view_books' %}"><i class="fas fa-bookmark"></i> {% trans 'Books' %}</a>
|
||||
|
||||
<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 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 fa-fw"></i> {% trans 'Units & Ingredients' %}</a>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}"><i
|
||||
class="fas fa-user-shield"></i> {% trans 'Admin' %}</a>
|
||||
</li>
|
||||
<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>
|
||||
{% 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 'login' %}">{% trans 'Login' %} <i class="fas fa-sign-in-alt"></i></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<br/>
|
||||
@@ -141,5 +178,8 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block script %}
|
||||
{% endblock script %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -41,7 +41,7 @@
|
||||
{% for r in b.recipes %}
|
||||
<div class="row">
|
||||
<div class="col col-md-10">
|
||||
<li><a href="#" onClick='openRecipe({{ r.recipe.pk }})'>{{ r.recipe.name }}</a></li>
|
||||
<li><a href="{% url 'view_recipe' r.recipe.pk %}">{{ r.recipe.name }}</a></li>
|
||||
</div>
|
||||
<div class="col col-md-2" style="text-align: right">
|
||||
<a href="{% url 'delete_recipe_book_entry' r.pk %}"><i class="fas fa-trash-alt"></i></a>
|
||||
@@ -58,5 +58,4 @@
|
||||
<br/>
|
||||
{% endfor %}
|
||||
|
||||
{% include 'include/recipe_open_modal.html' %}
|
||||
{% endblock %}
|
||||
@@ -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,16 +28,24 @@
|
||||
<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 %}"
|
||||
<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>
|
||||
@@ -47,37 +57,71 @@
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function selectText(node) {
|
||||
|
||||
if (document.body.createTextRange) {
|
||||
const range = document.body.createTextRange();
|
||||
range.moveToElementText(node);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.");
|
||||
}
|
||||
}
|
||||
$(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;
|
||||
};
|
||||
|
||||
//converts multiselct in recipe edit to searchable multiselect
|
||||
//shitty solution that needs to be redone at some point
|
||||
$(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,52 +129,84 @@
|
||||
movableRows: true,
|
||||
headerSort: false,
|
||||
columns: [
|
||||
{ title: "<i class='fas fa-sort'></i>", rowHandle:true, formatter:"handle", headerSort:false, frozen:true, width:36, minWidth:36},
|
||||
{
|
||||
title: "<i class='fas fa-sort'></i>",
|
||||
rowHandle: true,
|
||||
formatter: "handle",
|
||||
headerSort: false,
|
||||
frozen: true,
|
||||
width: 36,
|
||||
minWidth: 36
|
||||
},
|
||||
{
|
||||
title: "{% trans 'Ingredient' %}",
|
||||
field: "name",
|
||||
field: "ingredient__name",
|
||||
validator: "required",
|
||||
editor: "input"
|
||||
editor: select2IngredientEditor
|
||||
},
|
||||
{title: "{% trans 'Amount' %}", field: "amount", validator: "required", editor: "input"},
|
||||
{title: "{% trans 'Unit' %}", field: "unit", validator: "required", editor: "input"},
|
||||
{
|
||||
title: "{% trans 'Delete' %}",
|
||||
field: "delete",
|
||||
title: "{% trans 'Unit' %}",
|
||||
field: "unit__name",
|
||||
validator: "required",
|
||||
editor: select2UnitEditor
|
||||
},
|
||||
{title: "{% trans 'Note' %}", field: "note", editor: "input"},
|
||||
{
|
||||
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))
|
||||
|
||||
data.forEach(function (cur, i) {
|
||||
if (cur.delete) {
|
||||
table.deleteRow(cur.id);
|
||||
}
|
||||
})
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// save ingredient data before submitting form
|
||||
$('#id_form').submit(function () {
|
||||
$('#id_ingredients').val(JSON.stringify(table.getData()));
|
||||
return true;
|
||||
});
|
||||
|
||||
// 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",
|
||||
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) {
|
||||
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>
|
||||
|
||||
43
cookbook/templates/forms/ingredients.html
Normal file
43
cookbook/templates/forms/ingredients.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% load django_tables2 %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Cookbook" %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ units_form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2><i class="fas fa-shopping-cart"></i> {% trans 'Edit Ingredients' %}</h2>
|
||||
{% blocktrans %}
|
||||
The following form can be used if, accidentally, two (or more) units or ingredients where created that should be
|
||||
the same.
|
||||
It merges two units or ingredients and updates all recipes using them.
|
||||
{% endblocktrans %}
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h4>{% trans 'Units' %}</h4>
|
||||
<form action="{% url 'edit_ingredient' %}" method="post"
|
||||
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"
|
||||
><i
|
||||
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
|
||||
</form>
|
||||
|
||||
<h4>{% trans 'Ingredients' %}</h4>
|
||||
<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">
|
||||
<i class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -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,8 +21,8 @@
|
||||
{% 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 %}"
|
||||
class="btn btn-danger">{% trans 'Delete' %}</a>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -23,4 +23,15 @@
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
</form>
|
||||
|
||||
{% if default_recipe %}
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#id_recipe').val({{ default_recipe.pk }}).trigger('change');
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -43,15 +43,12 @@
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function openRecipe(id, force_external = false) {
|
||||
function openRecipe(id) {
|
||||
var link = $('#a_recipe_open');
|
||||
link.hide();
|
||||
$('#div_loader').show();
|
||||
|
||||
var url = "{% url 'api_get_file_link' recipe_id=12345 %}".replace(/12345/, id);
|
||||
if (force_external) {
|
||||
url = "{% url 'api_get_external_file_link' recipe_id=12345 %}".replace(/12345/, id);
|
||||
}
|
||||
var url = "{% url 'api_get_external_file_link' recipe_id=12345 %}".replace(/12345/, id);
|
||||
|
||||
link.text("{% trans 'Open Recipe' %}");
|
||||
$('#modal_recipe').modal('show');
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
{% block extra_head %}
|
||||
{{ filter.form.media }}
|
||||
|
||||
<style>
|
||||
.dropdown-toggle-no-arrow::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -21,31 +27,44 @@
|
||||
<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>
|
||||
{{ filter.form.ingredients | as_crispy_field }}
|
||||
</div>
|
||||
<div>
|
||||
{{ filter.form.internal | as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -60,6 +79,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include 'include/recipe_open_modal.html' %}
|
||||
|
||||
{% endblock %}
|
||||
70
cookbook/templates/meal_plan.html
Normal file
70
cookbook/templates/meal_plan.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>
|
||||
{% trans 'Meal-Plan' %} <a href="{% url 'new_meal_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_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>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -9,6 +9,40 @@
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pretty-checkbox@3.0/dist/pretty-checkbox.min.css"
|
||||
integrity="sha384-ICB8i/maQ/5+tGLDUEcswB7Ch+OO9Oj8Z4Ov/Gs0gxqfTgLLkD3F43MhcEJ2x6/D" crossorigin="anonymous">
|
||||
|
||||
|
||||
<!-- prevent weired character stuff escaping the pdf box -->
|
||||
<style>
|
||||
.textLayer > span {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: #f9f9f9;
|
||||
border-left: 4px solid #ccc;
|
||||
margin: 1.5em 10px;
|
||||
padding: .5em 10px;
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before {
|
||||
color: #ccc;
|
||||
content: open-quote;
|
||||
font-size: 4em;
|
||||
line-height: .1em;
|
||||
margin-right: .25em;
|
||||
vertical-align: -.4em;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -20,6 +54,12 @@
|
||||
<div class="col col-md-3 d-print-none" style="text-align: right">
|
||||
<button class="btn btn-success" onclick="$('#bookmarkModal').modal({'show':true})"><i
|
||||
class="fas fa-bookmark"></i></button>
|
||||
{% if ingredients %}
|
||||
<a class="btn btn-warning" href="{% url 'view_shopping' %}?r={{ recipe.pk }}"><i
|
||||
class="fas fa-shopping-cart"></i></a>
|
||||
{% endif %}
|
||||
<a class="btn btn-info" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}"><i
|
||||
class="fas fa-calendar"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,23 +69,26 @@
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.internal %}
|
||||
<small>{% trans 'by' %} {{ recipe.created_by.username }}<br/></small>
|
||||
<br/>
|
||||
<small>{% trans 'by' %} {{ recipe.created_by.get_user_name }}<br/></small>
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.all_tags %}
|
||||
{{ recipe.all_tags }}
|
||||
{% if recipe.keywords %}
|
||||
{% for x in recipe.keywords.all %}
|
||||
<span class="badge badge-pill badge-light">{{ x }}</span>
|
||||
{% endfor %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% 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 %}
|
||||
@@ -55,7 +98,7 @@
|
||||
|
||||
<div class="row">
|
||||
{% if ingredients %}
|
||||
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2">
|
||||
<div class="col-lg-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
@@ -74,10 +117,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">
|
||||
@@ -93,11 +136,36 @@
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td style="font-size: large">{{ i.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" 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>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<!-- Bottom border -->
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,47 +181,131 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if recipe.ingredients or recipe.image %}
|
||||
{% if ingredients or recipe.image %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
|
||||
{% if recipe.instructions %}
|
||||
{{ recipe.instructions | markdown | safe }}
|
||||
{% endif %}
|
||||
<div style="font-size: large">
|
||||
{% if recipe.instructions %}
|
||||
{{ recipe.instructions | markdown | safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% if recipe.storage %}
|
||||
<a href='#' onClick='openRecipe({{ recipe.id }}, true)' class="d-print-none">{% trans 'View external recipe' %}
|
||||
<i
|
||||
class="fas fa-external-link-alt"></i></a>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if recipe.internal %}
|
||||
<div class="col col-12" style="margin-top: 2vh">
|
||||
<a href='#' onClick='openRecipe({{ recipe.id }})'
|
||||
class="d-print-none">{% trans 'View external recipe' %} <i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% if not recipe.internal %}
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-info">
|
||||
<h5 class="card-title">{% trans 'External recipe' %}</h5>
|
||||
<p class="card-text">
|
||||
{% blocktrans %}
|
||||
This is an external recipe, which means you can only view it by opening the link above.
|
||||
You can convert this recipe to a fancy recipe by pressing the convert button. The original file
|
||||
will still be accessible.
|
||||
{% endblocktrans %}.
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="{% url 'edit_convert_recipe' recipe.pk %}"
|
||||
class="card-link btn btn-info">{% trans 'Convert now!' %}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col col-12" style="margin-top: 2vh">
|
||||
<div class="loader" id="id_loader"></div>
|
||||
|
||||
<div id="viewerContainer" class="border">
|
||||
<div id="viewer" class="pdfViewer"></div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" role="alert" id="id_warning_no_preview" style="display: none">
|
||||
{% trans 'Cloud not show a file preview. Maybe its not a PDF ?' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-12" style="margin-top: 2vh">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-info">
|
||||
<h5 class="card-title">{% trans 'External recipe' %}</h5>
|
||||
<p class="card-text">
|
||||
{% blocktrans %}
|
||||
This is an external recipe, which means you can only view it by opening the link
|
||||
above.
|
||||
You can convert this recipe to a fancy recipe by pressing the convert button. The
|
||||
original
|
||||
file
|
||||
will still be accessible.
|
||||
{% endblocktrans %}.
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="{% url 'edit_convert_recipe' recipe.pk %}"
|
||||
class="card-link btn btn-info">{% trans 'Convert now!' %}</a>
|
||||
<a href='#' onClick='openRecipe({{ recipe.id }})'
|
||||
class="d-print-none btn btn-warning">{% trans 'View external recipe' %} <i
|
||||
class="fas fa-external-link-alt"></i></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf.min.js"
|
||||
integrity="sha256-J4Z8Fhj2MITUakMQatkqOVdtqodUlwHtQ/ey6fSsudE="
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf_viewer.js"
|
||||
integrity="sha256-JW7ackRikw8/UM/hHV6vKaZBYc+t2ZQ77sd3LWR8vh8="
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var url = "{% url 'api_get_recipe_file' recipe_id=12345 %}".replace(/12345/, {{ recipe.id }});
|
||||
$('#viewerContainer').hide();
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
var base64Pdf = atob(this.responseText);
|
||||
$('#id_loader').hide();
|
||||
$('#viewerContainer').show();
|
||||
|
||||
var container = document.getElementById("viewerContainer");
|
||||
|
||||
var pdfViewer = new pdfjsViewer.PDFViewer({
|
||||
container: container,
|
||||
});
|
||||
|
||||
document.addEventListener("pagesinit", function () {
|
||||
// We can use pdfViewer now, e.g. let's change default scale.
|
||||
pdfViewer.currentScaleValue = "page-width";
|
||||
|
||||
});
|
||||
|
||||
var loadingTask = pdfjsLib.getDocument({
|
||||
data: base64Pdf
|
||||
});
|
||||
|
||||
loadingTask.promise.then(function (pdfDocument) {
|
||||
// Document loaded, specifying document for the viewer and
|
||||
// the (optional) linkService.
|
||||
pdfViewer.setDocument(pdfDocument);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
xhttp.open("GET", url, true);
|
||||
xhttp.send();
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h5>{% trans 'Comments' %}</h5>
|
||||
<h5><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">
|
||||
@@ -168,17 +320,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 %}
|
||||
@@ -213,6 +354,14 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover()
|
||||
});
|
||||
|
||||
$('.popover-dismiss').popover({
|
||||
trigger: 'focus'
|
||||
});
|
||||
|
||||
function reloadIngredients() {
|
||||
factor = Number($('#in_factor').val());
|
||||
ingredients = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
70
cookbook/templates/settings.html
Normal file
70
cookbook/templates/settings.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% 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-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 %}
|
||||
<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 fa-fw"></i> {% trans 'Style' %}</h4>
|
||||
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ preference_form|crispy }}
|
||||
<button class="btn btn-success" type="submit" name="preference_form"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
64
cookbook/templates/shopping_list.html
Normal file
64
cookbook/templates/shopping_list.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% load django_tables2 %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Cookbook" %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2><i class="fas fa-shopping-cart"></i> {% trans 'Shopping List' %}</h2>
|
||||
|
||||
<form action="{% url 'view_shopping' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-sync-alt"></i> {% trans 'Load' %}</button>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<!--// @formatter:off-->
|
||||
<textarea id="id_list" class="form-control" rows="{{ ingredients|length|add:1 }}">{% for i in ingredients %}{% if markdown_format %}- [ ]{% endif %} {{ i.amount.normalize }} {{ i.unit }} {{ i.ingredient.name }} {% endfor %}</textarea>
|
||||
<!--// @formatter:on-->
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-center">
|
||||
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
|
||||
data-placement="top" title="{% trans 'Copy list to clipboard' %}" id="id_btn_copy" onmouseout="resetTooltip()"><i
|
||||
class="far fa-copy"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function copy() {
|
||||
let list = $('#id_list');
|
||||
|
||||
list.select();
|
||||
|
||||
$('#id_btn_copy').attr('data-original-title','{% trans 'Copied!' %}').tooltip('show');
|
||||
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
function resetTooltip() {
|
||||
setTimeout(function () {
|
||||
$('#id_btn_copy').attr('data-original-title','{% trans 'Copy list to clipboard' %}');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,14 +1,32 @@
|
||||
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):
|
||||
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])
|
||||
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)
|
||||
|
||||
48
cookbook/templatetags/theming_tags.py
Normal file
48
cookbook/templatetags/theming_tags.py
Normal 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')
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
0
cookbook/tests/__init__.py
Normal file
0
cookbook/tests/__init__.py
Normal file
0
cookbook/tests/edits/__init__.py
Normal file
0
cookbook/tests/edits/__init__.py
Normal file
128
cookbook/tests/edits/test_edits_recipe.py
Normal file
128
cookbook/tests/edits/test_edits_recipe.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Recipe, RecipeIngredient, Ingredient, Unit, Storage
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
|
||||
|
||||
class TestEditsRecipe(TestViews):
|
||||
|
||||
def test_switch_recipe(self):
|
||||
internal_recipe = Recipe.objects.create(
|
||||
name='Test',
|
||||
internal=True,
|
||||
created_by=auth.get_user(self.client)
|
||||
)
|
||||
|
||||
external_recipe = Recipe.objects.create(
|
||||
name='Test',
|
||||
internal=False,
|
||||
created_by=auth.get_user(self.client)
|
||||
)
|
||||
|
||||
url = reverse('edit_recipe', args=[internal_recipe.pk])
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
r = self.client.get(r.url)
|
||||
self.assertTemplateUsed(r, 'forms/edit_internal_recipe.html')
|
||||
|
||||
url = reverse('edit_recipe', args=[external_recipe.pk])
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
r = self.client.get(r.url)
|
||||
self.assertTemplateUsed(r, 'generic/edit_template.html')
|
||||
|
||||
def test_convert_recipe(self):
|
||||
url = reverse('edit_convert_recipe', args=[42])
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
external_recipe = Recipe.objects.create(
|
||||
name='Test',
|
||||
internal=False,
|
||||
created_by=auth.get_user(self.client)
|
||||
)
|
||||
|
||||
url = reverse('edit_convert_recipe', args=[external_recipe.pk])
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
recipe = Recipe.objects.get(pk=external_recipe.pk)
|
||||
self.assertTrue(recipe.internal)
|
||||
|
||||
url = reverse('edit_convert_recipe', args=[recipe.pk])
|
||||
r = self.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)
|
||||
|
||||
Ingredient.objects.create(name='Egg')
|
||||
Unit.objects.create(name='g')
|
||||
|
||||
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,5","delete":false}]'})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(2, RecipeIngredient.objects.filter(recipe=recipe).count())
|
||||
|
||||
r = self.client.post(url,
|
||||
{'name': "Test", 'working_time': "Test", 'waiting_time': 15,
|
||||
'ingredients': '[{"ingredient__name":"Tomato","unit__name":"g","amount":100,"delete":false},{"ingredient__name":"Egg","unit__name":"Piece","amount":"2,5","delete":false}]'})
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
with open('cookbook/tests/resources/image.jpg', 'rb') as file:
|
||||
r = self.client.post(url, {'name': "Changed", 'working_time': 15, 'waiting_time': 15, 'image': file})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
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)
|
||||
39
cookbook/tests/edits/test_edits_storage.py
Normal file
39
cookbook/tests/edits/test_edits_storage.py
Normal 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.'])
|
||||
BIN
cookbook/tests/resources/image.jpg
Normal file
BIN
cookbook/tests/resources/image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
cookbook/tests/resources/image.png
Normal file
BIN
cookbook/tests/resources/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
24
cookbook/tests/test_setup.py
Normal file
24
cookbook/tests/test_setup.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, Client
|
||||
|
||||
|
||||
class TestBase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.anonymous_client = Client()
|
||||
|
||||
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)
|
||||
0
cookbook/tests/views/__init__.py
Normal file
0
cookbook/tests/views/__init__.py
Normal file
5
cookbook/tests/views/test_views.py
Normal file
5
cookbook/tests/views/test_views.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from cookbook.tests.test_setup import TestBase
|
||||
|
||||
|
||||
class TestViews(TestBase):
|
||||
pass
|
||||
37
cookbook/tests/views/test_views_general.py
Normal file
37
cookbook/tests/views/test_views_general.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
|
||||
|
||||
class TestViewsGeneral(TestViews):
|
||||
|
||||
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)
|
||||
@@ -1,3 +1,5 @@
|
||||
from pydoc import locate
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .views import *
|
||||
@@ -6,46 +8,24 @@ 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'),
|
||||
|
||||
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('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/ingredient/', edit.edit_ingredients, name='edit_ingredient'),
|
||||
|
||||
path('redirect/delete/<slug:name>/<int:pk>/', edit.delete_redirect, name='redirect_delete'),
|
||||
|
||||
path('delete/recipe/<int:pk>/', edit.RecipeDelete.as_view(), name='delete_recipe'),
|
||||
path('delete/recipe-source/<int:pk>/', edit.RecipeSourceDelete.as_view(), name='delete_recipe_source'),
|
||||
path('delete/keyword/<int:pk>/', edit.KeywordDelete.as_view(), name='delete_keyword'),
|
||||
path('delete/sync/<int:pk>/', edit.MonitorDelete.as_view(), name='delete_sync'),
|
||||
path('delete/import/<int:pk>/', edit.ImportDelete.as_view(), name='delete_import'),
|
||||
path('delete/storage/<int:pk>/', edit.StorageDelete.as_view(), name='delete_storage'),
|
||||
path('delete/comment/<int:pk>/', edit.CommentDelete.as_view(), name='delete_comment'),
|
||||
path('delete/recipe-book/<int:pk>/', edit.RecipeBookDelete.as_view(), name='delete_recipe_book'),
|
||||
path('delete/recipe-book-entry/<int:pk>/', edit.RecipeBookEntryDelete.as_view(), name='delete_recipe_book_entry'),
|
||||
path('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'),
|
||||
@@ -53,12 +33,30 @@ urlpatterns = [
|
||||
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
|
||||
path('data/statistics', data.statistics, name='data_stats'),
|
||||
|
||||
path('api/get_file_link/<int:recipe_id>/', api.get_file_link, name='api_get_file_link'),
|
||||
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
|
||||
|
||||
path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
|
||||
|
||||
path('api/sync_all/', api.sync_all, name='api_sync'),
|
||||
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||
path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'),
|
||||
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
|
||||
]
|
||||
|
||||
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}'))
|
||||
|
||||
@@ -3,4 +3,5 @@ from cookbook.views.api import *
|
||||
from cookbook.views.data import *
|
||||
from cookbook.views.edit import *
|
||||
from cookbook.views.new import *
|
||||
from cookbook.views.lists import *
|
||||
from cookbook.views.lists import *
|
||||
from cookbook.views.delete import *
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, FileResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -10,40 +10,40 @@ from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
|
||||
@login_required
|
||||
def get_file_link(request, recipe_id):
|
||||
recipe = Recipe.objects.get(id=recipe_id)
|
||||
def get_recipe_provider(recipe):
|
||||
if recipe.storage.method == Storage.DROPBOX:
|
||||
return Dropbox
|
||||
elif recipe.storage.method == Storage.NEXTCLOUD:
|
||||
return Nextcloud
|
||||
else:
|
||||
raise Exception('Provider not implemented')
|
||||
|
||||
if recipe.internal:
|
||||
return HttpResponse(reverse('view_recipe', args=[recipe_id]))
|
||||
if recipe.storage.method == Storage.DROPBOX: # TODO move to central location (as all provider related functions)
|
||||
if recipe.link == "":
|
||||
recipe.link = Dropbox.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
if recipe.storage.method == Storage.NEXTCLOUD:
|
||||
if recipe.link == "":
|
||||
recipe.link = Nextcloud.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
|
||||
return HttpResponse(recipe.link)
|
||||
def update_recipe_links(recipe):
|
||||
if not recipe.link:
|
||||
recipe.link = get_recipe_provider(recipe).get_share_link(recipe) # TODO response validation in apis
|
||||
|
||||
recipe.save()
|
||||
|
||||
|
||||
@login_required
|
||||
def get_external_file_link(request, recipe_id):
|
||||
recipe = Recipe.objects.get(id=recipe_id)
|
||||
|
||||
if recipe.storage.method == Storage.DROPBOX: # TODO move to central location (as all provider related functions)
|
||||
if recipe.link == "":
|
||||
recipe.link = Dropbox.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
if recipe.storage.method == Storage.NEXTCLOUD:
|
||||
if recipe.link == "":
|
||||
recipe.link = Nextcloud.get_share_link(recipe) # TODO response validation
|
||||
recipe.save()
|
||||
if not recipe.link:
|
||||
update_recipe_links(recipe)
|
||||
|
||||
return HttpResponse(recipe.link)
|
||||
|
||||
|
||||
@login_required
|
||||
def get_recipe_file(request, recipe_id):
|
||||
recipe = Recipe.objects.get(id=recipe_id)
|
||||
if not recipe.cors_link:
|
||||
update_recipe_links(recipe)
|
||||
|
||||
return HttpResponse(get_recipe_provider(recipe).get_base64_file(recipe))
|
||||
|
||||
|
||||
@login_required
|
||||
def sync_all(request):
|
||||
monitors = Sync.objects.filter(active=True)
|
||||
@@ -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')
|
||||
|
||||
@@ -44,7 +44,7 @@ def batch_import(request):
|
||||
recipe.save()
|
||||
new_recipe.delete()
|
||||
|
||||
return redirect('list_import')
|
||||
return redirect('list_recipe_import')
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
137
cookbook/views/delete.py
Normal file
137
cookbook/views/delete.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
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, Ingredient
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
|
||||
class RecipeDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Recipe
|
||||
success_url = reverse_lazy('index')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe")
|
||||
return context
|
||||
|
||||
|
||||
def delete_recipe_source(request, pk):
|
||||
recipe = get_object_or_404(Recipe, pk=pk)
|
||||
|
||||
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)
|
||||
|
||||
recipe.storage = None
|
||||
recipe.file_path = ''
|
||||
recipe.file_uid = ''
|
||||
recipe.save()
|
||||
|
||||
return HttpResponseRedirect(reverse('edit_recipe', args=[recipe.pk]))
|
||||
|
||||
|
||||
class RecipeImportDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeImport
|
||||
success_url = reverse_lazy('list_recipe_import')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeImportDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Import")
|
||||
return context
|
||||
|
||||
|
||||
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(SyncDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Monitor")
|
||||
return context
|
||||
|
||||
|
||||
class KeywordDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Keyword
|
||||
success_url = reverse_lazy('list_keyword')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(KeywordDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Keyword")
|
||||
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
|
||||
success_url = reverse_lazy('list_storage')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(StorageDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Storage Backend")
|
||||
return context
|
||||
|
||||
|
||||
class CommentDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Comment
|
||||
success_url = reverse_lazy('index')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CommentDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Comment")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeBook
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe Book")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookEntryDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeBookEntry
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Bookmarks")
|
||||
return context
|
||||
|
||||
|
||||
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
|
||||
@@ -1,21 +1,23 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
import simplejson
|
||||
import simplejson as json
|
||||
from PIL import Image
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.files import File
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, get_object_or_404, render
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import UpdateView, DeleteView
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, InternalRecipeForm, CommentForm
|
||||
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredients, RecipeBook, \
|
||||
RecipeBookEntry
|
||||
from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, InternalRecipeForm, CommentForm, \
|
||||
MealPlanForm, UnitMergeForm, IngredientMergeForm, IngredientForm
|
||||
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeIngredient, RecipeBook, \
|
||||
MealPlan, Unit, Ingredient
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
@@ -35,15 +37,19 @@ def convert_recipe(request, pk):
|
||||
if not recipe.internal:
|
||||
recipe.internal = True
|
||||
recipe.save()
|
||||
return HttpResponseRedirect(reverse('edit_internal_recipe', args=[pk]))
|
||||
|
||||
return HttpResponseRedirect(reverse('edit_internal_recipe', args=[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']
|
||||
@@ -62,41 +68,64 @@ def internal_recipe_update(request, pk):
|
||||
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'JPEG', quality=70)
|
||||
recipe.image = File(im_io, name=(str(recipe.pk) + '.jpeg'))
|
||||
img.save(im_io, 'PNG', quality=70)
|
||||
recipe.image = File(im_io, name=(str(recipe.pk) + '.png'))
|
||||
elif 'image' in form.changed_data and form.cleaned_data['image'] is False:
|
||||
recipe.image = None
|
||||
|
||||
recipe.save()
|
||||
|
||||
form_ingredients = json.loads(form.data['ingredients'])
|
||||
RecipeIngredients.objects.filter(recipe=recipe_instance).delete()
|
||||
try:
|
||||
form_ingredients = json.loads(form.cleaned_data['ingredients'])
|
||||
except simplejson.errors.JSONDecodeError:
|
||||
form_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 '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:
|
||||
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', 'note')
|
||||
|
||||
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):
|
||||
@@ -131,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']
|
||||
@@ -156,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:
|
||||
@@ -165,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):
|
||||
@@ -225,7 +268,23 @@ class RecipeBookUpdate(LoginRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
|
||||
class RecipeUpdate(LoginRequiredMixin, UpdateView):
|
||||
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 ExternalRecipeUpdate(LoginRequiredMixin, UpdateView):
|
||||
model = Recipe
|
||||
form_class = ExternalRecipeForm
|
||||
template_name = "generic/edit_template.html"
|
||||
@@ -235,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:
|
||||
@@ -262,115 +319,41 @@ class RecipeUpdate(LoginRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
|
||||
# Generic Delete views
|
||||
@login_required
|
||||
def edit_ingredients(request):
|
||||
if request.method == "POST":
|
||||
success = False
|
||||
units_form = UnitMergeForm(request.POST, prefix=UnitMergeForm.prefix)
|
||||
if units_form.is_valid():
|
||||
new_unit = units_form.cleaned_data['new_unit']
|
||||
old_unit = units_form.cleaned_data['old_unit']
|
||||
recipe_ingredients = RecipeIngredient.objects.filter(unit=old_unit).all()
|
||||
for i in recipe_ingredients:
|
||||
i.unit = new_unit
|
||||
i.save()
|
||||
|
||||
def delete_redirect(request, name, pk):
|
||||
return redirect(('delete_' + name), pk)
|
||||
old_unit.delete()
|
||||
success = True
|
||||
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
|
||||
|
||||
ingredients_form = IngredientMergeForm(request.POST, prefix=IngredientMergeForm.prefix)
|
||||
if ingredients_form.is_valid():
|
||||
new_ingredient = ingredients_form.cleaned_data['new_ingredient']
|
||||
old_ingredient = ingredients_form.cleaned_data['old_ingredient']
|
||||
recipe_ingredients = RecipeIngredient.objects.filter(ingredient=old_ingredient).all()
|
||||
for i in recipe_ingredients:
|
||||
i.ingredient = new_ingredient
|
||||
i.save()
|
||||
|
||||
class RecipeDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Recipe
|
||||
success_url = reverse_lazy('index')
|
||||
old_ingredient.delete()
|
||||
success = True
|
||||
messages.add_message(request, messages.SUCCESS, _('Ingredients merged!'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe")
|
||||
return context
|
||||
if success:
|
||||
units_form = UnitMergeForm()
|
||||
ingredients_form = IngredientMergeForm()
|
||||
else:
|
||||
units_form = UnitMergeForm()
|
||||
ingredients_form = IngredientMergeForm()
|
||||
|
||||
|
||||
class RecipeSourceDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Recipe
|
||||
success_url = reverse_lazy('index')
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if self.object.storage.method == Storage.DROPBOX:
|
||||
Dropbox.delete_file(self.object) # TODO central location to handle storage type switches
|
||||
if self.object.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.delete_file(self.object)
|
||||
|
||||
return super(RecipeSourceDelete, self).delete(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeSourceDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe")
|
||||
return context
|
||||
|
||||
|
||||
class ImportDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeImport
|
||||
success_url = reverse_lazy('list_import')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ImportDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Import")
|
||||
return context
|
||||
|
||||
|
||||
class MonitorDelete(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['title'] = _("Monitor")
|
||||
return context
|
||||
|
||||
|
||||
class KeywordDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Keyword
|
||||
success_url = reverse_lazy('list_keyword')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(KeywordDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Keyword")
|
||||
return context
|
||||
|
||||
|
||||
class StorageDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Storage
|
||||
success_url = reverse_lazy('list_storage')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(StorageDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Storage Backend")
|
||||
return context
|
||||
|
||||
|
||||
class CommentDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = Comment
|
||||
success_url = reverse_lazy('index')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CommentDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Comment")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeBook
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Recipe Book")
|
||||
return context
|
||||
|
||||
|
||||
class RecipeBookEntryDelete(LoginRequiredMixin, DeleteView):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = RecipeBookEntry
|
||||
success_url = reverse_lazy('view_books')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Bookmarks")
|
||||
return context
|
||||
return render(request, 'forms/ingredients.html', {'units_form': units_form, 'ingredients_form': ingredients_form})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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):
|
||||
@@ -74,6 +76,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()
|
||||
|
||||
@@ -82,7 +85,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:
|
||||
@@ -109,3 +112,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
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import copy
|
||||
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.http import HttpResponseRedirect
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
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 +30,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 +73,125 @@ def books(request):
|
||||
book_list.append({'book': b, 'recipes': RecipeBookEntry.objects.filter(book=b).all()})
|
||||
|
||||
return render(request, 'books.html', {'book_list': book_list})
|
||||
|
||||
|
||||
def get_start_end_from_week(p_year, p_week):
|
||||
first_day_of_week = datetime.strptime(f'{p_year}-W{int(p_week) - 1}-1', "%Y-W%W-%w").date()
|
||||
last_day_of_week = first_day_of_week + timedelta(days=6.9)
|
||||
return first_day_of_week, last_day_of_week
|
||||
|
||||
|
||||
def get_days_from_week(start, end):
|
||||
delta = end - start
|
||||
days = []
|
||||
for i in range(delta.days + 1):
|
||||
days.append(start + timedelta(days=i))
|
||||
return days
|
||||
|
||||
|
||||
@login_required()
|
||||
def meal_plan(request):
|
||||
js_week = datetime.now().strftime("%Y-W%V")
|
||||
if request.method == "POST":
|
||||
js_week = request.POST['week']
|
||||
|
||||
year, week = js_week.split('-')
|
||||
first_day, last_day = get_start_end_from_week(year, week.replace('W', ''))
|
||||
|
||||
surrounding_weeks = {'next': (last_day + timedelta(3)).strftime("%Y-W%V"), 'prev': (first_day - timedelta(3)).strftime("%Y-W%V")}
|
||||
|
||||
days = get_days_from_week(first_day, last_day)
|
||||
days_dict = {}
|
||||
for d in days:
|
||||
days_dict[d] = []
|
||||
|
||||
plan = {}
|
||||
for t in MealPlan.MEAL_TYPES:
|
||||
plan[t[0]] = {'type_name': t[1], 'days': copy.deepcopy(days_dict)}
|
||||
|
||||
for d in days:
|
||||
plan_day = MealPlan.objects.filter(date=d).all()
|
||||
for p in plan_day:
|
||||
plan[p.meal]['days'][d].append(p)
|
||||
|
||||
return render(request, 'meal_plan.html', {'js_week': js_week, 'plan': plan, 'days': days, 'surrounding_weeks': surrounding_weeks})
|
||||
|
||||
|
||||
@login_required
|
||||
def shopping_list(request):
|
||||
markdown_format = True
|
||||
|
||||
if request.method == "POST":
|
||||
form = ShoppingForm(request.POST)
|
||||
if form.is_valid():
|
||||
recipes = form.cleaned_data['recipe']
|
||||
markdown_format = form.cleaned_data['markdown_format']
|
||||
else:
|
||||
recipes = []
|
||||
else:
|
||||
raw_list = request.GET.getlist('r')
|
||||
|
||||
recipes = []
|
||||
for r in raw_list:
|
||||
if re.match(r'^([1-9])+$', r):
|
||||
if Recipe.objects.filter(pk=int(r)).exists():
|
||||
recipes.append(int(r))
|
||||
|
||||
form = ShoppingForm(initial={'recipe': recipes})
|
||||
|
||||
ingredients = []
|
||||
|
||||
for r in recipes:
|
||||
for ri in RecipeIngredient.objects.filter(recipe=r).all():
|
||||
index = None
|
||||
for x, ig in enumerate(ingredients):
|
||||
if ri.ingredient == ig.ingredient and ri.unit == ig.unit:
|
||||
index = x
|
||||
|
||||
if index:
|
||||
ingredients[index].amount = ingredients[index].amount + ri.amount
|
||||
else:
|
||||
ingredients.append(ri)
|
||||
|
||||
return render(request, 'shopping_list.html', {'ingredients': ingredients, 'recipes': recipes, 'form': form, 'markdown_format': markdown_format})
|
||||
|
||||
|
||||
@login_required
|
||||
def settings(request):
|
||||
try:
|
||||
up = request.user.userpreference
|
||||
except UserPreference.DoesNotExist:
|
||||
up = None
|
||||
|
||||
user_name_form = UserNameForm(instance=request.user)
|
||||
password_form = PasswordChangeForm(request.user)
|
||||
|
||||
if request.method == "POST":
|
||||
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:
|
||||
preference_form = UserPreferenceForm(instance=up)
|
||||
else:
|
||||
preference_form = UserPreferenceForm()
|
||||
|
||||
return render(request, 'settings.html', {'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: "postgres:11-alpine"
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- default
|
||||
|
||||
web_recipes:
|
||||
build: .
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
command: "gunicorn --bind 0.0.0.0:8080 recipes.wsgi"
|
||||
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
|
||||
networks:
|
||||
- default
|
||||
- nginx-proxy
|
||||
|
||||
networks:
|
||||
default:
|
||||
nginx-proxy:
|
||||
external:
|
||||
name: nginx-proxy
|
||||
11
docs/docker/nginx-proxy/README.md
Normal file
11
docs/docker/nginx-proxy/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
This is a docker compose example when using [jwilder's nginx reverse proxy](https://github.com/jwilder/docker-gen)
|
||||
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=
|
||||
```
|
||||
43
docs/docker/nginx-proxy/docker-compose.yml
Normal file
43
docs/docker/nginx-proxy/docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- default
|
||||
|
||||
web_recipes:
|
||||
image: vabene1111/recipes
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./staticfiles:/opt/recipes/staticfiles
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
- db_recipes
|
||||
networks:
|
||||
- default
|
||||
|
||||
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
|
||||
5
docs/docker/plain/README.md
Normal file
5
docs/docker/plain/README.md
Normal 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.
|
||||
29
docs/docker/plain/docker-compose.yml
Normal file
29
docs/docker/plain/docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- default
|
||||
|
||||
web_recipes:
|
||||
image: vabene1111/recipes
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./staticfiles:/opt/recipes/staticfiles
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
ports:
|
||||
- 80:8080
|
||||
depends_on:
|
||||
- db_recipes
|
||||
networks:
|
||||
- default
|
||||
|
||||
networks:
|
||||
default:
|
||||
60
docs/docker/traefik/README.md
Normal file
60
docs/docker/traefik/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
Please refer to the traefik documentation on how to setup a docker service in traefik. Since treafik can be a little
|
||||
confusing at times, the following are examples of my traefik configuration.
|
||||
|
||||
|
||||
You need to create a network called `traefik` using `docker network create traefik`.
|
||||
## docker-compose.yml
|
||||
|
||||
```
|
||||
version: "3.3"
|
||||
|
||||
services:
|
||||
|
||||
traefik:
|
||||
image: "traefik:v2.1"
|
||||
container_name: "traefik"
|
||||
ports:
|
||||
- "443:443"
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "./letsencrypt:/letsencrypt"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "./config:/etc/traefik/"
|
||||
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: traefik
|
||||
```
|
||||
|
||||
## traefik.toml
|
||||
Place this in a directory called `config` as this is mounted into the traefik container (see docer compose).
|
||||
**Change the email address accordingly**.
|
||||
```
|
||||
[api]
|
||||
insecure=true
|
||||
|
||||
[providers.docker]
|
||||
endpoint = "unix:///var/run/docker.sock"
|
||||
exposedByDefault = false
|
||||
network = "traefik"
|
||||
|
||||
#[log]
|
||||
# level = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web_secure]
|
||||
address = ":443"
|
||||
|
||||
[certificatesResolvers.le_resolver.acme]
|
||||
|
||||
email = "you_email@mail.com"
|
||||
storage = "/letsencrypt/acme.json"
|
||||
|
||||
tlsChallenge=true
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user