Compare commits

...

121 Commits
0.8.2 ... 0.9.0

Author SHA1 Message Date
vabene1111
0368630c92 added more view tests 2020-06-17 15:25:16 +02:00
vabene1111
02c1ba0c71 structured and extended test 2020-06-17 15:06:29 +02:00
vabene1111
83cc8832cb improved duplicate testing code 2020-06-17 14:53:00 +02:00
vabene1111
14a5d43dc8 added complete test for user preference api 2020-06-17 14:33:55 +02:00
vabene1111
bea079dd05 further permission cleanup 2020-06-17 13:23:04 +02:00
vabene1111
df8170fa55 improved permission handlin 2020-06-17 13:18:28 +02:00
vabene1111
2904d5938d fixed sync create permission 2020-06-17 13:00:13 +02:00
vabene1111
18bfecb026 fixed DRF object permission 2020-06-17 12:12:42 +02:00
vabene1111
4ee5a4fd9f testing with user permission 2020-06-17 11:06:08 +02:00
vabene1111
bbaedfad33 documentation update api 2020-06-17 10:29:01 +02:00
vabene1111
de413f1473 custom browsable api header 2020-06-17 10:12:27 +02:00
vabene1111
d012385088 WIP api stuff 2020-06-16 20:32:41 +02:00
vabene1111
d18a330135 reset log level 2020-06-16 19:56:42 +02:00
vabene1111
a8f7ef8ef7 added missing dependency 2020-06-16 19:55:48 +02:00
vabene1111
d6972cacfb pyyaml dependency 2020-06-16 19:43:44 +02:00
vabene1111
3b21e44422 log level debug 2020-06-16 18:57:08 +02:00
vabene1111
1a78ca68bb added access logging 2020-06-16 18:50:24 +02:00
vabene1111
fac7b8cd5b added gunicorn error logging 2020-06-16 18:43:16 +02:00
vabene1111
8f780545a4 api browser link 2020-06-16 18:14:41 +02:00
vabene1111
218f7d92d7 fixed password settings autofocus 2020-06-16 18:04:42 +02:00
vabene1111
621bacff1c api token settings 2020-06-16 18:01:16 +02:00
vabene1111
9a849a979c ui secret key check 2020-06-16 17:42:53 +02:00
vabene1111
e8366e5280 added secret key system check 2020-06-16 17:42:43 +02:00
vabene1111
0a8270e7cf api documentation basics 2020-06-16 17:21:50 +02:00
vabene1111
aad8b220d1 added custom 404 page 2020-06-16 16:21:15 +02:00
vabene1111
d5e0a0a623 added comment setting to user pref admin table 2020-06-16 12:45:46 +02:00
vabene1111
8cd94d49e8 added comment system preference 2020-06-16 12:44:45 +02:00
vabene1111
08b805a547 fixed meal plan delete after create 2020-06-16 12:12:37 +02:00
vabene1111
ecac30136b no sharing external recipes + hide buttons for unauth 2020-06-16 12:07:18 +02:00
vabene1111
d694408af6 share link admin 2020-06-16 12:05:36 +02:00
vabene1111
6e284f6ae8 moved all recipe button 2020-06-16 12:03:42 +02:00
vabene1111
62c049a6de fixed unauthenticated recipe viewing 2020-06-16 11:49:02 +02:00
vabene1111
dee7249347 added sharing links and appropriate tests 2020-06-16 11:23:58 +02:00
vabene1111
17946c8dac markdown hint text 2020-06-16 10:37:11 +02:00
vabene1111
fa2326949e added back markdown support to meal plannin notes 2020-06-16 10:34:20 +02:00
vabene1111
8177d9ba0f added synology install instruction 2020-06-16 10:19:24 +02:00
vabene1111
8781a6572d set some norefer labels
probably not really needed for this case but satisfies the code scanning and does not hurt to have
2020-06-16 10:13:54 +02:00
vabene1111
c7d518071c Create codeql-analysis.yml 2020-06-16 09:56:57 +02:00
vabene1111
ea96c63289 stupid apple fix 2020-06-13 14:03:50 +02:00
vabene1111
8485a64726 viewlog/cooklog admin 2020-06-12 00:07:47 +02:00
vabene1111
e89bd44412 added to string to meal plan 2020-06-11 23:43:48 +02:00
vabene1111
2e0e48bb38 removed celery 2 2020-06-11 23:29:24 +02:00
vabene1111
040fa7c192 removed celery 2020-06-11 23:29:10 +02:00
vabene1111
7000097602 fixed share reset in meal plan move 2020-06-11 22:53:04 +02:00
vabene1111
3cbc6b5609 meal type admin 2020-06-11 22:48:26 +02:00
vabene1111
8ff52f542e ingredient rounding upgrades 2020-06-11 22:32:45 +02:00
vabene1111
8cc0fcaed2 fixed properly deep copy meal_type array 2020-06-11 20:22:04 +02:00
vabene1111
d4197773bf clean up logging 2020-06-11 19:04:15 +02:00
vabene1111
f530b3dc7a dependency cleanup 2020-06-11 19:00:17 +02:00
vabene1111
d1bf4d4bbb added ical export support 2020-06-11 18:38:56 +02:00
vabene1111
d584a3db25 added more help 2020-06-11 17:57:30 +02:00
vabene1111
aaa3737ae0 help and name based type indexing 2020-06-11 17:55:24 +02:00
vabene1111
5072859e57 re enabled basic sharing 2020-06-11 17:27:27 +02:00
vabene1111
ead3c6ef76 fixed sharing 2020-06-11 17:13:31 +02:00
vabene1111
d734cb813e plan type editing 2020-06-11 16:58:57 +02:00
vabene1111
8aa24d4771 per user meal types 2020-06-11 15:19:15 +02:00
vabene1111
c714ff4dbe name function cleanup 2020-06-11 11:42:38 +02:00
vabene1111
a32545c1dc proper mobile handle 2020-06-11 11:40:32 +02:00
vabene1111
dfe8e1fd42 layout improvements 2020-06-11 10:59:26 +02:00
vabene1111
729d573460 imrpoved detail view 2020-06-11 10:26:40 +02:00
vabene1111
8472b541aa correctly update plan 2020-06-11 01:14:09 +02:00
vabene1111
7e95e985ec proper async loading 2020-06-11 01:05:02 +02:00
vabene1111
f7e2aa9b83 week switching 2020-06-11 00:49:19 +02:00
vabene1111
99cf428470 plan entry detail modal 2020-06-10 23:33:18 +02:00
vabene1111
60a533f9c8 Merge branch 'develop' into feature/meal-planning 2020-06-10 16:01:21 +02:00
vabene1111
723416575f added alt text 2020-06-10 15:52:18 +02:00
vabene1111
e34889953a cleanup and allow loading of images 2020-06-10 15:51:47 +02:00
vabene1111
ec8a879efa improved pdf viewer 2020-06-10 15:40:24 +02:00
vabene1111
f44ebe0d05 Update issue templates 2020-06-10 12:09:37 +02:00
vabene1111
13c82cdbf9 made remote auth a config setting 2020-06-10 12:02:27 +02:00
vabene1111
e9a60ece81 Merge pull request #88 from cazier/develop
Adding Reverse Proxy Authentication
2020-06-10 11:56:16 +02:00
vabene1111
9575a86480 fixed ingredient deletion error 2020-06-10 11:52:24 +02:00
vabene1111
12036b9972 fixes protect error on storage monitor delete #102 2020-06-10 11:45:52 +02:00
vabene1111
bffb260dfa fixed delete source permission 2020-06-10 11:42:44 +02:00
vabene1111
9d460a3623 Update docker-publish-release.yml 2020-06-10 11:38:45 +02:00
vabene1111
56c5f28348 Update docker-publish-latest.yml 2020-06-10 11:37:49 +02:00
vabene1111
29d4dcb73d fixed yaml formatting 2020-06-10 11:36:45 +02:00
vabene1111
e60441ec99 add version script to tagged releases 2020-06-10 11:34:05 +02:00
vabene1111
608e024caa formatting + further testing 2020-06-10 11:19:28 +02:00
vabene1111
f596b12f12 testing build ref version writing 2020-06-10 11:08:59 +02:00
vabene1111
bdd41a5ba2 removed redundant quotes 2020-06-10 10:47:47 +02:00
vabene1111
80e566917e version writer script fixes 2020-06-10 10:33:19 +02:00
vabene1111
4294c132c6 version script path fix 2020-06-10 10:24:54 +02:00
vabene1111
da12daaf03 add version to action 2020-06-10 10:15:28 +02:00
vabene1111
150cf5ebac testing auto version 2020-06-10 10:13:11 +02:00
vabene1111
01b9022451 version script test 2020-06-10 10:07:33 +02:00
vabene1111
2bda5bbbf7 note text saving 2020-06-09 20:27:31 +02:00
vabene1111
a743a4e202 basics completly working 2020-06-09 20:15:06 +02:00
vabene1111
ffa7513f9e recipes and notes basically working 2020-06-09 17:26:50 +02:00
vabene1111
8cb6ed2f60 adding items to plan 2020-06-09 13:11:01 +02:00
vabene1111
1d6375bf84 fixed pdf js files missing 2020-06-09 12:28:48 +02:00
vabene1111
cfab867e0d removed no longer valid warning 2020-06-09 00:18:28 +02:00
vabene1111
29dd7c9ee7 add port mapping to plain example 2020-06-09 00:17:17 +02:00
vabene1111
72cb046e37 updated plain example 2020-06-09 00:16:04 +02:00
vabene1111
a555906a32 fixed user setup staff permission 2020-06-06 11:31:19 +02:00
vabene1111
2e255aba0d more plan basics working 2020-06-04 19:46:35 +02:00
vabene1111
a136a18a8e basic updating and loading working 2020-06-04 19:34:47 +02:00
Brendan Cazier
cdf4c0d1bb Re-adding import statement 2020-06-04 07:51:43 -05:00
vabene1111
3aedbfbdc3 Merge branch 'develop' into feature/meal-planning
# Conflicts:
#	requirements.txt
2020-06-04 13:12:52 +02:00
vabene1111
dc7c688ed5 show english first in language chooser 2020-06-04 09:58:18 +02:00
vabene1111
8aa8f15ad7 fixed path + added language option dutch (nl) 2020-06-04 09:57:28 +02:00
vabene1111
6b8a231eee Merge pull request #95 from D0T1X/develop
added base translation file for Dutch
2020-06-04 09:33:17 +02:00
D0T1X
b0e338f08a added base translation files 2020-06-04 07:25:31 +02:00
D0T1X
1a6e0c8706 Uploaded Django.po 2020-06-04 07:24:45 +02:00
D0T1X
c7ceae4350 Create temp 2020-06-04 07:24:24 +02:00
D0T1X
caefa6099b Delete temp 2020-06-04 07:21:33 +02:00
D0T1X
2e4645bb0c Creatend nl/MC_MESSAGES folde 2020-06-04 07:20:23 +02:00
vabene1111
6c966f8ef6 added versions to requirements.txt 2020-06-03 23:43:25 +02:00
vabene1111
7140cb0f93 basic system information page 2020-06-03 23:07:19 +02:00
vabene1111
b95c3f6685 working lists 2020-06-03 18:10:34 +02:00
Brendan Cazier
81cd551975 Added some basic documentation 2020-06-03 10:40:12 -05:00
Brendan Cazier
55777fd948 Added REMOTE_USER auth 2020-06-03 10:16:17 -05:00
vabene1111
3b5b505116 basic plan table drag and drop working 2020-06-03 12:05:43 +02:00
vabene1111
aea3f62f9b event parameter 2020-06-03 11:18:37 +02:00
vabene1111
201c493658 some working drag and drop stuff 2020-06-03 00:03:54 +02:00
vabene1111
8ffc6a0236 some basic drag and drop workin 2020-06-02 22:52:28 +02:00
vabene1111
84ad88b30b Merge branch 'develop' into feature/meal-planning 2020-06-02 22:32:30 +02:00
vabene1111
233f2a911f nothing working yet 2020-06-02 14:33:27 +02:00
vabene1111
989d8765d7 basic vue stuff working 2020-06-02 12:47:36 +02:00
vabene1111
2fcd207dc7 basic api 2020-06-02 12:04:14 +02:00
vabene1111
a3dc5f283a dynamic meal types 2020-06-02 11:46:16 +02:00
273 changed files with 115380 additions and 8065 deletions

View File

@@ -1,4 +1,5 @@
# only set this to true when testing/debugging
# when unset: 1 (true) - dont unset this, just for development
DEBUG=0
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
@@ -18,4 +19,17 @@ POSTGRES_DB=djangodb
# Serve mediafiles directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
# provided that include an additional nxginx container to handle media file serving.
# If you know what you are doing turn this back on (1) to serve media files using djangos serve() method.
# when unset: 1 (true) - this is temporary until an appropriate amount of time has passed for everyone to migrate
GUNICORN_MEDIA=0
# allow authentication via reverse proxy (e.g. authelia), leave of if you dont know what you are doing
# docs: https://github.com/vabene1111/recipes/tree/develop/docs/docker/nginx-proxy%20with%20proxy%20authentication
# when unset: 0 (false)
REVERSE_PROXY_AUTH=0
# the default value for the user preference 'comments' (enable/disable commenting system)
# when unset: 1 (true)
COMMENT_PREF_DEFAULT=1

15
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,15 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Bug description
A clear and concise description of what the bug is.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

27
.github/ISSUE_TEMPLATE/help-request.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Help request
about: If there is anything wrong with your setup
title: ''
labels: setup issue
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Issue
Please describe your problem here
### `.env`
Please include your `.env` config file (**make sure to remove/replace all secrets**)
```
env content
```
### `docker-compose.yml`
When running with docker compose please provide your `docker-compose.yml`
```
docker-compose.yml content
```

51
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 13 * * 2'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -10,6 +10,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = 'develop'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@2.13
with:

View File

@@ -1,14 +1,25 @@
name: publish latest image docker
on:
push:
branches:
- master
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:

View File

@@ -7,19 +7,27 @@ on:
jobs:
build:
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master#
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master#
- name: Get version number
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Update version file
uses: DamianReeves/write-file-action@v1.0
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,7 +1,9 @@
<component name="ProjectDictionaryState">
<dictionary name="vabene1111-PC">
<words>
<w>csrftoken</w>
<w>gunicorn</w>
<w>ical</w>
<w>traefik</w>
</words>
</dictionary>

2
.idea/recipes.iml generated
View File

@@ -27,8 +27,8 @@
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
<option value="$MODULE_DIR$/cookbook/templates" />
<option value="$MODULE_DIR$/recipes/templates" />
</list>
</option>
</component>

View File

@@ -8,4 +8,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :8080 --access-logfile - --error-logfile - recipes.wsgi
exec gunicorn -b :8080 --access-logfile - --error-logfile - --log-level INFO recipes.wsgi

View File

@@ -3,7 +3,7 @@ from .models import *
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style')
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style', 'comments')
@staticmethod
def name(obj):
@@ -94,7 +94,7 @@ admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin)
class MealPlanAdmin(admin.ModelAdmin):
list_display = ('user', 'recipe', 'meal', 'date')
list_display = ('user', 'recipe', 'meal_type', 'date')
@staticmethod
def user(obj):
@@ -102,3 +102,31 @@ class MealPlanAdmin(admin.ModelAdmin):
admin.site.register(MealPlan, MealPlanAdmin)
class MealTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'order')
admin.site.register(MealType, MealTypeAdmin)
class ViewLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at')
admin.site.register(ViewLog, ViewLogAdmin)
class CookLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
admin.site.register(CookLog, CookLogAdmin)
class ShareLinkAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'uuid', 'created_at',)
admin.site.register(ShareLink, ShareLinkAdmin)

View File

@@ -31,13 +31,15 @@ class UserPreferenceForm(forms.ModelForm):
class Meta:
model = UserPreference
fields = ('default_unit', 'theme', 'nav_color', 'default_page', 'show_recent', 'search_style', 'plan_share')
fields = ('default_unit', 'theme', 'nav_color', 'default_page', 'show_recent', 'search_style', 'plan_share', 'ingredient_decimals', 'comments')
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'plan_share': _('Default user to share newly created meal plan entries with.'),
'show_recent': _('Show recently viewed recipes on search page.'),
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.')
}
widgets = {
@@ -263,7 +265,7 @@ class MealPlanForm(forms.ModelForm):
class Meta:
model = MealPlan
fields = ('recipe', 'title', 'meal', 'note', 'date', 'shared')
fields = ('recipe', 'title', 'meal_type', 'note', 'date', 'shared')
help_texts = {
'shared': _('You can list default users to share recipes with in the settings.'),

View File

@@ -0,0 +1,10 @@
# Permission Config
from cookbook.helper.permission_helper import CustomIsUser, CustomIsOwner, CustomIsAdmin, CustomIsGuest
class PermissionConfig:
BOOKS = {
'owner': True,
'groups': ['user'],
'drf': [CustomIsUser],
}

View File

@@ -3,12 +3,25 @@ Source: https://djangosnippets.org/snippets/1703/
"""
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse
from rest_framework import permissions
from cookbook.models import ShareLink
# Helper Functions
def get_allowed_groups(groups_required):
"""
Builds a list of all groups equal or higher to the provided groups
This means checking for guest will also allow admins to access
:param groups_required: list or tuple of groups
:return: tuple of groups
"""
groups_allowed = tuple(groups_required)
if 'guest' in groups_required:
groups_allowed = groups_allowed + ('user', 'admin')
@@ -17,15 +30,70 @@ def get_allowed_groups(groups_required):
return groups_allowed
def has_group_permission(user, groups):
"""
Tests if a given user is member of a certain group (or any higher group)
Superusers always bypass permission checks. Unauthenticated users cant be member of any
group thus always return false.
:param user: django auth user object
:param groups: list or tuple of groups the user should be checked for
:return: True if user is in allowed groups, false otherwise
"""
if not user.is_authenticated:
return False
groups_allowed = get_allowed_groups(groups)
if user.is_authenticated:
if user.is_superuser | bool(user.groups.filter(name__in=groups_allowed)):
return True
return False
def is_object_owner(user, obj):
"""
Tests if a given user is the owner of a given object
test performed by checking user against the objects user and create_by field (if exists)
superusers bypass all checks, unauthenticated users cannot own anything
:param user django auth user object
:param obj any object that should be tested
:return: true if user is owner of object, false otherwise
"""
# TODO this could be improved/cleaned up by adding get_owner methods to all models that allow owner checks
if not user.is_authenticated:
return False
if user.is_superuser:
return True
if owner := getattr(obj, 'created_by', None):
return owner == user
if owner := getattr(obj, 'user', None):
return owner == user
return False
def share_link_valid(recipe, share):
"""
Verifies the validity of a share uuid
:param recipe: recipe object
:param share: share uuid
:return: true if a share link with the given recipe and uuid exists, false otherwise
"""
try:
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
except ValidationError:
return False
# Django Views
def group_required(*groups_required):
"""Requires user membership in at least one of the groups passed in."""
"""
Decorator that tests the requesting user to be member of at least one of the provided groups
or higher level groups
:param groups_required: list of required groups
:return: true if member of group, false otherwise
"""
def in_groups(u):
groups_allowed = get_allowed_groups(groups_required)
if u.is_authenticated:
if u.is_superuser | bool(u.groups.filter(name__in=groups_allowed)):
return True
return False
return has_group_permission(u, groups_required)
return user_passes_test(in_groups, login_url='index')
@@ -38,18 +106,10 @@ class GroupRequiredMixin(object):
groups_required = None
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('login'))
else:
if not request.user.is_superuser:
group_allowed = get_allowed_groups(self.groups_required)
user_groups = []
for group in request.user.groups.values_list('name', flat=True):
user_groups.append(group)
if len(set(user_groups).intersection(group_allowed)) <= 0:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
if not has_group_permission(request.user, self.groups_required):
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
@@ -60,9 +120,59 @@ class OwnerRequiredMixin(object):
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('login'))
else:
obj = self.get_object()
if not (obj.created_by == request.user or request.user.is_superuser):
if not is_object_owner(request.user, self.get_object()):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as its not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
# Django Rest Framework Permission classes
class CustomIsOwner(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies user has ownership over object
(either user or created_by or user is request user)
"""
message = _('You cannot interact with this object as its not owned by you!')
def has_permission(self, request, view):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return is_object_owner(request.user, obj)
class CustomIsGuest(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: guest
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
has_group_permission(request.user, ['guest'])
class CustomIsUser(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: user
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
has_group_permission(request.user, ['user'])
class CustomIsAdmin(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies the user is member of at least the group: admin
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view):
has_group_permission(request.user, ['admin'])

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.5 on 2020-06-02 09:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0045_userpreference_show_recent'),
]
operations = [
migrations.CreateModel(
name='MealType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('order', models.IntegerField(default=0)),
],
),
migrations.AddField(
model_name='mealplan',
name='meal_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.MealType'),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 3.0.5 on 2020-06-02 09:33
from django.db import migrations
from django.utils.translation import gettext as _
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
breakfast = MealType.objects.create(
name=_('Breakfast'),
order=0,
)
lunch = MealType.objects.create(
name=_('Lunch'),
order=0,
)
dinner = MealType.objects.create(
name=_('Dinner'),
order=0,
)
other = MealType.objects.create(
name=_('Other'),
order=0,
)
for m in MealPlan.objects.all():
if m.meal == 'BREAKFAST':
m.meal_type = breakfast
if m.meal == 'LUNCH':
m.meal_type = lunch
if m.meal == 'DINNER':
m.meal_type = dinner
if m.meal == 'OTHER':
m.meal_type = other
m.save()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0046_auto_20200602_1133'),
]
operations = [
migrations.RunPython(migrate_meal_types),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.5 on 2020-06-02 09:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0047_auto_20200602_1133'),
]
operations = [
migrations.RemoveField(
model_name='mealplan',
name='meal',
),
migrations.AlterField(
model_name='mealplan',
name='meal_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.MealType'),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-06-11 13:08
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', '0048_auto_20200602_1140'),
]
operations = [
migrations.AddField(
model_name='mealtype',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.0.7 on 2020-06-11 13:09
from django.db import migrations
from django.db.models import Q
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
for t in MealType.objects.filter(created_by=None).all():
user_type = MealType.objects.create(
name=t.name,
created_by=u,
)
MealPlan.objects.filter(Q(created_by=u) and Q(meal_type=t)).update(meal_type=user_type)
MealType.objects.filter(created_by=None).delete()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0049_mealtype_created_by'),
]
operations = [
migrations.RunPython(migrate_meal_types),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.0.7 on 2020-06-11 13:18
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', '0050_auto_20200611_1509'),
]
operations = [
migrations.AlterField(
model_name='mealtype',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0051_auto_20200611_1518'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='ingredient_decimals',
field=models.IntegerField(default=2),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-11 20:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0052_userpreference_ingredient_decimals'),
]
operations = [
migrations.AlterField(
model_name='recipeingredient',
name='amount',
field=models.DecimalField(decimal_places=16, default=0, max_digits=32),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.7 on 2020-06-16 08:57
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0053_auto_20200611_2217'),
]
operations = [
migrations.CreateModel(
name='ShareLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.UUID('dbbf5150-0795-4305-b9bd-3952dfa2264b'))),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
],
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-06-16 10:36
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0054_sharelink'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='comments',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='sharelink',
name='uuid',
field=models.UUIDField(default=uuid.UUID('a6e8f192-cc03-4dd4-8a03-58d7ab6b7df7')),
),
]

View File

@@ -1,11 +1,13 @@
import re
import uuid
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import User
from django.utils.translation import gettext as _
from django.db import models
from recipes.settings import COMMENT_PREF_DEFAULT
def get_user_name(self):
if not (name := f"{self.first_name} {self.last_name}") == " ":
@@ -63,6 +65,8 @@ class UserPreference(models.Model):
search_style = models.CharField(choices=SEARCH_STYLE, max_length=64, default=LARGE)
show_recent = models.BooleanField(default=True)
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
ingredient_decimals = models.IntegerField(default=2)
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
def __str__(self):
return str(self.user)
@@ -163,7 +167,7 @@ class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, on_delete=models.PROTECT)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT)
amount = models.DecimalField(default=0, decimal_places=2, max_digits=16)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
note = models.CharField(max_length=64, null=True, blank=True)
def __str__(self):
@@ -211,18 +215,21 @@ class RecipeBookEntry(models.Model):
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')),)
class MealType(models.Model):
name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.name
class MealPlan(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
title = models.CharField(max_length=64, blank=True, default='')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal = models.CharField(choices=MEAL_TYPES, max_length=128, default=BREAKFAST)
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
note = models.TextField(blank=True)
date = models.DateField()
@@ -232,8 +239,17 @@ class MealPlan(models.Model):
return str(self.recipe)
def get_meal_name(self):
meals = dict(self.MEAL_TYPES)
return meals.get(self.meal)
return self.meal_type.name
def __str__(self):
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
class ShareLink(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4())
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class CookLog(models.Model):

View File

@@ -1,4 +1,5 @@
import base64
import io
import os
from datetime import datetime
@@ -90,14 +91,14 @@ class Dropbox(Provider):
return response['url']
@staticmethod
def get_base64_file(recipe):
def get_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)
return io.BytesIO(response.content)
@staticmethod
def rename_file(recipe, new_name):

View File

@@ -1,4 +1,5 @@
import base64
import io
import os
import tempfile
from datetime import datetime
@@ -85,18 +86,17 @@ class Nextcloud(Provider):
return Nextcloud.create_share_link(recipe)
@staticmethod
def get_base64_file(recipe):
def get_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())
file = io.BytesIO(open(tmp_file_path, 'rb').read())
os.remove(tmp_file_path)
return val
return file
@staticmethod
def rename_file(recipe, new_name):

View File

@@ -12,7 +12,7 @@ class Provider:
raise Exception('Method not implemented in storage provider')
@staticmethod
def get_base64_file(recipe):
def get_file(recipe):
raise Exception('Method not implemented in storage provider')
@staticmethod

128
cookbook/serializer.py Normal file
View File

@@ -0,0 +1,128 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Ingredient, Unit, RecipeIngredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog
from cookbook.templatetags.custom_tags import markdown
class UserNameSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name')
class UserPreferenceSerializer(serializers.ModelSerializer):
class Meta:
model = UserPreference
fields = '__all__'
read_only_fields = ['user']
class StorageSerializer(serializers.ModelSerializer):
class Meta:
model = Storage
fields = '__all__'
class SyncSerializer(serializers.ModelSerializer):
class Meta:
model = Sync
fields = '__all__'
class SyncLogSerializer(serializers.ModelSerializer):
class Meta:
model = SyncLog
fields = '__all__'
class KeywordSerializer(serializers.ModelSerializer):
class Meta:
model = Keyword
fields = '__all__'
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = '__all__'
class UnitSerializer(serializers.ModelSerializer):
class Meta:
model = Unit
fields = '__all__'
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = '__all__'
class RecipeIngredientSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeIngredient
fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class RecipeImportSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeImport
fields = '__all__'
class RecipeBookSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeBook
fields = '__all__'
read_only_fields = ['id', 'created_by']
class RecipeBookEntrySerializer(serializers.ModelSerializer):
class Meta:
model = RecipeBookEntry
fields = '__all__'
class MealTypeSerializer(serializers.ModelSerializer):
class Meta:
model = MealType
fields = '__all__'
class MealPlanSerializer(serializers.ModelSerializer):
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
note_markdown = serializers.SerializerMethodField('get_note_markdown')
def get_note_markdown(self, obj):
return markdown(obj.note)
class Meta:
model = MealPlan
fields = ('id', 'title', 'recipe', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name')
class ShareLinkSerializer(serializers.ModelSerializer):
class Meta:
model = ShareLink
fields = '__all__'
class CookLogSerializer(serializers.ModelSerializer):
class Meta:
model = CookLog
fields = '__all__'
class ViewLogSerializer(serializers.ModelSerializer):
class Meta:
model = ViewLog
fields = '__all__'

2
cookbook/static/js/Sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
cookbook/static/js/js.cookie.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(e){var n;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var t=window.Cookies,o=window.Cookies=e();o.noConflict=function(){return window.Cookies=t,o}}}(function(){function f(){for(var e=0,n={};e<arguments.length;e++){var t=arguments[e];for(var o in t)n[o]=t[o]}return n}function a(e){return e.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent)}return function e(u){function c(){}function t(e,n,t){if("undefined"!=typeof document){"number"==typeof(t=f({path:"/"},c.defaults,t)).expires&&(t.expires=new Date(1*new Date+864e5*t.expires)),t.expires=t.expires?t.expires.toUTCString():"";try{var o=JSON.stringify(n);/^[\{\[]/.test(o)&&(n=o)}catch(e){}n=u.write?u.write(n,e):encodeURIComponent(String(n)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),e=encodeURIComponent(String(e)).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[\(\)]/g,escape);var r="";for(var i in t)t[i]&&(r+="; "+i,!0!==t[i]&&(r+="="+t[i].split(";")[0]));return document.cookie=e+"="+n+r}}function n(e,n){if("undefined"!=typeof document){for(var t={},o=document.cookie?document.cookie.split("; "):[],r=0;r<o.length;r++){var i=o[r].split("="),c=i.slice(1).join("=");n||'"'!==c.charAt(0)||(c=c.slice(1,-1));try{var f=a(i[0]);if(c=(u.read||u)(c,f)||a(c),n)try{c=JSON.parse(c)}catch(e){}if(t[f]=c,e===f)break}catch(e){}}return e?t[e]:t}}return c.set=t,c.get=function(e){return n(e,!1)},c.getJSON=function(e){return n(e,!0)},c.remove=function(e,n){t(e,"",f(n,{expires:-1}))},c.defaults={},c.withConverter=e,c}(function(){})});

View File

@@ -0,0 +1 @@
{"version":3,"sources":["js.cookie.js"],"names":["factory","registeredInModuleLoader","define","amd","exports","module","OldCookies","window","Cookies","api","noConflict","extend","i","result","arguments","length","attributes","key","decode","s","replace","decodeURIComponent","init","converter","set","value","document","path","defaults","expires","Date","toUTCString","JSON","stringify","test","e","write","encodeURIComponent","String","escape","stringifiedAttributes","attributeName","split","cookie","get","json","jar","cookies","parts","slice","join","charAt","name","read","parse","getJSON","remove","withConverter"],"mappings":"CAOE,SAAUA,GACX,IAAIC,EASJ,GARsB,mBAAXC,QAAyBA,OAAOC,MAC1CD,OAAOF,GACPC,GAA2B,GAEL,iBAAZG,UACVC,OAAOD,QAAUJ,IACjBC,GAA2B,IAEvBA,EAA0B,CAC9B,IAAIK,EAAaC,OAAOC,QACpBC,EAAMF,OAAOC,QAAUR,IAC3BS,EAAIC,WAAa,WAEhB,OADAH,OAAOC,QAAUF,EACVG,IAfT,CAkBC,WACD,SAASE,IAGR,IAFA,IAAIC,EAAI,EACJC,EAAS,GACND,EAAIE,UAAUC,OAAQH,IAAK,CACjC,IAAII,EAAaF,UAAWF,GAC5B,IAAK,IAAIK,KAAOD,EACfH,EAAOI,GAAOD,EAAWC,GAG3B,OAAOJ,EAGR,SAASK,EAAQC,GAChB,OAAOA,EAAEC,QAAQ,mBAAoBC,oBA0HtC,OAvHA,SAASC,EAAMC,GACd,SAASd,KAET,SAASe,EAAKP,EAAKQ,EAAOT,GACzB,GAAwB,oBAAbU,SAAX,CAQkC,iBAJlCV,EAAaL,EAAO,CACnBgB,KAAM,KACJlB,EAAImB,SAAUZ,IAEKa,UACrBb,EAAWa,QAAU,IAAIC,KAAkB,EAAb,IAAIA,KAAkC,MAArBd,EAAWa,UAI3Db,EAAWa,QAAUb,EAAWa,QAAUb,EAAWa,QAAQE,cAAgB,GAE7E,IACC,IAAIlB,EAASmB,KAAKC,UAAUR,GACxB,UAAUS,KAAKrB,KAClBY,EAAQZ,GAER,MAAOsB,IAETV,EAAQF,EAAUa,MACjBb,EAAUa,MAAMX,EAAOR,GACvBoB,mBAAmBC,OAAOb,IACxBL,QAAQ,4DAA6DC,oBAExEJ,EAAMoB,mBAAmBC,OAAOrB,IAC9BG,QAAQ,2BAA4BC,oBACpCD,QAAQ,UAAWmB,QAErB,IAAIC,EAAwB,GAC5B,IAAK,IAAIC,KAAiBzB,EACpBA,EAAWyB,KAGhBD,GAAyB,KAAOC,GACE,IAA9BzB,EAAWyB,KAWfD,GAAyB,IAAMxB,EAAWyB,GAAeC,MAAM,KAAK,KAGrE,OAAQhB,SAASiB,OAAS1B,EAAM,IAAMQ,EAAQe,GAG/C,SAASI,EAAK3B,EAAK4B,GAClB,GAAwB,oBAAbnB,SAAX,CAUA,IANA,IAAIoB,EAAM,GAGNC,EAAUrB,SAASiB,OAASjB,SAASiB,OAAOD,MAAM,MAAQ,GAC1D9B,EAAI,EAEDA,EAAImC,EAAQhC,OAAQH,IAAK,CAC/B,IAAIoC,EAAQD,EAAQnC,GAAG8B,MAAM,KACzBC,EAASK,EAAMC,MAAM,GAAGC,KAAK,KAE5BL,GAA6B,MAArBF,EAAOQ,OAAO,KAC1BR,EAASA,EAAOM,MAAM,GAAI,IAG3B,IACC,IAAIG,EAAOlC,EAAO8B,EAAM,IAIxB,GAHAL,GAAUpB,EAAU8B,MAAQ9B,GAAWoB,EAAQS,IAC9ClC,EAAOyB,GAEJE,EACH,IACCF,EAASX,KAAKsB,MAAMX,GACnB,MAAOR,IAKV,GAFAW,EAAIM,GAAQT,EAER1B,IAAQmC,EACX,MAEA,MAAOjB,KAGV,OAAOlB,EAAM6B,EAAI7B,GAAO6B,GAoBzB,OAjBArC,EAAIe,IAAMA,EACVf,EAAImC,IAAM,SAAU3B,GACnB,OAAO2B,EAAI3B,GAAK,IAEjBR,EAAI8C,QAAU,SAAUtC,GACvB,OAAO2B,EAAI3B,GAAK,IAEjBR,EAAI+C,OAAS,SAAUvC,EAAKD,GAC3BQ,EAAIP,EAAK,GAAIN,EAAOK,EAAY,CAC/Ba,SAAU,MAIZpB,EAAImB,SAAW,GAEfnB,EAAIgD,cAAgBnC,EAEbb,EAGDa,CAAK"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11965
cookbook/static/js/vue.js Normal file

File diff suppressed because it is too large Load Diff

6
cookbook/static/js/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 40 40">
<path
d="M 1.5006714,23.536225 6.8925879,18.994244 14.585721,26.037937 34.019683,4.5410479 38.499329,9.2235032 14.585721,35.458952 z"
id="path4"
style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:1.25402856;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
height="40"
width="40"
viewBox="0 0 40 40">
<rect
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
width="33.76017"
height="33.76017"
x="3.119915"
y="3.119915" />
<path
d="m 20.677967,8.54499 c -7.342801,0 -13.295293,4.954293 -13.295293,11.065751 0,2.088793 0.3647173,3.484376 1.575539,5.150563 L 6.0267418,31.45501 13.560595,29.011117 c 2.221262,1.387962 4.125932,1.665377 7.117372,1.665377 7.3428,0 13.295291,-4.954295 13.295291,-11.065753 0,-6.111458 -5.952491,-11.065751 -13.295291,-11.065751 z"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.93031836;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 40 40">
<g
transform="translate(0,-60)"
id="layer1">
<rect
width="36.460953"
height="34.805603"
x="1.7695236"
y="62.597198"
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.30826771;stroke-opacity:1" />
<g
transform="matrix(0.88763677,0,0,0.88763677,2.2472646,8.9890584)">
<path
d="M 20,64.526342 C 11.454135,64.526342 4.5263421,71.454135 4.5263421,80 4.5263421,88.545865 11.454135,95.473658 20,95.473658 28.545865,95.473658 35.473658,88.545865 35.473658,80 35.473658,71.454135 28.545865,64.526342 20,64.526342 z m -0.408738,9.488564 c 3.527079,0 6.393832,2.84061 6.393832,6.335441 0,3.494831 -2.866753,6.335441 -6.393832,6.335441 -3.527079,0 -6.393832,-2.84061 -6.393832,-6.335441 0,-3.494831 2.866753,-6.335441 6.393832,-6.335441 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.02768445;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 7.2335209,71.819938 4.9702591,4.161823 c -1.679956,2.581606 -1.443939,6.069592 0.159325,8.677725 l -5.1263071,3.424463 c 0.67516,1.231452 3.0166401,3.547686 4.2331971,4.194757 l 3.907728,-4.567277 c 2.541952,1.45975 5.730694,1.392161 8.438683,-0.12614 l 3.469517,6.108336 c 1.129779,-0.44367 4.742234,-3.449633 5.416358,-5.003859 l -5.46204,-4.415541 c 1.44319,-2.424098 1.651175,-5.267515 0.557303,-7.748623 l 5.903195,-3.833951 C 33.14257,71.704996 30.616217,69.018606 29.02952,67.99296 l -4.118813,4.981678 C 22.411934,71.205099 18.900853,70.937534 16.041319,72.32916 l -3.595408,-5.322091 c -1.345962,0.579488 -4.1293881,2.921233 -5.2123901,4.812869 z m 8.1010311,3.426672 c 2.75284,-2.446266 6.769149,-2.144694 9.048998,0.420874 2.279848,2.56557 2.113919,6.596919 -0.638924,9.043185 -2.752841,2.446267 -6.775754,2.13726 -9.055604,-0.428308 -2.279851,-2.565568 -2.107313,-6.589485 0.64553,-9.035751 z"
style="fill:#000000;fill-opacity:1;stroke:none" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 64 64">
<path
d="M 32.003143,1.4044602 57.432701,62.632577 6.5672991,62.627924 z"
style="fill:#ffff00;fill-opacity:0.94117647;fill-rule:nonzero;stroke:#000000;stroke-width:1.00493038;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 64 64">
<path
d="M 25.470843,9.4933766 C 25.30219,12.141818 30.139101,14.445969 34.704831,13.529144 40.62635,12.541995 41.398833,7.3856498 35.97505,5.777863 31.400921,4.1549155 25.157674,6.5445892 25.470843,9.4933766 z M 4.5246282,17.652051 C 4.068249,11.832873 9.2742983,5.9270407 18.437379,3.0977088 29.751911,-0.87185184 45.495663,1.4008022 53.603953,7.1104009 c 9.275765,6.1889221 7.158128,16.2079421 -3.171076,21.5939521 -1.784316,1.635815 -6.380222,1.21421 -7.068351,3.186186 -1.04003,0.972427 -1.288046,2.050158 -1.232864,3.168203 1.015111,2.000108 -3.831548,1.633216 -3.270553,3.759574 0.589477,5.264544 -0.179276,10.53738 -0.362842,15.806257 -0.492006,2.184998 1.163456,4.574232 -0.734888,6.610642 -2.482919,2.325184 -7.30604,2.189143 -9.193497,-0.274767 -2.733688,-1.740626 -8.254447,-3.615254 -6.104247,-6.339626 3.468112,-1.708686 -2.116197,-3.449897 0.431242,-5.080274 5.058402,-1.39256 -2.393215,-2.304318 -0.146889,-4.334645 3.069198,-0.977415 2.056986,-2.518352 -0.219121,-3.540397 1.876567,-1.807151 1.484149,-4.868919 -2.565455,-5.942205 0.150866,-1.805474 2.905737,-4.136876 -1.679967,-5.20493 C 10.260902,27.882167 4.6872697,22.95045 4.5245945,17.652051 z"
id="path604"
style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:1.72665179;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 64 64">
<path
d="M 32.003143,10.913072 57.432701,53.086929 6.567299,53.083723 z"
id="path2985"
style="fill:#ffff00;fill-opacity:0.94117647;fill-rule:nonzero;stroke:#000000;stroke-width:0.83403099;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</svg>

After

Width:  |  Height:  |  Size: 426 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 40 40">
</svg>

After

Width:  |  Height:  |  Size: 158 B

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 40 40">
<rect
width="36.075428"
height="31.096582"
x="1.962286"
y="4.4517088"
id="rect4"
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.23004246;stroke-opacity:1" />
<rect
width="27.96859"
height="1.5012145"
x="6.0157046"
y="10.285"
id="rect6"
style="fill:#000000;fill-opacity:1;stroke:none" />
<rect
width="27.96859"
height="0.85783684"
x="6.0157056"
y="23.21689"
id="rect8"
style="fill:#000000;fill-opacity:1;stroke:none" />
<rect
width="27.96859"
height="0.85783684"
x="5.8130345"
y="28.964394"
id="rect10"
style="fill:#000000;fill-opacity:1;stroke:none" />
<rect
width="27.96859"
height="0.85783684"
x="6.0157046"
y="17.426493"
id="rect12"
style="fill:#000000;fill-opacity:1;stroke:none" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 40 40">
<rect
width="33.76017"
height="33.76017"
x="3.119915"
y="3.119915"
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 17.692678,34.50206 0,-16.182224 c -1.930515,-0.103225 -3.455824,-0.730383 -4.57593,-1.881473 -1.12011,-1.151067 -1.680164,-2.619596 -1.680164,-4.405591 0,-1.992435 0.621995,-3.5796849 1.865988,-4.7617553 1.243989,-1.1820288 3.06352,-1.7730536 5.458598,-1.7730764 l 9.802246,0 0,2.6789711 -2.229895,0 0,26.3251486 -2.632515,0 0,-26.3251486 -3.45324,0 0,26.3251486 z"
style="font-size:29.42051125px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.07795751;stroke-opacity:1;font-family:Arial;-inkscape-font-specification:Arial" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Some files were not shown because too many files have changed in this diff Show More