mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-27 20:18:58 -05:00
Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c22fb0ef4 | ||
|
|
f869fc85ae | ||
|
|
f435110810 | ||
|
|
36a0693b49 | ||
|
|
32a565c1e0 | ||
|
|
f3fd087b81 | ||
|
|
8e8d25071c | ||
|
|
129cc76624 | ||
|
|
0e48ee60cb | ||
|
|
a784421b5c | ||
|
|
314d8d41d1 | ||
|
|
02fd73f43e | ||
|
|
f12c47603d | ||
|
|
e341b96249 | ||
|
|
7ed5606e9d | ||
|
|
f3fffc1a3b | ||
|
|
c5a3e22542 | ||
|
|
c6990ef2d8 | ||
|
|
12a438752b | ||
|
|
8642298eda | ||
|
|
6260aba668 | ||
|
|
c56489cfa0 | ||
|
|
33b84c3d6a | ||
|
|
45454eb27b | ||
|
|
f628823ab2 | ||
|
|
781cbc16f7 | ||
|
|
28e6c9d922 | ||
|
|
d96bac47c4 | ||
|
|
1dafeb4db9 | ||
|
|
c02cdf6e68 | ||
|
|
3628835fd4 | ||
|
|
747b5937eb | ||
|
|
2fde8f9b52 | ||
|
|
7c06da89f0 | ||
|
|
2b17b12252 | ||
|
|
07afa7957a | ||
|
|
f0488c93d2 | ||
|
|
86a3d87276 | ||
|
|
fd14d79c12 | ||
|
|
4e852374cc | ||
|
|
898b1699b7 | ||
|
|
e92dbcd63e | ||
|
|
0c28e7e1b4 | ||
|
|
f8c1411e4d | ||
|
|
f1a194e166 | ||
|
|
f374e0cb27 | ||
|
|
6f7a41f3b8 | ||
|
|
a562b571b5 | ||
|
|
d71420484c | ||
|
|
bc7758e233 | ||
|
|
0f5185c677 | ||
|
|
1e6096d5c8 | ||
|
|
bc2a092e79 | ||
|
|
9809f58c28 | ||
|
|
589302a87d | ||
|
|
173eaf44a0 | ||
|
|
576c62b8a1 | ||
|
|
da57e656eb | ||
|
|
b42e6ac0f6 | ||
|
|
cebcf266fc | ||
|
|
d5ebb0047d | ||
|
|
6d553035db | ||
|
|
0f57dc9c8a | ||
|
|
31a4bc7747 | ||
|
|
0eed67b5aa | ||
|
|
3dbf40ff44 | ||
|
|
16459c1ec1 | ||
|
|
ba575ff79b | ||
|
|
2f9e407f49 | ||
|
|
1779c1ac14 | ||
|
|
ca1c353575 | ||
|
|
cac643266b | ||
|
|
5518199f64 | ||
|
|
a508fa81c0 | ||
|
|
c5bec8b69e | ||
|
|
c4905d39c1 | ||
|
|
5275e5eba7 | ||
|
|
272341f1dc | ||
|
|
78885987f0 | ||
|
|
cdbcc971b1 | ||
|
|
5378b4c577 | ||
|
|
269593cd98 | ||
|
|
82c83f4b8d | ||
|
|
42885cefc9 | ||
|
|
54a5009f98 | ||
|
|
7c49164387 | ||
|
|
1b8d4e4494 | ||
|
|
932211e7f6 | ||
|
|
9658993163 | ||
|
|
13e3c98fac | ||
|
|
9a9644dc6c | ||
|
|
cc43b2a9b0 | ||
|
|
91c0dbd8d2 | ||
|
|
3271ec6867 | ||
|
|
d39b779db9 | ||
|
|
536b0bad20 | ||
|
|
5c2020b8dd | ||
|
|
5851d061a2 | ||
|
|
a8bcc1457d | ||
|
|
6ba9cb8b55 | ||
|
|
8e53cce3b2 | ||
|
|
7f3a4ada75 | ||
|
|
0163988593 | ||
|
|
bcf78aed0a | ||
|
|
a7c89cc32e | ||
|
|
e66502ee8f | ||
|
|
40a12f35d7 | ||
|
|
fd1a399d03 | ||
|
|
538e45d20c | ||
|
|
0304e2a1ed | ||
|
|
65245234d8 | ||
|
|
a88d7625dc | ||
|
|
1af06b6480 | ||
|
|
48270197fa | ||
|
|
86cea901b4 | ||
|
|
f5312496e3 | ||
|
|
7f57e7ab56 | ||
|
|
c8d8dd581e | ||
|
|
256c1a7d41 | ||
|
|
7aa71dc744 | ||
|
|
07c34ea7b5 | ||
|
|
2d2582b449 | ||
|
|
4f81cb10de | ||
|
|
0256864904 | ||
|
|
7725665aa4 | ||
|
|
74e3d09065 | ||
|
|
0522fa0236 | ||
|
|
38aeb285c5 | ||
|
|
a9a0716c45 | ||
|
|
afc31b313f | ||
|
|
151f43b0d5 | ||
|
|
a695261b9c | ||
|
|
1229a37d74 | ||
|
|
ef200a4283 | ||
|
|
018fcf27ea | ||
|
|
b3504699b1 | ||
|
|
cdf4476345 | ||
|
|
0edc9f48c9 | ||
|
|
41885f7d05 | ||
|
|
2b7769e92f | ||
|
|
6a47d56da4 | ||
|
|
d456d4ae64 | ||
|
|
7b55bcb045 | ||
|
|
bee1d717c5 | ||
|
|
c91fc096b3 | ||
|
|
2297210a3f | ||
|
|
d59f14001c | ||
|
|
5e5941397b | ||
|
|
3ca71c6847 | ||
|
|
2c381eb870 | ||
|
|
230a368d38 | ||
|
|
5db7267a7d | ||
|
|
528da111f9 | ||
|
|
926097a699 | ||
|
|
0b3e86f6fd | ||
|
|
24e4ea354d | ||
|
|
08bc87960b | ||
|
|
9751821f76 | ||
|
|
fa12d02a3d | ||
|
|
9856857c51 | ||
|
|
c52cd359a1 | ||
|
|
5e9ce955bc | ||
|
|
f7d85bb4b8 | ||
|
|
46db6d4186 | ||
|
|
6724328b51 | ||
|
|
0da39f2e1f | ||
|
|
99fbc5e97c | ||
|
|
a0b6261275 | ||
|
|
061aefd233 | ||
|
|
d4e332456b | ||
|
|
500bb3af72 | ||
|
|
888106bb6f | ||
|
|
8daa0ada9b | ||
|
|
98711619ff | ||
|
|
9eb17df575 | ||
|
|
c71a7dad24 | ||
|
|
b92e51c0c7 | ||
|
|
87e8268a43 | ||
|
|
5b79db0725 | ||
|
|
3aade540c1 | ||
|
|
74f155b6f5 | ||
|
|
2539d19ff4 | ||
|
|
166f4c5f6b | ||
|
|
ed313cbf9a | ||
|
|
b1db591e9f | ||
|
|
94c51f90cd | ||
|
|
3074d916dc | ||
|
|
bf467b1ec0 | ||
|
|
348c1c78f1 | ||
|
|
913e896906 | ||
|
|
a0a673a0c9 | ||
|
|
3d60379ed0 | ||
|
|
fd7e20a46b | ||
|
|
a970f0c00e | ||
|
|
297dd6244a | ||
|
|
c9fcbc9ff0 | ||
|
|
c83eb1a42b | ||
|
|
8181a6d416 | ||
|
|
4a8b50aeba | ||
|
|
388ef32475 | ||
|
|
bfe72210df | ||
|
|
02c5aed0a3 | ||
|
|
0ec29636b3 | ||
|
|
d00fa10b9f | ||
|
|
eba3bfa828 | ||
|
|
6e91f34779 |
@@ -57,7 +57,9 @@ GUNICORN_MEDIA=0
|
||||
# S3_ACCESS_KEY=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION_NAME= # default none, set your region might be required
|
||||
# S3_QUERYSTRING_AUTH=1 # default true, set to 0 to serve media from a public bucket without signed urls
|
||||
# S3_QUERYSTRING_EXPIRE=3600 # number of seconds querystring are valid for
|
||||
# S3_ENDPOINT_URL= # when using a custom endpoint like minio
|
||||
|
||||
# Email Settings, see https://docs.djangoproject.com/en/3.2/ref/settings/#email-host
|
||||
@@ -79,12 +81,27 @@ REVERSE_PROXY_AUTH=0
|
||||
# Default settings for spaces, apply per space and can be changed in the admin view
|
||||
# SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
|
||||
# SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
|
||||
# SPACE_DEFAULT_FILES=1 # 1=can upload files (images, etc.) NOT IMPLEMENTED YET
|
||||
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
|
||||
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
|
||||
|
||||
# allow people to create accounts on your application instance (without an invite link)
|
||||
# when unset: 0 (false)
|
||||
# ENABLE_SIGNUP=0
|
||||
|
||||
# If signup is enabled you might want to add a captcha to it to prevent spam
|
||||
# HCAPTCHA_SITEKEY=
|
||||
# HCAPTCHA_SECRET=
|
||||
|
||||
# if signup is enabled you might want to provide urls to data protection policies or terms and conditions
|
||||
# TERMS_URL=
|
||||
# PRIVACY_URL=
|
||||
# IMPRINT_URL=
|
||||
|
||||
# enable serving of prometheus metrics under the /metrics path
|
||||
# ATTENTION: view is not secured (as per the prometheus default way) so make sure to secure it
|
||||
# trough your web server (or leave it open of you dont care if the stats are exposed)
|
||||
# ENABLE_METRICS=0
|
||||
|
||||
# allows you to setup OAuth providers
|
||||
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
|
||||
# SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
|
||||
@@ -95,4 +112,9 @@ REVERSE_PROXY_AUTH=0
|
||||
# SOCIAL_DEFAULT_ACCESS = 1
|
||||
|
||||
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
|
||||
# SOCIAL_DEFAULT_GROUP=guest
|
||||
# SOCIAL_DEFAULT_GROUP=guest
|
||||
|
||||
# Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
|
||||
# when running under the same database
|
||||
# SESSION_COOKIE_DOMAIN=.example.com
|
||||
# SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain
|
||||
15
README.md
15
README.md
@@ -9,18 +9,17 @@
|
||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop" >
|
||||
<img src="https://img.shields.io/github/stars/vabene1111/recipes" >
|
||||
<img src="https://img.shields.io/github/forks/vabene1111/recipes" >
|
||||
<img src="https://img.shields.io/docker/pulls/vabene1111/recipes" >
|
||||
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/install/docker.html" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
|
||||
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
@@ -8,7 +8,7 @@ from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
|
||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
|
||||
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
|
||||
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
|
||||
ImportLog, TelegramBot, BookmarkletImport)
|
||||
ImportLog, TelegramBot, BookmarkletImport, UserFile)
|
||||
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
@@ -23,14 +23,20 @@ admin.site.unregister(Group)
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'message')
|
||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
|
||||
admin.site.register(Space, SpaceAdmin)
|
||||
|
||||
|
||||
class UserPreferenceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',)
|
||||
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',) # TODO add new fields
|
||||
search_fields = ('user__username', 'space__name')
|
||||
list_filter = ('theme', 'nav_color', 'default_page', 'search_style')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@staticmethod
|
||||
def name(obj):
|
||||
@@ -42,6 +48,7 @@ admin.site.register(UserPreference, UserPreferenceAdmin)
|
||||
|
||||
class StorageAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'method')
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
admin.site.register(Storage, StorageAdmin)
|
||||
@@ -49,6 +56,7 @@ admin.site.register(Storage, StorageAdmin)
|
||||
|
||||
class SyncAdmin(admin.ModelAdmin):
|
||||
list_display = ('storage', 'path', 'active', 'last_checked')
|
||||
search_fields = ('storage__name', 'path')
|
||||
|
||||
|
||||
admin.site.register(Sync, SyncAdmin)
|
||||
@@ -77,6 +85,7 @@ admin.site.register(Keyword)
|
||||
|
||||
class StepAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'type', 'order')
|
||||
search_fields = ('name', 'type')
|
||||
|
||||
|
||||
admin.site.register(Step, StepAdmin)
|
||||
@@ -84,6 +93,9 @@ admin.site.register(Step, StepAdmin)
|
||||
|
||||
class RecipeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'internal', 'created_by', 'storage')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
list_filter = ('internal',)
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@staticmethod
|
||||
def created_by(obj):
|
||||
@@ -98,6 +110,7 @@ admin.site.register(Food)
|
||||
|
||||
class IngredientAdmin(admin.ModelAdmin):
|
||||
list_display = ('food', 'amount', 'unit')
|
||||
search_fields = ('food__name', 'unit__name')
|
||||
|
||||
|
||||
admin.site.register(Ingredient, IngredientAdmin)
|
||||
@@ -105,6 +118,8 @@ admin.site.register(Ingredient, IngredientAdmin)
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_display = ('recipe', 'name', 'created_at')
|
||||
search_fields = ('text', 'user__username')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@staticmethod
|
||||
def name(obj):
|
||||
@@ -123,6 +138,7 @@ admin.site.register(RecipeImport, RecipeImportAdmin)
|
||||
|
||||
class RecipeBookAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'user_name')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
|
||||
@staticmethod
|
||||
def user_name(obj):
|
||||
@@ -152,6 +168,7 @@ admin.site.register(MealPlan, MealPlanAdmin)
|
||||
|
||||
class MealTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'order')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
|
||||
|
||||
admin.site.register(MealType, MealTypeAdmin)
|
||||
@@ -166,7 +183,7 @@ admin.site.register(ViewLog, ViewLogAdmin)
|
||||
|
||||
class InviteLinkAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'username', 'group', 'valid_until',
|
||||
'group', 'valid_until',
|
||||
'created_by', 'created_at', 'used_by'
|
||||
)
|
||||
|
||||
@@ -235,3 +252,10 @@ class BookmarkletImportAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
admin.site.register(BookmarkletImport, BookmarkletImportAdmin)
|
||||
|
||||
|
||||
class UserFileAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'file_size_kb', 'created_at',)
|
||||
|
||||
|
||||
admin.site.register(UserFile, UserFileAdmin)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scopes_disabled
|
||||
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
|
||||
from emoji_picker.widgets import EmojiPickerTextInput
|
||||
from hcaptcha.fields import hCaptchaField
|
||||
|
||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
|
||||
@@ -119,17 +121,20 @@ class ImportExportBase(forms.Form):
|
||||
SAFRON = 'SAFRON'
|
||||
CHEFTAP = 'CHEFTAP'
|
||||
PEPPERPLATE = 'PEPPERPLATE'
|
||||
RECIPEKEEPER = 'RECIPEKEEPER'
|
||||
RECETTETEK = 'RECETTETEK'
|
||||
RECIPESAGE = 'RECIPESAGE'
|
||||
DOMESTICA = 'DOMESTICA'
|
||||
MEALMASTER = 'MEALMASTER'
|
||||
REZKONV = 'REZKONV'
|
||||
OPENEATS = 'OPENEATS'
|
||||
|
||||
type = forms.ChoiceField(choices=(
|
||||
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
|
||||
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
|
||||
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
||||
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'),
|
||||
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||
|
||||
))
|
||||
|
||||
|
||||
@@ -411,19 +416,11 @@ class InviteLinkForm(forms.ModelForm):
|
||||
|
||||
return email
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data['username']
|
||||
with scopes_disabled():
|
||||
if username != '' and (User.objects.filter(username=username).exists() or InviteLink.objects.filter(username=username).exists()):
|
||||
raise ValidationError(_('Username already taken!'))
|
||||
return username
|
||||
|
||||
class Meta:
|
||||
model = InviteLink
|
||||
fields = ('username', 'email', 'group', 'valid_until', 'space')
|
||||
fields = ('email', 'group', 'valid_until', 'space')
|
||||
help_texts = {
|
||||
'username': _('A username is not required, if left blank the new user can choose one.'),
|
||||
'email': _('An email address is not required but if present the invite link will be send to the user.')
|
||||
'email': _('An email address is not required but if present the invite link will be send to the user.'),
|
||||
}
|
||||
field_classes = {
|
||||
'space': SafeModelChoiceField,
|
||||
@@ -447,6 +444,21 @@ class SpaceJoinForm(forms.Form):
|
||||
token = forms.CharField()
|
||||
|
||||
|
||||
class AllAuthSignupForm(forms.Form):
|
||||
captcha = hCaptchaField()
|
||||
terms = forms.BooleanField(label=_('Accept Terms and Privacy'))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AllAuthSignupForm, self).__init__(**kwargs)
|
||||
if settings.PRIVACY_URL == '' and settings.TERMS_URL == '':
|
||||
self.fields.pop('terms')
|
||||
if settings.HCAPTCHA_SECRET == '':
|
||||
self.fields.pop('captcha')
|
||||
|
||||
def signup(self, request, user):
|
||||
pass
|
||||
|
||||
|
||||
class UserCreateForm(forms.Form):
|
||||
name = forms.CharField(label='Username')
|
||||
password = forms.CharField(
|
||||
|
||||
@@ -7,6 +7,8 @@ from django.contrib import messages
|
||||
from django.core.cache import caches
|
||||
from gettext import gettext as _
|
||||
|
||||
from cookbook.models import InviteLink
|
||||
|
||||
|
||||
class AllAuthCustomAdapter(DefaultAccountAdapter):
|
||||
|
||||
@@ -14,7 +16,11 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
|
||||
"""
|
||||
Whether to allow sign ups.
|
||||
"""
|
||||
if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP:
|
||||
signup_token = False
|
||||
if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
|
||||
signup_token = True
|
||||
|
||||
if (request.resolver_match.view_name == 'account_signup' or request.resolver_match.view_name == 'socialaccount_signup') and not settings.ENABLE_SIGNUP and not signup_token:
|
||||
return False
|
||||
else:
|
||||
return super(AllAuthCustomAdapter, self).is_open_for_signup(request)
|
||||
|
||||
19
cookbook/helper/CustomStorageClass.py
Normal file
19
cookbook/helper/CustomStorageClass.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import hashlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from storages.backends.s3boto3 import S3Boto3Storage
|
||||
|
||||
|
||||
class CachedS3Boto3Storage(S3Boto3Storage):
|
||||
def url(self, name, **kwargs):
|
||||
key = hashlib.md5(f'recipes_media_urls_{name}'.encode('utf-8')).hexdigest()
|
||||
if result := cache.get(key):
|
||||
return result
|
||||
|
||||
result = super(CachedS3Boto3Storage, self).url(name, **kwargs)
|
||||
|
||||
timeout = int(settings.AWS_QUERYSTRING_EXPIRE * .95)
|
||||
cache.set(key, result, timeout)
|
||||
|
||||
return result
|
||||
13
cookbook/helper/context_processors.py
Normal file
13
cookbook/helper/context_processors.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def context_settings(request):
|
||||
return {
|
||||
'EMAIL_ENABLED': settings.EMAIL_HOST != '',
|
||||
'SIGNUP_ENABLED': settings.ENABLE_SIGNUP,
|
||||
'CAPTCHA_ENABLED': settings.HCAPTCHA_SITEKEY != '',
|
||||
'HOSTED': settings.HOSTED,
|
||||
'TERMS_URL': settings.TERMS_URL,
|
||||
'PRIVACY_URL': settings.PRIVACY_URL,
|
||||
'IMPRINT_URL': settings.IMPRINT_URL,
|
||||
}
|
||||
45
cookbook/helper/image_processing.py
Normal file
45
cookbook/helper/image_processing.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def rescale_image_jpeg(image_object, base_width=720):
|
||||
img = Image.open(image_object)
|
||||
icc_profile = img.info.get('icc_profile') # remember color profile to not mess up colors
|
||||
width_percent = (base_width / float(img.size[0]))
|
||||
height = int((float(img.size[1]) * float(width_percent)))
|
||||
|
||||
img = img.resize((base_width, height), Image.ANTIALIAS)
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, 'JPEG', quality=75, optimize=True, icc_profile=icc_profile)
|
||||
|
||||
return img_bytes
|
||||
|
||||
|
||||
def rescale_image_png(image_object, base_width=720):
|
||||
basewidth = 720
|
||||
wpercent = (basewidth / float(image_object.size[0]))
|
||||
hsize = int((float(image_object.size[1]) * float(wpercent)))
|
||||
img = image_object.resize((basewidth, hsize), Image.ANTIALIAS)
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'PNG', quality=70)
|
||||
return img
|
||||
|
||||
|
||||
def get_filetype(name):
|
||||
try:
|
||||
return os.path.splitext(name)[1]
|
||||
except:
|
||||
return '.jpeg'
|
||||
|
||||
|
||||
def handle_image(request, image_object, filetype='.jpeg'):
|
||||
if sys.getsizeof(image_object) / 8 > 500:
|
||||
if filetype == '.jpeg':
|
||||
return rescale_image_jpeg(image_object), filetype
|
||||
if filetype == '.png':
|
||||
return rescale_image_png(image_object), filetype
|
||||
return image_object, filetype
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
import string
|
||||
import unicodedata
|
||||
|
||||
@@ -22,20 +23,16 @@ def parse_fraction(x):
|
||||
def parse_amount(x):
|
||||
amount = 0
|
||||
unit = ''
|
||||
note = ''
|
||||
|
||||
did_check_frac = False
|
||||
end = 0
|
||||
while (
|
||||
end < len(x)
|
||||
and (
|
||||
x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
)
|
||||
)
|
||||
):
|
||||
while (end < len(x) and (x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
))):
|
||||
end += 1
|
||||
if end > 0:
|
||||
if "/" in x[:end]:
|
||||
@@ -55,7 +52,11 @@ def parse_amount(x):
|
||||
unit = x[end + 1:]
|
||||
except ValueError:
|
||||
unit = x[end:]
|
||||
return amount, unit
|
||||
|
||||
if unit.startswith('(') or unit.startswith('-'): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
|
||||
unit = ''
|
||||
note = x
|
||||
return amount, unit, note
|
||||
|
||||
|
||||
def parse_ingredient_with_comma(tokens):
|
||||
@@ -106,6 +107,13 @@ def parse(x):
|
||||
unit = ''
|
||||
ingredient = ''
|
||||
note = ''
|
||||
unit_note = ''
|
||||
|
||||
# if the string contains parenthesis early on remove it and place it at the end
|
||||
# because its likely some kind of note
|
||||
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
|
||||
match = re.search('\((.[^\(])+\)', x)
|
||||
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()]
|
||||
|
||||
tokens = x.split()
|
||||
if len(tokens) == 1:
|
||||
@@ -114,17 +122,17 @@ def parse(x):
|
||||
else:
|
||||
try:
|
||||
# try to parse first argument as amount
|
||||
amount, unit = parse_amount(tokens[0])
|
||||
amount, unit, unit_note = parse_amount(tokens[0])
|
||||
# only try to parse second argument as amount if there are at least
|
||||
# three arguments if it already has a unit there can't be
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
try:
|
||||
if not unit == '':
|
||||
# a unit is already found, no need to try the second argument for a fraction # noqa: E501
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½' # noqa: E501
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
amount += parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
@@ -142,7 +150,10 @@ def parse(x):
|
||||
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
|
||||
try:
|
||||
ingredient, note = parse_ingredient(tokens[2:])
|
||||
unit = tokens[1]
|
||||
if unit == '':
|
||||
unit = tokens[1]
|
||||
else:
|
||||
note = tokens[1]
|
||||
except ValueError:
|
||||
ingredient, note = parse_ingredient(tokens[1:])
|
||||
else:
|
||||
@@ -158,11 +169,16 @@ def parse(x):
|
||||
ingredient, note = parse_ingredient(tokens)
|
||||
except ValueError:
|
||||
ingredient = ' '.join(tokens[1:])
|
||||
|
||||
if unit_note not in note:
|
||||
note += ' ' + unit_note
|
||||
return amount, unit.strip(), ingredient.strip(), note.strip()
|
||||
|
||||
|
||||
# small utility functions to prevent emtpy unit/food creation
|
||||
def get_unit(unit, space):
|
||||
if not unit:
|
||||
return None
|
||||
if len(unit) > 0:
|
||||
u, created = Unit.objects.get_or_create(name=unit, space=space)
|
||||
return u
|
||||
@@ -170,6 +186,8 @@ def get_unit(unit, space):
|
||||
|
||||
|
||||
def get_food(food, space):
|
||||
if not food:
|
||||
return None
|
||||
if len(food) > 0:
|
||||
f, created = Food.objects.get_or_create(name=food, space=space)
|
||||
return f
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Source: https://djangosnippets.org/snippets/1703/
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.views.generic.detail import SingleObjectTemplateResponseMixin
|
||||
from django.views.generic.edit import ModelFormMixin
|
||||
|
||||
@@ -90,7 +92,18 @@ def share_link_valid(recipe, share):
|
||||
:return: true if a share link with the given recipe and uuid exists
|
||||
"""
|
||||
try:
|
||||
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
|
||||
CACHE_KEY = f'recipe_share_{recipe.pk}_{share}'
|
||||
if c := caches['default'].get(CACHE_KEY, False):
|
||||
return c
|
||||
|
||||
if link := ShareLink.objects.filter(recipe=recipe, uuid=share, abuse_blocked=False).first():
|
||||
if 0 < settings.SHARING_LIMIT < link.request_count:
|
||||
return False
|
||||
link.request_count += 1
|
||||
link.save()
|
||||
caches['default'].set(CACHE_KEY, True, timeout=3)
|
||||
return True
|
||||
return False
|
||||
except ValidationError:
|
||||
return False
|
||||
|
||||
@@ -121,15 +134,18 @@ class GroupRequiredMixin(object):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not has_group_permission(request.user, self.groups_required):
|
||||
if not request.user.is_authenticated:
|
||||
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You are not logged in and therefore cannot view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
try:
|
||||
obj = self.get_object()
|
||||
if obj.get_space() != request.space:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -141,17 +157,20 @@ class OwnerRequiredMixin(object):
|
||||
|
||||
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!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You are not logged in and therefore cannot view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
|
||||
else:
|
||||
if not is_object_owner(request.user, self.get_object()):
|
||||
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You cannot interact with this object as it is not owned by you!'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
try:
|
||||
obj = self.get_object()
|
||||
if obj.get_space() != request.space:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -21,17 +21,21 @@ def search_recipes(request, queryset, params):
|
||||
|
||||
search_internal = params.get('internal', None)
|
||||
search_random = params.get('random', False)
|
||||
search_new = params.get('new', False)
|
||||
search_last_viewed = int(params.get('last_viewed', 0))
|
||||
|
||||
if search_last_viewed > 0:
|
||||
last_viewed_recipes = ViewLog.objects.filter(created_by=request.user, space=request.space,
|
||||
created_at__gte=datetime.now() - timedelta(days=14)).order_by('pk').values_list('recipe__pk', flat=True).distinct()
|
||||
|
||||
return queryset.filter(pk__in=last_viewed_recipes[len(last_viewed_recipes)-search_last_viewed:])
|
||||
return queryset.filter(pk__in=last_viewed_recipes[len(last_viewed_recipes) - min(len(last_viewed_recipes), search_last_viewed):])
|
||||
|
||||
queryset = queryset.annotate(
|
||||
new_recipe=Case(When(created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
|
||||
default=Value(0), )).order_by('-new_recipe', 'name')
|
||||
if search_new == 'true':
|
||||
queryset = queryset.annotate(
|
||||
new_recipe=Case(When(created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
|
||||
default=Value(0), )).order_by('-new_recipe', 'name')
|
||||
else:
|
||||
queryset = queryset.order_by('name')
|
||||
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']:
|
||||
|
||||
@@ -101,22 +101,21 @@ def get_from_scraper(scrape, space):
|
||||
for x in scrape.ingredients():
|
||||
try:
|
||||
amount, unit, ingredient, note = parse_single_ingredient(x)
|
||||
if ingredient:
|
||||
ingredients.append(
|
||||
{
|
||||
'amount': amount,
|
||||
'unit': {
|
||||
'text': unit,
|
||||
'id': random.randrange(10000, 99999)
|
||||
},
|
||||
'ingredient': {
|
||||
'text': ingredient,
|
||||
'id': random.randrange(10000, 99999)
|
||||
},
|
||||
'note': note,
|
||||
'original': x
|
||||
}
|
||||
)
|
||||
ingredients.append(
|
||||
{
|
||||
'amount': amount,
|
||||
'unit': {
|
||||
'text': unit,
|
||||
'id': random.randrange(10000, 99999)
|
||||
},
|
||||
'ingredient': {
|
||||
'text': ingredient,
|
||||
'id': random.randrange(10000, 99999)
|
||||
},
|
||||
'note': note,
|
||||
'original': x
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
ingredients.append(
|
||||
{
|
||||
@@ -359,3 +358,11 @@ def normalize_string(string):
|
||||
unescaped_string = re.sub(r'\n\s*\n', '\n\n', unescaped_string)
|
||||
unescaped_string = unescaped_string.replace("\xa0", " ").replace("\t", " ").strip()
|
||||
return unescaped_string
|
||||
|
||||
|
||||
def iso_duration_to_minutes(string):
|
||||
match = re.match(
|
||||
r'P((?P<years>\d+)Y)?((?P<months>\d+)M)?((?P<weeks>\d+)W)?((?P<days>\d+)D)?T((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+)S)?',
|
||||
string
|
||||
).groupdict()
|
||||
return int(match['days'] or 0) * 24 * 60 + int(match['hours'] or 0) * 60 + int(match['minutes'] or 0)
|
||||
|
||||
@@ -16,7 +16,10 @@ class ScopeMiddleware:
|
||||
with scopes_disabled():
|
||||
return self.get_response(request)
|
||||
|
||||
if request.path.startswith('/signup/'):
|
||||
if request.path.startswith('/signup/') or request.path.startswith('/invite/'):
|
||||
return self.get_response(request)
|
||||
|
||||
if request.path.startswith('/accounts/'):
|
||||
return self.get_response(request)
|
||||
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -40,7 +40,7 @@ class Pepperplate(Integration):
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n'
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@@ -49,7 +49,7 @@ class Pepperplate(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class ChefTap(Integration):
|
||||
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
step = Step.objects.create(instruction='\n'.join(directions))
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
|
||||
|
||||
if source_url != '':
|
||||
step.instruction += '\n' + source_url
|
||||
@@ -50,7 +50,7 @@ class ChefTap(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
|
||||
@@ -54,7 +55,7 @@ class Chowdown(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions)
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@@ -62,7 +63,7 @@ class Chowdown(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
@@ -71,7 +72,7 @@ class Chowdown(Integration):
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^images/{image}$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
from io import BytesIO
|
||||
from re import match
|
||||
from zipfile import ZipFile
|
||||
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.serializer import RecipeExportSerializer
|
||||
|
||||
@@ -15,8 +17,9 @@ class Default(Integration):
|
||||
|
||||
recipe_string = recipe_zip.read('recipe.json').decode("utf-8")
|
||||
recipe = self.decode_recipe(recipe_string)
|
||||
if 'image.png' in recipe_zip.namelist():
|
||||
self.import_recipe_image(recipe, BytesIO(recipe_zip.read('image.png')))
|
||||
images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist()))
|
||||
if images:
|
||||
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
|
||||
return recipe
|
||||
|
||||
def decode_recipe(self, string):
|
||||
|
||||
@@ -28,7 +28,7 @@ class Domestica(Integration):
|
||||
recipe.save()
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=file['directions']
|
||||
instruction=file['directions'], space=self.request.space,
|
||||
)
|
||||
|
||||
if file['source'] != '':
|
||||
@@ -40,12 +40,12 @@ class Domestica(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
if file['image'] != '':
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))))
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))), filetype='.jpeg')
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import BytesIO, StringIO
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
@@ -11,6 +13,7 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scope
|
||||
|
||||
from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.models import Keyword, Recipe
|
||||
|
||||
|
||||
@@ -58,7 +61,7 @@ class Integration:
|
||||
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
|
||||
recipe_stream.close()
|
||||
try:
|
||||
recipe_zip_obj.writestr('image.png', r.image.file.read())
|
||||
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -104,26 +107,54 @@ class Integration:
|
||||
try:
|
||||
self.files = files
|
||||
for f in files:
|
||||
if '.zip' in f['name'] or '.paprikarecipes' in f['name']:
|
||||
if 'RecipeKeeper' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
file_list = []
|
||||
for z in import_zip.filelist:
|
||||
if self.import_file_name_filter(z):
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
file_list.append(z)
|
||||
il.total_recipes += len(file_list)
|
||||
|
||||
for z in file_list:
|
||||
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
|
||||
for d in data_list:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
import_zip.close()
|
||||
elif '.zip' in f['name'] or '.paprikarecipes' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
file_list = []
|
||||
for z in import_zip.filelist:
|
||||
if self.import_file_name_filter(z):
|
||||
file_list.append(z)
|
||||
il.total_recipes += len(file_list)
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
import_zip.close()
|
||||
elif '.json' in f['name'] or '.txt' in f['name']:
|
||||
data_list = self.split_recipe_file(f['file'])
|
||||
il.total_recipes += len(data_list)
|
||||
for d in data_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
elif '.rtk' in f['name']:
|
||||
@@ -131,12 +162,16 @@ class Integration:
|
||||
for z in import_zip.filelist:
|
||||
if self.import_file_name_filter(z):
|
||||
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
|
||||
il.total_recipes += len(data_list)
|
||||
|
||||
for d in data_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
import_zip.close()
|
||||
@@ -148,6 +183,9 @@ class Integration:
|
||||
except BadZipFile:
|
||||
il.msg += 'ERROR ' + _(
|
||||
'Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
|
||||
except:
|
||||
il.msg += 'ERROR ' + _(
|
||||
'An unexpected error occurred during the import. Please make sure you have uploaded a valid file.') + '\n'
|
||||
|
||||
if len(self.ignored_recipes) > 0:
|
||||
il.msg += '\n' + _(
|
||||
@@ -170,13 +208,14 @@ class Integration:
|
||||
self.ignored_recipes.append(recipe.name)
|
||||
|
||||
@staticmethod
|
||||
def import_recipe_image(recipe, image_file):
|
||||
def import_recipe_image(recipe, image_file, filetype='.jpeg'):
|
||||
"""
|
||||
Adds an image to a recipe naming it correctly
|
||||
:param recipe: Recipe object
|
||||
:param image_file: ByteIO stream containing the image
|
||||
:param filetype: type of file to write bytes to, default to .jpeg if unknown
|
||||
"""
|
||||
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}.png')
|
||||
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
|
||||
recipe.save()
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
@@ -11,40 +12,55 @@ from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
class Mealie(Integration):
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
return re.match(r'^recipes/([A-Za-z\d-])+.json$', zip_info_object.filename)
|
||||
return re.match(r'^recipes/([A-Za-z\d-])+/([A-Za-z\d-])+.json$', zip_info_object.filename)
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe_json = json.loads(file.getvalue().decode("utf-8"))
|
||||
|
||||
description = '' if len(recipe_json['description'].strip()) > 500 else recipe_json['description'].strip()
|
||||
|
||||
recipe = Recipe.objects.create(
|
||||
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
||||
name=recipe_json['name'].strip(), description=description,
|
||||
created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
# TODO parse times (given in PT2H3M )
|
||||
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipeInstructions']:
|
||||
for s in recipe_json['recipe_instructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text']
|
||||
instruction=s['text'], space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
|
||||
for ingredient in recipe_json['recipeIngredient']:
|
||||
amount, unit, ingredient, note = parse(ingredient)
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
))
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
|
||||
|
||||
for ingredient in recipe_json['recipe_ingredient']:
|
||||
try:
|
||||
if ingredient['food']:
|
||||
f = get_food(ingredient['food'], self.request.space)
|
||||
u = get_unit(ingredient['unit'], self.request.space)
|
||||
amount = ingredient['quantity']
|
||||
note = ingredient['note']
|
||||
else:
|
||||
amount, unit, ingredient, note = parse(ingredient['note'])
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
except:
|
||||
pass
|
||||
recipe.steps.add(step)
|
||||
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||
try:
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
|
||||
except:
|
||||
pass
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class MealMaster(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n'
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@@ -53,7 +53,7 @@ class MealMaster(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
@@ -16,8 +17,10 @@ class NextcloudCookbook(Integration):
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe_json = json.loads(file.getvalue().decode("utf-8"))
|
||||
|
||||
description = '' if len(recipe_json['description'].strip()) > 500 else recipe_json['description'].strip()
|
||||
|
||||
recipe = Recipe.objects.create(
|
||||
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
||||
name=recipe_json['name'].strip(), description=description,
|
||||
created_by=self.request.user, internal=True,
|
||||
servings=recipe_json['recipeYield'], space=self.request.space)
|
||||
|
||||
@@ -27,9 +30,12 @@ class NextcloudCookbook(Integration):
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipeInstructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s
|
||||
instruction=s, space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
|
||||
|
||||
ingredients_added = True
|
||||
|
||||
for ingredient in recipe_json['recipeIngredient']:
|
||||
@@ -37,7 +43,7 @@ class NextcloudCookbook(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
@@ -46,7 +52,7 @@ class NextcloudCookbook(Integration):
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
|
||||
|
||||
return recipe
|
||||
|
||||
|
||||
71
cookbook/integration/openeats.py
Normal file
71
cookbook/integration/openeats.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
|
||||
|
||||
class OpenEats(Integration):
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe = Recipe.objects.create(name=file['name'].strip(), created_by=self.request.user, internal=True,
|
||||
servings=file['servings'], space=self.request.space, waiting_time=file['cook_time'], working_time=file['prep_time'])
|
||||
|
||||
instructions = ''
|
||||
if file["info"] != '':
|
||||
instructions += file["info"]
|
||||
|
||||
if file["directions"] != '':
|
||||
instructions += file["directions"]
|
||||
|
||||
if file["source"] != '':
|
||||
instructions += file["source"]
|
||||
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
|
||||
for ingredient in file['ingredients']:
|
||||
f = get_food(ingredient['food'], self.request.space)
|
||||
u = get_unit(ingredient['unit'], self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=ingredient['amount'], space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
return recipe
|
||||
|
||||
def split_recipe_file(self, file):
|
||||
recipe_json = json.loads(file.read())
|
||||
recipe_dict = {}
|
||||
ingredient_group_dict = {}
|
||||
|
||||
for o in recipe_json:
|
||||
if o['model'] == 'recipe.recipe':
|
||||
recipe_dict[o['pk']] = {
|
||||
'name': o['fields']['title'],
|
||||
'info': o['fields']['info'],
|
||||
'directions': o['fields']['directions'],
|
||||
'source': o['fields']['source'],
|
||||
'prep_time': o['fields']['prep_time'],
|
||||
'cook_time': o['fields']['cook_time'],
|
||||
'servings': o['fields']['servings'],
|
||||
'ingredients': [],
|
||||
}
|
||||
if o['model'] == 'ingredient.ingredientgroup':
|
||||
ingredient_group_dict[o['pk']] = o['fields']['recipe']
|
||||
|
||||
for o in recipe_json:
|
||||
if o['model'] == 'ingredient.ingredient':
|
||||
ingredient = {
|
||||
'food': o['fields']['title'],
|
||||
'unit': o['fields']['measurement'],
|
||||
'amount': round(o['fields']['numerator'] / o['fields']['denominator'], 2),
|
||||
}
|
||||
recipe_dict[ingredient_group_dict[o['fields']['ingredient_group']]]['ingredients'].append(ingredient)
|
||||
|
||||
return list(recipe_dict.values())
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
@@ -23,10 +23,10 @@ class Paprika(Integration):
|
||||
name=recipe_json['name'].strip(), created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
if 'description' in recipe_json:
|
||||
recipe.description = recipe_json['description'].strip()
|
||||
recipe.description = '' if len(recipe_json['description'].strip()) > 500 else recipe_json['description'].strip()
|
||||
|
||||
try:
|
||||
if re.match(r'([0-9])+\s(.)*', recipe_json['servings'] ):
|
||||
if re.match(r'([0-9])+\s(.)*', recipe_json['servings']):
|
||||
s = recipe_json['servings'].split(' ')
|
||||
recipe.servings = s[0]
|
||||
recipe.servings_text = s[1]
|
||||
@@ -55,9 +55,12 @@ class Paprika(Integration):
|
||||
pass
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=instructions
|
||||
instruction=instructions, space=self.request.space,
|
||||
)
|
||||
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
|
||||
|
||||
if 'categories' in recipe_json:
|
||||
for c in recipe_json['categories']:
|
||||
keyword, created = Keyword.objects.get_or_create(name=c.strip(), space=self.request.space)
|
||||
@@ -70,7 +73,7 @@ class Paprika(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -78,6 +81,6 @@ class Paprika(Integration):
|
||||
recipe.steps.add(step)
|
||||
|
||||
if recipe_json.get("photo_data", None):
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
|
||||
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -7,6 +7,7 @@ from zipfile import ZipFile
|
||||
import imghdr
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
|
||||
@@ -25,7 +26,7 @@ class RecetteTek(Integration):
|
||||
recipe_list = [r for r in recipe_json]
|
||||
|
||||
return recipe_list
|
||||
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
|
||||
# Create initial recipe with just a title and a decription
|
||||
@@ -44,7 +45,7 @@ class RecetteTek(Integration):
|
||||
if not instructions:
|
||||
instructions = ''
|
||||
|
||||
step = Step.objects.create(instruction=instructions)
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
|
||||
# Append the original import url to the step (if it exists)
|
||||
try:
|
||||
@@ -53,7 +54,7 @@ class RecetteTek(Integration):
|
||||
step.save()
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to import source url ', str(e))
|
||||
|
||||
|
||||
try:
|
||||
# Process the ingredients. Assumes 1 ingredient per line.
|
||||
for ingredient in file['ingredients'].split('\n'):
|
||||
@@ -62,7 +63,7 @@ class RecetteTek(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to parse recipe ingredients ', str(e))
|
||||
@@ -96,7 +97,7 @@ class RecetteTek(Integration):
|
||||
recipe.waiting_time = int(file['cookingTime'])
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to parse cooking time ', str(e))
|
||||
|
||||
|
||||
recipe.save()
|
||||
|
||||
# Import the recipe keywords
|
||||
@@ -110,20 +111,20 @@ class RecetteTek(Integration):
|
||||
pass
|
||||
|
||||
# TODO: Parse Nutritional Information
|
||||
|
||||
|
||||
# Import the original image from the zip file, if we cannot do that, attempt to download it again.
|
||||
try:
|
||||
if file['pictures'][0] !='':
|
||||
if file['pictures'][0] != '':
|
||||
image_file_name = file['pictures'][0].split('/')[-1]
|
||||
for f in self.files:
|
||||
if '.rtk' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)), filetype=get_filetype(image_file_name))
|
||||
else:
|
||||
if file['originalPicture'] != '':
|
||||
response=requests.get(file['originalPicture'])
|
||||
response = requests.get(file['originalPicture'])
|
||||
if imghdr.what(BytesIO(response.content)) != None:
|
||||
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||
self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
|
||||
else:
|
||||
raise Exception("Original image failed to download.")
|
||||
except Exception as e:
|
||||
|
||||
80
cookbook/integration/recipekeeper.py
Normal file
80
cookbook/integration/recipekeeper.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.helper.recipe_url_import import parse_servings, iso_duration_to_minutes
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
|
||||
|
||||
|
||||
class RecipeKeeper(Integration):
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
return re.match(r'^recipes.html$', zip_info_object.filename)
|
||||
|
||||
def split_recipe_file(self, file):
|
||||
recipe_html = BeautifulSoup(file, 'html.parser')
|
||||
return recipe_html.find_all('div', class_='recipe-details')
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
# 'file' comes is as a beautifulsoup object
|
||||
recipe = Recipe.objects.create(name=file.find("h2", {"itemprop": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
# add 'Courses' and 'Categories' as keywords
|
||||
for course in file.find_all("span", {"itemprop": "recipeCourse"}):
|
||||
keyword, created = Keyword.objects.get_or_create(name=course.text, space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
for category in file.find_all("meta", {"itemprop": "recipeCategory"}):
|
||||
keyword, created = Keyword.objects.get_or_create(name=category.get("content"), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
try:
|
||||
recipe.servings = parse_servings(file.find("span", {"itemprop": "recipeYield"}).text.strip())
|
||||
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
|
||||
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
|
||||
recipe.save()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
step = Step.objects.create(instruction='', space=self.request.space,)
|
||||
|
||||
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
|
||||
if ingredient.text == "":
|
||||
continue
|
||||
amount, unit, ingredient, note = parse(ingredient.text.strip())
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
|
||||
for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"):
|
||||
if s.text == "":
|
||||
continue
|
||||
step.instruction += s.text + ' \n'
|
||||
|
||||
if file.find("span", {"itemprop": "recipeSource"}).text != '':
|
||||
step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text
|
||||
step.save()
|
||||
source_url_added = True
|
||||
|
||||
recipe.steps.add(step)
|
||||
|
||||
# import the Primary recipe image that is stored in the Zip
|
||||
try:
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))), filetype='.jpeg')
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return recipe
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
@@ -36,7 +36,7 @@ class RecipeSage(Integration):
|
||||
ingredients_added = False
|
||||
for s in file['recipeInstructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text']
|
||||
instruction=s['text'], space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
@@ -46,7 +46,7 @@ class RecipeSage(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class RezKonv(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n'
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@@ -52,7 +52,7 @@ class RezKonv(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
@@ -43,14 +43,14 @@ class Safron(Integration):
|
||||
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
step = Step.objects.create(instruction='\n'.join(directions))
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
|
||||
|
||||
for ingredient in ingredients:
|
||||
amount, unit, ingredient, note = parse(ingredient)
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/nb_NO/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/nb_NO/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/sv/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/sv/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/zh_Hant/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/zh_Hant/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2303
cookbook/locale/zh_Hant/LC_MESSAGES/django.po
Normal file
2303
cookbook/locale/zh_Hant/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
18
cookbook/migrations/0125_space_demo.py
Normal file
18
cookbook/migrations/0125_space_demo.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-04 14:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0124_alter_userpreference_theme'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='demo',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0126_alter_userpreference_theme.py
Normal file
18
cookbook/migrations/0126_alter_userpreference_theme.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-05 15:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0125_space_demo'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='theme',
|
||||
field=models.CharField(choices=[('TANDOOR', 'Tandoor'), ('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero')], default='TANDOOR', max_length=128),
|
||||
),
|
||||
]
|
||||
17
cookbook/migrations/0127_remove_invitelink_username.py
Normal file
17
cookbook/migrations/0127_remove_invitelink_username.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-07 14:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0126_alter_userpreference_theme'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='invitelink',
|
||||
name='username',
|
||||
),
|
||||
]
|
||||
30
cookbook/migrations/0128_userfile.py
Normal file
30
cookbook/migrations/0128_userfile.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-08 10:23
|
||||
|
||||
import cookbook.models
|
||||
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', '0127_remove_invitelink_username'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserFile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('file', models.FileField(upload_to='files/')),
|
||||
('file_size_kb', models.IntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||
],
|
||||
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||
),
|
||||
]
|
||||
22
cookbook/migrations/0129_auto_20210608_1233.py
Normal file
22
cookbook/migrations/0129_auto_20210608_1233.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-08 10:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0128_userfile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='space',
|
||||
name='allow_files',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='max_file_storage_mb',
|
||||
field=models.IntegerField(default=0, help_text='Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0130_alter_userfile_file_size_kb.py
Normal file
18
cookbook/migrations/0130_alter_userfile_file_size_kb.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-08 10:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0129_auto_20210608_1233'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userfile',
|
||||
name='file_size_kb',
|
||||
field=models.IntegerField(blank=True, default=0),
|
||||
),
|
||||
]
|
||||
24
cookbook/migrations/0131_auto_20210608_1929.py
Normal file
24
cookbook/migrations/0131_auto_20210608_1929.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-08 17:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0130_alter_userfile_file_size_kb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='step',
|
||||
name='file',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.userfile'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='step',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('TEXT', 'Text'), ('TIME', 'Time'), ('FILE', 'File')], default='TEXT', max_length=16),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0132_sharelink_request_count.py
Normal file
18
cookbook/migrations/0132_sharelink_request_count.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-12 18:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0131_auto_20210608_1929'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sharelink',
|
||||
name='request_count',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0133_sharelink_abuse_blocked.py
Normal file
18
cookbook/migrations/0133_sharelink_abuse_blocked.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-12 18:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0132_sharelink_request_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sharelink',
|
||||
name='abuse_blocked',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0134_space_allow_sharing.py
Normal file
18
cookbook/migrations/0134_space_allow_sharing.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-15 19:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0133_sharelink_abuse_blocked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='allow_sharing',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
29
cookbook/migrations/0135_auto_20210615_2210.py
Normal file
29
cookbook/migrations/0135_auto_20210615_2210.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-15 20:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0134_space_allow_sharing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ingredient',
|
||||
name='space',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='nutritioninformation',
|
||||
name='space',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='step',
|
||||
name='space',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
|
||||
]
|
||||
23
cookbook/migrations/0136_auto_20210617_1343.py
Normal file
23
cookbook/migrations/0136_auto_20210617_1343.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-17 11:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0135_auto_20210615_2210'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='importlog',
|
||||
name='imported_recipes',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importlog',
|
||||
name='total_recipes',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
34
cookbook/migrations/0137_auto_20210617_1501.py
Normal file
34
cookbook/migrations/0137_auto_20210617_1501.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-17 13:01
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Subquery, OuterRef
|
||||
from django_scopes import scopes_disabled
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_spaces(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
Recipe = apps.get_model('cookbook', 'Recipe')
|
||||
Step = apps.get_model('cookbook', 'Step')
|
||||
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
||||
NutritionInformation = apps.get_model('cookbook', 'NutritionInformation')
|
||||
|
||||
Step.objects.filter(recipe__isnull=True).delete()
|
||||
Ingredient.objects.filter(step__recipe__isnull=True).delete()
|
||||
NutritionInformation.objects.filter(recipe__isnull=True).delete()
|
||||
|
||||
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
|
||||
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
|
||||
NutritionInformation.objects.update(space=Subquery(NutritionInformation.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0136_auto_20210617_1343'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_spaces),
|
||||
|
||||
]
|
||||
31
cookbook/migrations/0138_auto_20210617_1602.py
Normal file
31
cookbook/migrations/0138_auto_20210617_1602.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-17 14:02
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Subquery, OuterRef
|
||||
from django_scopes import scopes_disabled
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0137_auto_20210617_1501'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='space',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='nutritioninformation',
|
||||
name='space',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='step',
|
||||
name='space',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
]
|
||||
20
cookbook/migrations/0139_space_created_at.py
Normal file
20
cookbook/migrations/0139_space_created_at.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-22 16:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0138_auto_20210617_1602'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
20
cookbook/migrations/0140_userpreference_created_at.py
Normal file
20
cookbook/migrations/0140_userpreference_created_at.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-22 16:19
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0139_space_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
24
cookbook/migrations/0141_auto_20210713_1042.py
Normal file
24
cookbook/migrations/0141_auto_20210713_1042.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-13 08:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0140_userpreference_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='step',
|
||||
name='step_recipe',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.recipe'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='step',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('TEXT', 'Text'), ('TIME', 'Time'), ('FILE', 'File'), ('RECIPE', 'Recipe')], default='TEXT', max_length=16),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-29 14:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0141_auto_20210713_1042'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='search_style',
|
||||
field=models.CharField(choices=[('SMALL', 'Small'), ('LARGE', 'Large'), ('NEW', 'New')], default='NEW', max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,5 @@
|
||||
import operator
|
||||
import pathlib
|
||||
import re
|
||||
import uuid
|
||||
from datetime import date, timedelta
|
||||
@@ -5,10 +7,12 @@ from datetime import date, timedelta
|
||||
from annoying.fields import AutoOneToOneField
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django_prometheus.models import ExportModelOperationsMixin
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
||||
@@ -52,18 +56,23 @@ class PermissionModelMixin:
|
||||
|
||||
def get_space(self):
|
||||
p = '.'.join(self.get_space_key())
|
||||
if getattr(self, p, None):
|
||||
return getattr(self, p)
|
||||
raise NotImplementedError('get space for method not implemented and standard fields not available')
|
||||
try:
|
||||
if space := operator.attrgetter(p)(self):
|
||||
return space
|
||||
except AttributeError:
|
||||
raise NotImplementedError('get space for method not implemented and standard fields not available')
|
||||
|
||||
|
||||
class Space(models.Model):
|
||||
class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
name = models.CharField(max_length=128, default='Default')
|
||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
message = models.CharField(max_length=512, default='', blank=True)
|
||||
max_recipes = models.IntegerField(default=0)
|
||||
allow_files = models.BooleanField(default=True)
|
||||
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
|
||||
max_users = models.IntegerField(default=0)
|
||||
allow_sharing = models.BooleanField(default=True)
|
||||
demo = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -78,11 +87,11 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
TANDOOR = 'TANDOOR'
|
||||
|
||||
THEMES = (
|
||||
(TANDOOR, 'Tandoor'),
|
||||
(BOOTSTRAP, 'Bootstrap'),
|
||||
(DARKLY, 'Darkly'),
|
||||
(FLATLY, 'Flatly'),
|
||||
(SUPERHERO, 'Superhero'),
|
||||
(TANDOOR, 'Tandoor')
|
||||
)
|
||||
|
||||
# Nav colors
|
||||
@@ -124,7 +133,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')), (NEW, _('New')))
|
||||
|
||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
|
||||
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
||||
nav_color = models.CharField(
|
||||
choices=COLORS, max_length=128, default=PRIMARY
|
||||
)
|
||||
@@ -134,7 +143,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
choices=PAGES, max_length=64, default=SEARCH
|
||||
)
|
||||
search_style = models.CharField(
|
||||
choices=SEARCH_STYLE, max_length=64, default=LARGE
|
||||
choices=SEARCH_STYLE, max_length=64, default=NEW
|
||||
)
|
||||
show_recent = models.BooleanField(default=True)
|
||||
plan_share = models.ManyToManyField(
|
||||
@@ -145,6 +154,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
shopping_auto_sync = models.IntegerField(default=5)
|
||||
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
@@ -247,7 +257,7 @@ class SyncLog(models.Model, PermissionModelMixin):
|
||||
return f"{self.created_at}:{self.sync} - {self.status}"
|
||||
|
||||
|
||||
class Keyword(models.Model, PermissionModelMixin):
|
||||
class Keyword(ExportModelOperationsMixin('keyword'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=64)
|
||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
description = models.TextField(default="", blank=True)
|
||||
@@ -267,7 +277,7 @@ class Keyword(models.Model, PermissionModelMixin):
|
||||
unique_together = (('space', 'name'),)
|
||||
|
||||
|
||||
class Unit(models.Model, PermissionModelMixin):
|
||||
class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
@@ -281,7 +291,7 @@ class Unit(models.Model, PermissionModelMixin):
|
||||
unique_together = (('space', 'name'),)
|
||||
|
||||
|
||||
class Food(models.Model, PermissionModelMixin):
|
||||
class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
@@ -298,7 +308,7 @@ class Food(models.Model, PermissionModelMixin):
|
||||
unique_together = (('space', 'name'),)
|
||||
|
||||
|
||||
class Ingredient(models.Model, PermissionModelMixin):
|
||||
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
|
||||
food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
|
||||
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
@@ -307,14 +317,8 @@ class Ingredient(models.Model, PermissionModelMixin):
|
||||
no_amount = models.BooleanField(default=False)
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
objects = ScopedManager(space='step__recipe__space')
|
||||
|
||||
@staticmethod
|
||||
def get_space_key():
|
||||
return 'step', 'recipe', 'space'
|
||||
|
||||
def get_space(self):
|
||||
return self.step_set.first().recipe_set.first().space
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
|
||||
@@ -323,13 +327,15 @@ class Ingredient(models.Model, PermissionModelMixin):
|
||||
ordering = ['order', 'pk']
|
||||
|
||||
|
||||
class Step(models.Model, PermissionModelMixin):
|
||||
class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixin):
|
||||
TEXT = 'TEXT'
|
||||
TIME = 'TIME'
|
||||
FILE = 'FILE'
|
||||
RECIPE = 'RECIPE'
|
||||
|
||||
name = models.CharField(max_length=128, default='', blank=True)
|
||||
type = models.CharField(
|
||||
choices=((TEXT, _('Text')), (TIME, _('Time')),),
|
||||
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
|
||||
default=TEXT,
|
||||
max_length=16
|
||||
)
|
||||
@@ -337,16 +343,12 @@ class Step(models.Model, PermissionModelMixin):
|
||||
ingredients = models.ManyToManyField(Ingredient, blank=True)
|
||||
time = models.IntegerField(default=0, blank=True)
|
||||
order = models.IntegerField(default=0)
|
||||
file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
|
||||
show_as_header = models.BooleanField(default=True)
|
||||
step_recipe = models.ForeignKey('Recipe', default=None, blank=True, null=True, on_delete=models.PROTECT)
|
||||
|
||||
objects = ScopedManager(space='recipe__space')
|
||||
|
||||
@staticmethod
|
||||
def get_space_key():
|
||||
return 'recipe', 'space'
|
||||
|
||||
def get_space(self):
|
||||
return self.recipe_set.first().space
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def get_instruction_render(self):
|
||||
from cookbook.helper.template_helper import render_instructions
|
||||
@@ -367,20 +369,14 @@ class NutritionInformation(models.Model, PermissionModelMixin):
|
||||
max_length=512, default="", null=True, blank=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(space='recipe__space')
|
||||
|
||||
@staticmethod
|
||||
def get_space_key():
|
||||
return 'recipe', 'space'
|
||||
|
||||
def get_space(self):
|
||||
return self.recipe_set.first().space
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
return 'Nutrition'
|
||||
return f'Nutrition {self.pk}'
|
||||
|
||||
|
||||
class Recipe(models.Model, PermissionModelMixin):
|
||||
class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=512, blank=True, null=True)
|
||||
servings = models.IntegerField(default=1)
|
||||
@@ -412,7 +408,7 @@ class Recipe(models.Model, PermissionModelMixin):
|
||||
return self.name
|
||||
|
||||
|
||||
class Comment(models.Model, PermissionModelMixin):
|
||||
class Comment(ExportModelOperationsMixin('comment'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
text = models.TextField()
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
@@ -446,7 +442,7 @@ class RecipeImport(models.Model, PermissionModelMixin):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeBook(models.Model, PermissionModelMixin):
|
||||
class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.TextField(blank=True)
|
||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
@@ -460,7 +456,7 @@ class RecipeBook(models.Model, PermissionModelMixin):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeBookEntry(models.Model, PermissionModelMixin):
|
||||
class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE)
|
||||
|
||||
@@ -495,7 +491,7 @@ class MealType(models.Model, PermissionModelMixin):
|
||||
return self.name
|
||||
|
||||
|
||||
class MealPlan(models.Model, PermissionModelMixin):
|
||||
class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
|
||||
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
||||
title = models.CharField(max_length=64, blank=True, default='')
|
||||
@@ -520,7 +516,7 @@ class MealPlan(models.Model, PermissionModelMixin):
|
||||
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
|
||||
|
||||
|
||||
class ShoppingListRecipe(models.Model, PermissionModelMixin):
|
||||
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
|
||||
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
||||
|
||||
@@ -543,7 +539,7 @@ class ShoppingListRecipe(models.Model, PermissionModelMixin):
|
||||
return None
|
||||
|
||||
|
||||
class ShoppingListEntry(models.Model, PermissionModelMixin):
|
||||
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
|
||||
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
|
||||
food = models.ForeignKey(Food, on_delete=models.CASCADE)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
|
||||
@@ -573,7 +569,7 @@ class ShoppingListEntry(models.Model, PermissionModelMixin):
|
||||
return None
|
||||
|
||||
|
||||
class ShoppingList(models.Model, PermissionModelMixin):
|
||||
class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, PermissionModelMixin):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
note = models.TextField(blank=True, null=True)
|
||||
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
|
||||
@@ -591,9 +587,11 @@ class ShoppingList(models.Model, PermissionModelMixin):
|
||||
return f'Shopping list {self.id}'
|
||||
|
||||
|
||||
class ShareLink(models.Model, PermissionModelMixin):
|
||||
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
request_count = models.IntegerField(default=0)
|
||||
abuse_blocked = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -608,9 +606,8 @@ def default_valid_until():
|
||||
return date.today() + timedelta(days=14)
|
||||
|
||||
|
||||
class InviteLink(models.Model, PermissionModelMixin):
|
||||
class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, PermissionModelMixin):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
username = models.CharField(blank=True, max_length=64)
|
||||
email = models.EmailField(blank=True)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
valid_until = models.DateField(default=default_valid_until)
|
||||
@@ -641,7 +638,7 @@ class TelegramBot(models.Model, PermissionModelMixin):
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class CookLog(models.Model, PermissionModelMixin):
|
||||
class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
@@ -655,7 +652,7 @@ class CookLog(models.Model, PermissionModelMixin):
|
||||
return self.recipe.name
|
||||
|
||||
|
||||
class ViewLog(models.Model, PermissionModelMixin):
|
||||
class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -672,6 +669,10 @@ class ImportLog(models.Model, PermissionModelMixin):
|
||||
running = models.BooleanField(default=True)
|
||||
msg = models.TextField(default="")
|
||||
keyword = models.ForeignKey(Keyword, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
||||
total_recipes = models.IntegerField(default=0)
|
||||
imported_recipes = models.IntegerField(default=0)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
@@ -682,7 +683,7 @@ class ImportLog(models.Model, PermissionModelMixin):
|
||||
return f"{self.created_at}:{self.type}"
|
||||
|
||||
|
||||
class BookmarkletImport(models.Model, PermissionModelMixin):
|
||||
class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models.Model, PermissionModelMixin):
|
||||
html = models.TextField()
|
||||
url = models.CharField(max_length=256, null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -690,3 +691,20 @@ class BookmarkletImport(models.Model, PermissionModelMixin):
|
||||
|
||||
objects = ScopedManager(space='space')
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class UserFile(ExportModelOperationsMixin('user_files'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
file = models.FileField(upload_to='files/')
|
||||
file_size_kb = models.IntegerField(default=0, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
objects = ScopedManager(space='space')
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if hasattr(self.file, 'file') and isinstance(self.file.file, UploadedFile) or isinstance(self.file.file, InMemoryUploadedFile):
|
||||
self.file.name = f'{uuid.uuid4()}' + pathlib.Path(self.file.name).suffix
|
||||
self.file_size_kb = round(self.file.size / 1000)
|
||||
super(UserFile, self).save(*args, **kwargs)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from decimal import Decimal
|
||||
from gettext import gettext as _
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models import QuerySet, Sum, Avg
|
||||
from drf_writable_nested import (UniqueFieldsMixin,
|
||||
WritableNestedModelSerializer)
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError, NotAuthenticated, NotFound, ParseError
|
||||
from rest_framework.fields import ModelField
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
|
||||
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
||||
MealPlan, MealType, NutritionInformation, Recipe,
|
||||
@@ -15,7 +14,7 @@ from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
||||
ShareLink, ShoppingList, ShoppingListEntry,
|
||||
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
|
||||
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket,
|
||||
SupermarketCategoryRelation, ImportLog, BookmarkletImport)
|
||||
SupermarketCategoryRelation, ImportLog, BookmarkletImport, UserFile)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
|
||||
|
||||
@@ -103,6 +102,51 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class UserFileSerializer(serializers.ModelSerializer):
|
||||
|
||||
def check_file_limit(self, validated_data):
|
||||
if self.context['request'].space.max_file_storage_mb == -1:
|
||||
raise ValidationError(_('File uploads are not enabled for this Space.'))
|
||||
|
||||
try:
|
||||
current_file_size_mb = UserFile.objects.filter(space=self.context['request'].space).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
|
||||
except TypeError:
|
||||
current_file_size_mb = 0
|
||||
|
||||
if (validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5) > self.context['request'].space.max_file_storage_mb != 0:
|
||||
raise ValidationError(_('You have reached your file upload limit.'))
|
||||
|
||||
def create(self, validated_data):
|
||||
self.check_file_limit(validated_data)
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
self.check_file_limit(validated_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
class Meta:
|
||||
model = UserFile
|
||||
fields = ('name', 'file', 'file_size_kb', 'id',)
|
||||
read_only_fields = ('id', 'file_size_kb')
|
||||
extra_kwargs = {"file": {"required": False, }}
|
||||
|
||||
|
||||
class UserFileViewSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
raise ValidationError('Cannot create File over this view')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = UserFile
|
||||
fields = ('name', 'file', 'id',)
|
||||
read_only_fields = ('id', 'file')
|
||||
|
||||
|
||||
class StorageSerializer(SpacedModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -177,7 +221,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
obj, created = Unit.objects.get_or_create(name=validated_data['name'].strip(), space=self.context['request'].space)
|
||||
return obj
|
||||
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
return super(UnitSerializer, self).update(instance, validated_data)
|
||||
@@ -202,7 +246,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
|
||||
fields = ('id', 'name')
|
||||
|
||||
|
||||
class SupermarketCategoryRelationSerializer(SpacedModelSerializer):
|
||||
class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
|
||||
category = SupermarketCategorySerializer()
|
||||
|
||||
class Meta:
|
||||
@@ -239,6 +283,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
||||
unit = UnitSerializer(allow_null=True)
|
||||
amount = CustomDecimalField()
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = (
|
||||
@@ -251,6 +299,12 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
ingredients = IngredientSerializer(many=True)
|
||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
|
||||
file = UserFileViewSerializer(allow_null=True, required=False)
|
||||
step_recipe_data = serializers.SerializerMethodField('get_step_recipe_data')
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
def get_ingredients_vue(self, obj):
|
||||
return obj.get_instruction_render()
|
||||
@@ -258,22 +312,65 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
def get_ingredients_markdown(self, obj):
|
||||
return obj.get_instruction_render()
|
||||
|
||||
def get_step_recipe_data(self, obj):
|
||||
# check if root type is recipe to prevent infinite recursion
|
||||
# can be improved later to allow multi level embedding
|
||||
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
||||
return StepRecipeSerializer(obj.step_recipe).data
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = (
|
||||
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header'
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data'
|
||||
)
|
||||
|
||||
|
||||
class StepRecipeSerializer(WritableNestedModelSerializer):
|
||||
steps = StepSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = (
|
||||
'id', 'name', 'steps',
|
||||
)
|
||||
|
||||
|
||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = NutritionInformation
|
||||
fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source')
|
||||
|
||||
|
||||
class RecipeOverviewSerializer(WritableNestedModelSerializer):
|
||||
class RecipeBaseSerializer(WritableNestedModelSerializer):
|
||||
def get_recipe_rating(self, obj):
|
||||
try:
|
||||
rating = obj.cooklog_set.filter(created_by=self.context['request'].user, rating__gt=0).aggregate(Avg('rating'))
|
||||
if rating['rating__avg']:
|
||||
return rating['rating__avg']
|
||||
except TypeError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def get_recipe_last_cooked(self, obj):
|
||||
try:
|
||||
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
|
||||
if last:
|
||||
return last.created_at
|
||||
except TypeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||
keywords = KeywordLabelSerializer(many=True)
|
||||
rating = serializers.SerializerMethodField('get_recipe_rating')
|
||||
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
@@ -286,22 +383,24 @@ class RecipeOverviewSerializer(WritableNestedModelSerializer):
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'servings', 'file_path'
|
||||
'internal', 'servings', 'servings_text', 'rating', 'last_cooked',
|
||||
)
|
||||
read_only_fields = ['image', 'created_by', 'created_at']
|
||||
|
||||
|
||||
class RecipeSerializer(WritableNestedModelSerializer):
|
||||
class RecipeSerializer(RecipeBaseSerializer):
|
||||
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
|
||||
steps = StepSerializer(many=True)
|
||||
keywords = KeywordSerializer(many=True)
|
||||
rating = serializers.SerializerMethodField('get_recipe_rating')
|
||||
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'nutrition', 'servings', 'file_path', 'servings_text',
|
||||
'internal', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
|
||||
)
|
||||
read_only_fields = ['image', 'created_by', 'created_at']
|
||||
|
||||
@@ -342,6 +441,14 @@ class RecipeBookSerializer(SpacedModelSerializer):
|
||||
|
||||
|
||||
class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
book_content = serializers.SerializerMethodField(method_name='get_book_content', read_only=True)
|
||||
recipe_content = serializers.SerializerMethodField(method_name='get_recipe_content', read_only=True)
|
||||
|
||||
def get_book_content(self, obj):
|
||||
return RecipeBookSerializer(context={'request': self.context['request']}).to_representation(obj.book)
|
||||
|
||||
def get_recipe_content(self, obj):
|
||||
return RecipeOverviewSerializer(context={'request': self.context['request']}).to_representation(obj.recipe)
|
||||
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
@@ -351,7 +458,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RecipeBookEntry
|
||||
fields = ('id', 'book', 'recipe',)
|
||||
fields = ('id', 'book', 'book_content', 'recipe', 'recipe_content',)
|
||||
|
||||
|
||||
class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
@@ -475,7 +582,7 @@ class ImportLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ImportLog
|
||||
fields = ('id', 'type', 'msg', 'running', 'keyword', 'created_by', 'created_at')
|
||||
fields = ('id', 'type', 'msg', 'running', 'keyword', 'total_recipes', 'imported_recipes', 'created_by', 'created_at')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@@ -533,6 +640,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||
unit = UnitExportSerializer(allow_null=True)
|
||||
amount = CustomDecimalField()
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
|
||||
@@ -541,6 +652,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||
class StepExportSerializer(WritableNestedModelSerializer):
|
||||
ingredients = IngredientExportSerializer(many=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
|
||||
|
||||
BIN
cookbook/static/assets/brand_logo.png
Normal file
BIN
cookbook/static/assets/brand_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
115
cookbook/static/assets/spinner.svg
Normal file
115
cookbook/static/assets/spinner.svg
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="2000px"
|
||||
height="2000px"
|
||||
viewBox="0 0 2000 2000"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="spinner.svg"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
|
||||
<defs
|
||||
id="defs265" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.24748737"
|
||||
inkscape:cx="507.59315"
|
||||
inkscape:cy="671.7335"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata268">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path5640-9"
|
||||
d="m 1363.6895,468.03149 c -6.2646,0 -11.3386,5.07398 -11.3386,11.33858 0,6.25981 5.074,11.33859 11.3386,11.33859 6.2645,0 11.3386,-5.07878 11.3386,-11.33859 0,-6.2646 -5.0741,-11.33858 -11.3386,-11.33858 z m 63.425,-26.44248 42.4018,-162.5339 h -219.2126 c -12.5054,0 -22.6772,10.17173 -22.6772,22.67732 v 75.5904 c 0,12.50559 10.1718,22.67716 22.6772,22.67716 h 48.3118 l 3.5576,40.89453 c -17.0411,6.52913 -29.1923,22.91807 -29.1923,42.25508 v 22.67717 c 0,8.34795 6.7703,15.11811 15.1181,15.11811 H 1439.28 c 8.3483,0 15.1181,-6.77016 15.1181,-15.11811 V 483.1496 c 0,-18.61414 -11.2391,-34.5733 -27.2836,-41.56059 z m -176.8108,-56.70713 c -4.1621,0 -7.5591,-3.3874 -7.5591,-7.55905 v -75.5904 c 0,-4.17165 3.397,-7.55905 7.5591,-7.55905 h 39.1086 l 7.8898,90.7085 z m 199.644,-90.7085 -7.8897,30.23606 h -74.589 c -2.0882,0 -3.7795,1.69138 -3.7795,3.77968 v 7.55891 c 0,2.0883 1.6913,3.77953 3.7795,3.77953 h 70.6488 l -7.8897,30.23622 h -62.7591 c -2.0882,0 -3.7795,1.69137 -3.7795,3.77952 v 7.55906 c 0,2.0883 1.6913,3.77952 3.7795,3.77952 h 58.8144 l -13.8048,52.91339 h -95.4001 L 1304.5871,294.17338 Z M 1439.28,505.82677 H 1288.0989 V 483.1496 c 0,-16.67244 13.564,-30.23622 30.2362,-30.23622 h 90.7087 c 16.6726,0 30.2362,13.56378 30.2362,30.23622 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5684-6-0"
|
||||
d="m 1740.0225,645.7638 h -53.5609 l 50.4238,-50.42358 c 11.3904,-11.39059 8.2723,-30.59063 -6.1322,-37.79536 a 23.601733,23.601733 0 0 0 -10.5495,-2.48976 c -5.0177,0 -9.9972,1.59693 -14.1641,4.71969 l -114.652,85.98901 h -85.6959 c -2.0881,0 -3.7795,1.6913 -3.7795,3.77957 v 7.55905 c 0,2.08815 1.6914,3.77945 3.7795,3.77945 h 11.3386 V 676 c 0,38.4661 23.9811,71.2583 57.7746,84.4488 -6.0283,7.9276 -10.2519,17.3056 -11.7732,27.6095 -0.6848,4.6441 2.8252,8.8866 7.5213,8.8866 h 104.6078 c 4.6961,0 8.2065,-4.2378 7.5213,-8.8866 -1.5163,-10.3039 -5.74,-19.6866 -11.7728,-27.6095 33.7935,-13.1905 57.7746,-45.9827 57.7746,-84.4488 v -15.11813 h 11.3386 c 2.0881,0 3.7795,-1.6913 3.7795,-3.77945 v -7.55905 c 0,-2.08827 -1.6914,-3.77957 -3.7795,-3.77957 z m -24.9166,-73.89453 c 8.3055,-6.22673 18.5246,5.34331 11.088,12.77961 l -61.11,61.11492 h -48.5008 z M 1713.5658,676 c 0,31.3276 -18.9026,58.9464 -48.1512,70.3654 l -18.6664,7.285 12.1323,15.9495 c 2.8729,3.7797 5.1357,7.9135 6.7042,12.2269 h -85.4506 c 1.5685,-4.3134 3.8267,-8.4472 6.7041,-12.2269 l 12.1323,-15.9495 -18.6663,-7.285 C 1551.0506,734.9464 1532.1484,707.3276 1532.1484,676 v -15.11813 h 181.4174 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5695-2"
|
||||
d="M 1075.5907,1636.198 H 924.40969 a 30.236221,30.236221 0 0 0 -30.2366,30.2361 v 196.5355 a 15.118111,15.118111 0 0 0 15.11811,15.1181 h 181.4177 a 15.118111,15.118111 0 0 0 15.118,-15.1181 v -196.5355 a 30.236221,30.236221 0 0 0 -30.2362,-30.2361 z m 15.1181,226.7716 H 909.2912 v -151.1811 h 181.4176 z m 0,-166.2992 H 909.2912 v -30.2363 a 15.118111,15.118111 0 0 1 15.11849,-15.118 h 151.18101 a 15.118111,15.118111 0 0 1 15.1181,15.118 z m -22.6775,-30.2363 a 7.5590556,7.5590556 0 1 0 7.5594,7.5591 7.5590556,7.5590556 0 0 0 -7.5594,-7.5591 z m -30.2359,0 a 7.5590556,7.5590556 0 1 0 7.5591,7.5591 7.5590556,7.5590556 0 0 0 -7.5591,-7.5591 z m -75.59041,0 a 7.5590556,7.5590556 0 1 0 7.5587,7.5591 7.5590556,7.5590556 0 0 0 -7.5587,-7.5591 z m -30.23625,0 a 7.5590556,7.5590556 0 1 0 7.55906,7.5591 7.5590556,7.5590556 0 0 0 -7.55906,-7.5591 z m 0,181.4175 h 136.06256 a 7.5590556,7.5590556 0 0 0 7.5594,-7.5591 v -105.8268 a 7.5590556,7.5590556 0 0 0 -7.5594,-7.559 H 931.96874 a 7.5590556,7.5590556 0 0 0 -7.55905,7.559 v 105.8268 a 7.5590556,7.5590556 0 0 0 7.55905,7.5591 z m 7.55906,-105.8268 h 120.9444 v 90.7086 H 939.5278 Z m 98.2676,15.118 h -75.59041 a 7.5591,7.5591 0 0 0 0,15.1182 h 75.59041 a 7.5591,7.5591 0 0 0 0,-15.1182 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5673-8-4-5"
|
||||
d="m 1467.3153,1614.6952 c -6.378,-5.3858 -14.4567,-8.315 -22.7717,-8.315 -10.3464,0 -20.0787,4.4881 -26.8819,12.3307 l -9.6378,-42.0473 c -7.4173,-32.5984 -35.7165,-55.3228 -68.7874,-55.3228 -5.3386,0 -10.7244,0.6139 -15.9685,1.8425 -37.9842,8.8347 -61.748,47.2442 -53.0078,85.5592 l 21.0708,92.126 c 0.189,0.8974 0.5197,1.7479 0.7559,2.6456 -4.4409,1.4646 -8.5039,5.5748 -8.5039,10.5827 v 37.7952 c 0,6.2835 5.0551,11.3386 11.2441,11.3386 h 143.1968 c 6.189,0 11.9055,-5.0551 11.9055,-11.3386 v -37.7952 c 0,-5.3858 -4.4409,-9.6378 -9.4015,-10.8189 l 31.37,-37.9843 c 12.5671,-15.2126 10.4884,-37.8897 -4.5826,-50.5984 z m -32.5512,133.4173 h -136.063 v -30.2362 h 136.063 z m 25.6063,-92.5039 -38.9764,47.1497 h -113.7638 c -0.5669,-1.7482 -1.2756,-3.4962 -1.7008,-5.3388 l -21.0708,-92.1259 c -6.8976,-30.1889 11.811,-60.4253 41.7637,-67.4174 4.1575,-0.9453 8.3622,-1.4645 12.567,-1.4645 26.0787,0 48.3779,17.9055 54.2362,43.6064 l 12.9921,56.8818 3.4016,14.8347 9.685,-11.7166 9.1654,-11.1023 c 3.9213,-4.7718 9.685,-7.4646 15.8268,-7.4646 4.8189,0 9.4488,1.7007 13.1811,4.8189 8.7874,7.37 9.9685,20.504 2.6929,29.3386 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5706-9"
|
||||
d="m 455.99659,983.47136 c -22.56427,-58.66282 -79.4948,-97.74958 -143.13917,-97.74958 -63.64457,0 -120.58155,39.08676 -143.12757,97.74958 -25.31405,0.97615 -28.28993,22.26634 -28.28993,27.32554 a 27.43272,27.43272 0 0 0 27.50999,27.2958 c 1.61917,0 2.83909,-0.3566 4.35685,-0.4524 l 19.32015,57.9249 a 27.379152,27.379152 0 0 0 25.96867,18.713 h 188.52348 a 27.379152,27.379152 0 0 0 25.96829,-18.719 l 19.30226,-57.9189 c 1.51209,0.1014 2.73806,0.4524 4.35693,0.4524 a 27.43272,27.43272 0 0 0 27.5278,-27.2958 c 0,-5.22 -3.06519,-26.272 -28.27775,-27.32554 z m -40.97343,106.05254 a 8.3327858,8.3327858 0 0 1 -7.9041,5.7079 H 218.5895 a 8.3327858,8.3327858 0 0 1 -7.90412,-5.702 l -18.60593,-55.8237 a 75.631939,75.631939 0 0 0 17.37958,-9.9338 c 5.41071,-4.0116 7.49951,-5.6128 15.12436,0.039 7.21381,5.3568 19.32588,14.2849 40.4377,14.2849 21.11197,0 33.23583,-8.9638 40.47356,-14.3325 5.39857,-3.9937 7.44592,-5.5948 15.00504,0.028 7.23173,5.3568 19.32022,14.2848 40.43164,14.2848 21.11189,0 33.21824,-8.9637 40.43777,-14.3205 6.47601,-4.8032 8.40441,-4.8807 14.95147,0 a 75.536702,75.536702 0 0 0 17.30819,9.9339 l -18.6056,55.8297 z m 41.73547,-70.4775 c -14.8447,0 -22.73066,-5.8568 -29.11133,-10.6005 -17.6834,-13.09438 -29.79589,-5.7675 -37.62855,0.039 -6.33277,4.708 -14.22515,10.5648 -29.08742,10.5648 -14.86181,0 -22.74814,-5.8568 -29.12872,-10.5768 -18.93914,-14.09428 -31.84332,-4.3152 -37.67614,0 -6.34458,4.72 -14.23157,10.5768 -29.11144,10.5768 -14.87973,0 -22.76637,-5.8568 -29.16451,-10.5768 -4.03546,-2.976 -17.68349,-14.87988 -37.77753,0 -6.35673,4.7021 -14.28477,10.5589 -29.16452,10.5589 a 8.2554502,8.2554502 0 1 1 0,-16.5109 c 3.20802,0 6.59458,0.2861 15.92147,-4.86268 31.74184,-91.61298 118.33737,-92.88672 128.02748,-92.88672 9.52324,0 96.25581,1.19038 127.96763,92.85092 9.37404,5.30318 12.24317,4.94608 15.93934,4.94608 a 8.25545,8.25545 0 1 1 0,16.5109 z m -196.779,-84.70277 a 9.5648466,9.5648466 0 0 0 -12.7788,4.26161 l -9.5233,19.04637 a 9.5231832,9.5231832 0 0 0 4.26186,12.77891 c 5.6782,2.7975 10.92787,-0.59555 12.77845,-4.26159 l 9.5233,-19.04632 a 9.5231832,9.5231832 0 0 0 -4.26151,-12.77898 z m 118.52804,4.25571 a 9.5252893,9.5252893 0 1 0 -17.04022,8.51727 l 9.52286,19.04637 c 1.84526,3.67236 7.10105,7.05909 12.77882,4.26159 a 9.5231832,9.5231832 0 0 0 4.2615,-12.77891 z m -65.65025,-5.26154 a 9.5231832,9.5231832 0 0 0 -9.52334,9.5232 v 19.04625 a 9.5233032,9.5233032 0 1 0 19.04658,0 V 942.861 a 9.5231832,9.5231832 0 0 0 -9.52324,-9.5232 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.595201" />
|
||||
<path
|
||||
id="path5662-5"
|
||||
d="m 1075.5909,154.17314 a 44.962217,44.962217 0 0 0 -27.2742,9.3261 C 1037.2899,148.7401 1019.8425,139.0551 1000,139.0551 c -19.84243,0 -37.28983,9.685 -48.31663,24.44414 a 45.146469,45.146469 0 0 0 -72.62811,36.02827 c 0,19.71972 30.23621,105.82684 30.23621,105.82684 v 60.47252 a 15.118115,15.118115 0 0 0 15.1181,15.11803 h 151.18133 a 15.118115,15.118115 0 0 0 15.1181,-15.11803 v -60.47252 c 0,0 30.2362,-86.10712 30.2362,-105.82684 a 45.354343,45.354343 0 0 0 -45.3543,-45.35437 z M 931.96867,358.26781 v -37.79527 h 136.06323 v 37.79527 z M 1069.3264,297.7953 h -13.9609 l 5.1073,-63.66146 a 3.7795287,3.7795287 0 0 0 -3.4628,-4.0676 l -7.5591,-0.60506 h -0.3061 a 3.7795287,3.7795287 0 0 0 -3.7795,3.48185 l -5.1969,64.86621 h -32.92 V 233.5431 a 3.7795287,3.7795287 0 0 0 -3.7796,-3.77952 h -7.55897 a 3.7795287,3.7795287 0 0 0 -3.77986,3.77952 v 64.25193 h -32.324 l -5.1968,-64.86606 a 3.7795287,3.7795287 0 0 0 -3.7799,-3.48201 h -0.3062 l -7.5587,0.60507 a 3.7795287,3.7795287 0 0 0 -3.4631,4.06764 l 5.1352,63.67563 h -13.9604 c -13.8237,-39.3165 -28.72444,-88.18587 -28.94176,-98.26779 a 22.474022,22.474022 0 0 1 36.24566,-17.95272 l 18.1984,13.80476 13.6724,-18.2976 c 7.285,-9.75598 18.274,-15.34968 30.15113,-15.34968 11.8771,0 22.8661,5.5937 30.1511,15.34019 l 13.6729,18.29764 18.1984,-13.79531 a 22.474022,22.474022 0 0 1 36.2456,17.915 c -0.2192,10.11964 -15.1181,58.98901 -28.9417,98.30551 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5739"
|
||||
d="m 689.18112,501.24409 6.7087,-77.85831 c -19.88984,-15.63783 -33.25984,-36.37803 -33.25984,-61.18099 0.0476,-44.55122 41.66925,-83.14968 71.85815,-83.14968 10.44099,0 18.89764,8.1733 18.89764,18.18894 v 205.51174 c 0,10.01578 -8.45665,18.18909 -18.89764,18.18909 h -26.45661 c -10.77162,0 -19.74803,-8.8348 -18.8504,-19.70079 z m -11.38586,-139.0393 c 0,24.09438 15.21263,40.39355 33.92126,54.09438 l -7.46453,86.26772 c -0.13644,1.74799 1.60619,3.25984 3.77953,3.25984 h 26.45661 c 2.03158,0 3.77953,-1.41778 3.77953,-3.07094 V 297.24405 c 0,-1.65347 -1.74795,-3.0708 -3.77953,-3.0708 -21.07082,0 -56.69287,31.08651 -56.69287,68.03154 z m -37.46457,-67.18118 c -1.55913,-9.21252 -10.01567,-15.9685 -21.77945,-15.9685 -5.62205,0 -11.38587,1.60637 -15.7324,5.29137 -3.96839,-3.35433 -9.44878,-5.29137 -15.73221,-5.29137 -6.28354,0 -11.76382,1.93704 -15.73236,5.29137 -4.34634,-3.685 -10.1102,-5.29137 -15.73221,-5.29137 -11.90555,0 -20.22047,6.85047 -21.7796,15.9685 -0.85096,4.44087 -7.22823,39.96854 -7.22823,54.85039 0,24.04714 12.61413,43.27568 33.54323,52.29918 l -5.38579,99.59059 c -0.56689,10.39359 7.70071,19.18111 18.1417,19.18111 h 28.34641 c 10.39386,0 18.70874,-8.74027 18.14181,-19.18111 l -5.3859,-99.59059 c 20.88197,-9.0235 33.54342,-28.25204 33.54342,-52.29918 0,-14.88185 -6.3781,-50.40952 -7.22842,-54.85039 z m -41.76382,97.41733 5.76386,110.12595 c 0.0915,1.74799 -1.27499,3.25984 -3.07095,3.25984 h -28.34641 c -1.748,0 -3.16532,-1.46491 -3.07083,-3.25984 l 5.76374,-110.12595 c -20.03146,-4.15748 -33.8739,-20.31504 -33.8739,-42.56694 0,-14.03145 6.99208,-52.25204 6.99208,-52.25204 0.75523,-4.67713 13.37001,-4.58264 13.93694,0.0911 V 355.587 c 0.42561,5.433 13.32294,5.52752 13.937,0.0911 l 3.49614,-58.06295 c 0.75523,-4.58264 13.18111,-4.58264 13.93694,0 l 3.49613,58.06295 c 0.61395,5.3859 13.51182,5.29138 13.93701,-0.0911 v -57.82681 c 0.56689,-4.67716 13.18107,-4.77154 13.93693,-0.0911 0,0 6.9921,38.22055 6.9921,52.25205 0.0476,22.11023 -13.70079,38.36217 -33.82678,42.51961 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5772-1"
|
||||
d="m 1751.068,1034.0124 c -6.2598,0 -11.3384,5.0739 -11.3384,11.3384 0,6.2597 5.0786,11.3383 11.3384,11.3383 6.2597,0 11.3383,-5.0786 11.3383,-11.3383 0,-6.2645 -5.0786,-11.3384 -11.3383,-11.3384 z m -79.3686,-64.25069 c 0,-6.26453 -5.0786,-11.3384 -11.3384,-11.3384 -6.2597,0 -11.3383,5.07387 -11.3383,11.3384 0,6.25969 5.0786,11.33828 11.3383,11.33828 6.2598,0 11.3384,-5.07859 11.3384,-11.33828 z m 3.7795,64.25069 c -6.2598,0 -11.3383,5.0739 -11.3383,11.3384 0,6.2597 5.0785,11.3383 11.3383,11.3383 6.2596,0 11.3384,-5.0786 11.3384,-11.3383 0,-6.2645 -5.0788,-11.3384 -11.3384,-11.3384 z m 45.3534,-45.3535 c -6.2597,0 -11.3384,5.07386 -11.3384,11.33839 0,6.25971 5.0787,11.33841 11.3384,11.33841 6.2597,0 11.3384,-5.0787 11.3384,-11.33841 0,-6.26453 -5.0787,-11.33839 -11.3384,-11.33839 z m 105.1256,11.25331 c -33.0562,-0.40123 -59.7485,-27.25459 -59.7485,-60.40509 -33.1505,0 -59.9989,-26.68762 -60.4051,-59.73902 -3.1275,-0.47698 -6.2786,-0.71299 -9.4156,-0.71299 -9.7699,0 -19.4547,2.29133 -28.2467,6.76995 l -32.6592,16.6391 a 62.465402,62.465402 0 0 0 -27.3019,27.3112 l -16.5824,32.5411 a 62.665709,62.665709 0 0 0 -6.0564,38.24344 l 5.7069,36.0371 a 62.623193,62.623193 0 0 0 17.5556,34.4685 l 25.875,25.8704 a 62.378004,62.378004 0 0 0 34.3506,17.5084 l 36.2402,5.7354 c 3.2409,0.5155 6.5007,0.7652 9.751,0.7652 9.7888,0 19.4879,-2.3056 28.2939,-6.7937 l 32.6592,-16.639 a 62.465402,62.465402 0 0 0 27.3018,-27.3112 l 16.5825,-32.5412 c 5.9337,-11.636 8.036,-24.8357 6.0991,-37.74735 z m -19.5681,30.87819 -16.5825,32.5458 c -4.573,8.9762 -11.7304,16.1335 -20.6971,20.702 l -32.6593,16.6391 c -6.6046,3.3636 -14.0171,5.1446 -21.4295,5.1446 -2.4614,0 -4.9464,-0.1935 -7.3841,-0.5813 l -36.2356,-5.7353 c -9.921,-1.5731 -18.9208,-6.1605 -26.0262,-13.2658 l -25.8751,-25.8704 c -7.1337,-7.129 -11.7352,-16.1713 -13.3131,-26.1397 l -5.707,-36.03228 c -1.5826,-9.97784 0.015,-20.01219 4.5921,-29.01204 l 16.5823,-32.54109 c 4.5731,-8.97622 11.7305,-16.13351 20.6973,-20.70194 l 32.6544,-16.64383 c 5.3527,-2.72591 11.2439,-4.40784 17.2485,-4.94162 6.2125,29.83412 30.1601,53.36111 60.1169,58.98788 5.6266,29.96157 29.1538,53.90912 58.9926,60.11682 -0.5193,6.0141 -2.1921,11.8769 -4.9746,17.3288 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472433" />
|
||||
<path
|
||||
id="path5627-3"
|
||||
d="m 1556.6704,1294.1237 c -8.3858,0 -15.1181,-6.7323 -15.1181,-15.118 0,-14.6458 15.1181,-10.8662 15.1181,-30.2363 5.6693,0 15.1181,13.9371 15.1181,26.4567 0,12.5197 -6.7323,18.8976 -15.1181,18.8976 z m 60.4724,0 c -8.3858,0 -15.118,-6.7323 -15.118,-15.118 0,-14.6458 15.118,-10.8662 15.118,-30.2363 5.6693,0 15.1182,13.9371 15.1182,26.4567 0,12.5197 -6.7323,18.8976 -15.1182,18.8976 z m 60.4725,0 c -8.3858,0 -15.1181,-6.7323 -15.1181,-15.118 0,-14.6458 15.1181,-10.8662 15.1181,-30.2363 5.6693,0 15.1181,13.9371 15.1181,26.4567 0,12.5197 -6.7323,18.8976 -15.1181,18.8976 z m 22.6771,75.5907 h -15.118 v -68.0316 h -15.1182 v 68.0316 h -45.3543 v -68.0316 h -15.1181 v 68.0316 h -45.3543 v -68.0316 h -15.1181 v 68.0316 h -15.1181 c -12.5197,0 -22.6772,10.1573 -22.6772,22.6771 v 98.2677 h 211.6535 v -98.2677 c 0,-12.5198 -10.1575,-22.6771 -22.6772,-22.6771 z m 7.5591,105.8267 h -181.4173 v -34.036 c 7.6639,-4.4783 11.3045,-11.3183 20.1968,-11.3183 13.2052,0 14.7652,15.118 35.315,15.118 20.2408,0 22.3072,-15.118 35.1968,-15.118 13.2983,0 14.7407,15.118 35.315,15.118 20.4837,0 22.0947,-15.118 35.315,-15.118 8.7345,0 12.3992,6.8386 20.0787,11.3173 z m 0,-53.0901 c -4.5476,-3.72 -10.0238,-7.3823 -20.0787,-7.3823 -20.5181,0 -22.1221,15.1181 -35.315,15.1181 -13.0842,0 -14.8602,-15.1181 -35.315,-15.1181 -20.2403,0 -22.3076,15.1181 -35.1968,15.1181 -13.2983,0 -14.7411,-15.1181 -35.315,-15.1181 -10.1399,0 -15.6382,3.6728 -20.1968,7.3974 v -30.0746 c 0,-4.1678 3.3912,-7.5591 7.5591,-7.5591 h 166.2991 c 4.168,0 7.5591,3.3913 7.5591,7.5591 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5794"
|
||||
d="m 455.82468,561.09456 c -59.4562,0 -111.33852,39.33117 -140.22863,73.01275 l -57.67284,-51.27053 c -8.03297,-6.61302 -19.97164,0.40092 -17.95038,10.50915 L 255.94899,676 239.96708,758.66025 c -2.01551,10.10208 9.91131,17.11599 17.92597,10.52702 l 57.67356,-51.25873 c 28.91344,33.64577 80.82557,72.9769 140.25807,72.9769 90.81482,0 164.43402,-91.92438 164.43402,-114.90544 0,-22.98105 -73.6192,-114.90544 -164.43402,-114.90544 z m 0,210.66009 c -50.77599,0 -98.29963,-34.27421 -125.83995,-66.35192 l -12.09918,-14.09391 c -18.54503,15.24893 -7.27783,5.31435 -54.74779,48.8408 12.93728,-68.15931 11.15984,-58.87111 12.2299,-64.14962 -1.07006,-5.27254 0.71277,4.02766 -12.22415,-64.14959 47.56512,43.59825 36.26847,33.63976 54.74778,48.84688 l 12.09918,-14.09398 c 27.54104,-32.07776 75.03488,-66.35793 125.83421,-66.35793 76.21859,0 140.366,75.69999 145.28359,95.75462 -4.91759,20.05463 -69.065,95.75465 -145.28359,95.75465 z m -12.94948,-149.82603 -6.72448,-6.76864 a 4.7565564,4.7877296 0 0 0 -6.73058,0 l -50.34816,50.66611 a 4.7565564,4.7877296 0 0 0 0,6.76863 l 6.72484,6.76862 a 4.7565564,4.7877296 0 0 0 6.72448,0 l 50.3539,-50.64218 a 4.7565564,4.7877296 0 0 0 0,-6.79254 z m 66.59166,9.57547 -6.72448,-6.7686 a 4.7565564,4.7877296 0 0 0 -6.73023,0 l -88.40107,88.9679 a 4.7565564,4.7877296 0 0 0 0,6.76865 l 6.7252,6.76861 a 4.7565564,4.7877296 0 0 0 6.72449,0 l 88.40609,-88.94403 a 4.7565564,4.7877296 0 0 0 0,-6.79253 z m 21.81517,41.10869 a 4.7565564,4.7877296 0 0 0 -6.73096,0 l -50.34798,50.66612 a 4.7565564,4.7877296 0 0 0 0,6.76863 l 6.72413,6.76861 a 4.7565564,4.7877296 0 0 0 6.7252,0 l 50.35336,-50.64219 a 4.7565564,4.7877296 0 0 0 0,-6.77461 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.596513" />
|
||||
<path
|
||||
id="path5728"
|
||||
d="m 385.71444,1248.7695 c -66.79359,0 -120.94515,54.1512 -120.94515,120.9449 0,66.7937 54.15156,120.9448 120.94515,120.9448 66.79355,0 120.94484,-54.1511 120.94484,-120.9448 0,-66.7937 -54.15129,-120.9449 -120.94484,-120.9449 z m 0,226.7716 c -58.35119,0 -105.82666,-47.4756 -105.82666,-105.8267 0,-58.3512 47.47547,-105.8268 105.82666,-105.8268 58.35107,0 105.82669,47.4756 105.82669,105.8268 0,58.3511 -47.47562,105.8267 -105.82669,105.8267 z m 74.82984,-105.8267 13.3606,-13.3607 c 1.47405,-1.474 1.47405,-3.8693 0,-5.3433 l -5.34324,-5.3433 c -1.47401,-1.474 -3.8694,-1.474 -5.34342,0 l -13.36059,13.3606 -21.3826,-21.3779 10.6913,-10.6913 8.01736,8.0173 c 1.47402,1.4739 3.86922,1.4739 5.34323,0 l 5.34342,-5.3433 c 1.47402,-1.4741 1.47402,-3.8693 0,-5.3433 l -8.01736,-8.0174 2.67394,-2.674 c 1.47402,-1.4739 1.47402,-3.8693 0,-5.3433 l -5.34323,-5.3434 c -1.47401,-1.4739 -3.86921,-1.4739 -5.34323,0 l -2.67413,2.6741 -8.01736,-8.0172 c -1.47402,-1.474 -3.86922,-1.474 -5.34323,0 l -5.34327,5.3431 c -1.47402,1.4741 -1.47402,3.8694 0,5.3434 l 8.01721,8.0173 -10.6913,10.6913 -21.3826,-21.3779 13.36524,-13.3606 c 1.47401,-1.474 1.47401,-3.8692 0,-5.3433 l -5.34788,-5.3622 c -1.47402,-1.474 -3.86941,-1.474 -5.34342,0 l -13.36528,13.3654 -13.36539,-13.3654 c -1.47402,-1.474 -3.86922,-1.474 -5.34324,0 l -5.34346,5.3433 c -1.47397,1.474 -1.47397,3.8692 0,5.3434 l 13.36544,13.3606 -21.37796,21.3779 -10.6913,-10.6914 8.01717,-8.0173 c 1.47402,-1.4739 1.47402,-3.8693 0,-5.3433 l -5.34323,-5.3433 c -1.47402,-1.474 -3.86922,-1.474 -5.34323,0 l -8.01736,8.0174 -2.67413,-2.6741 c -1.47402,-1.474 -3.86922,-1.474 -5.34323,0 l -5.34312,5.3433 c -1.47402,1.474 -1.47402,3.8693 0,5.3434 l 2.67364,2.6739 -8.01714,8.0173 c -1.47401,1.4741 -1.47401,3.8694 0,5.3433 l 5.3435,5.3434 c 1.47401,1.474 3.8691,1.474 5.34312,0 l 8.01736,-8.0174 10.6913,10.6914 -21.37814,21.378 -13.36517,-13.3607 c -1.47402,-1.4739 -3.86948,-1.4739 -5.3435,0 l -5.35748,5.3575 c -1.47401,1.4741 -1.47401,3.8693 0,5.3433 l 13.36555,13.3654 -13.36555,13.3653 c -1.47401,1.4741 -1.47401,3.8693 0,5.3433 l 5.3435,5.3434 c 1.47402,1.474 3.8691,1.474 5.34312,0 l 13.36554,-13.3606 21.37803,21.3778 -10.69149,10.6914 -8.01706,-8.0173 c -1.47401,-1.474 -3.86948,-1.474 -5.34349,0 l -5.3435,5.3432 c -1.47364,1.474 -1.47364,3.8694 0,5.3434 l 8.01751,8.0172 -2.67401,2.6741 c -1.47402,1.474 -1.47402,3.8694 0,5.3434 l 5.34349,5.3432 c 1.47402,1.4741 3.8691,1.4741 5.34312,0 l 2.67394,-2.6739 8.01736,8.0172 c 1.47402,1.474 3.86941,1.474 5.34342,0 l 5.34323,-5.3433 c 1.47402,-1.474 1.47402,-3.8692 0,-5.3432 l -8.01736,-8.0175 10.6913,-10.6912 21.3828,21.3827 -13.36544,13.3652 c -1.47401,1.474 -1.47401,3.8695 0,5.3433 l 5.34343,5.3433 c 1.47401,1.4741 3.86921,1.4741 5.34323,0 l 13.37488,-13.3606 13.36528,13.3653 c 1.47401,1.474 3.8694,1.474 5.34342,0 l 5.34323,-5.3433 c 1.47401,-1.4739 1.47401,-3.8692 0,-5.3432 l -13.36543,-13.3653 21.38279,-21.3828 10.6913,10.6913 -8.01736,8.0173 c -1.47402,1.474 -1.47402,3.8694 0,5.3434 l 5.34323,5.3433 c 1.47402,1.474 3.86941,1.474 5.34342,0 l 8.01736,-8.0173 2.67394,2.674 c 1.47402,1.474 3.86922,1.474 5.34323,0 l 5.34342,-5.3434 c 1.47406,-1.474 1.47406,-3.8692 0,-5.3432 l -2.67409,-2.6741 8.01736,-8.0172 c 1.47402,-1.4741 1.47402,-3.8694 0,-5.3434 l -5.34327,-5.3433 c -1.47401,-1.474 -3.8694,-1.474 -5.34342,0 l -8.01717,8.0174 -10.69149,-10.6914 21.37799,-21.378 13.3654,13.3606 c 1.47405,1.474 3.86925,1.474 5.34326,0 l 5.34343,-5.3432 c 1.47397,-1.474 1.47397,-3.8694 0,-5.3433 z m -74.82984,-53.452 21.3778,21.378 -21.3778,21.3827 -21.37795,-21.3781 z m -53.45201,53.452 21.37795,-21.3827 21.38261,21.3827 -21.38261,21.3827 z m 53.45201,53.4519 -21.38275,-21.3827 21.38275,-21.3826 21.38264,21.3826 z m 32.06929,-32.074 -21.37795,-21.3779 21.3826,-21.378 21.37795,21.378 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472441" />
|
||||
<path
|
||||
id="path5717-1"
|
||||
d="m 692.85695,1593.1058 a 3.8361918,3.8361918 0 0 0 3.77953,3.685 h 7.55867 a 3.7511533,3.7511533 0 0 0 3.77991,-3.8739 c -0.99288,-19.8897 -9.11811,-34.6297 -19.65317,-45.1178 -8.69329,-8.8345 -9.96888,-18.0471 -10.34683,-23.1022 a 3.7180827,3.7180827 0 0 0 -3.77953,-3.3543 l -7.55905,0.045 a 3.7794994,3.7794994 0 0 0 -3.73191,4.1102 49.823248,49.823248 0 0 0 14.74016,32.9289 c 8.17285,8.1259 14.36183,19.2282 15.21222,34.6768 z m -52.67679,0 a 3.8361918,3.8361918 0 0 0 3.77953,3.685 h 7.55905 a 3.7511533,3.7511533 0 0 0 3.77953,-3.8739 c -0.99212,-19.8897 -9.11811,-34.6297 -19.65354,-45.1178 -8.64567,-8.8345 -9.92088,-18.0471 -10.29884,-23.1022 a 3.7322557,3.7322557 0 0 0 -3.82677,-3.3543 l -7.55905,0.045 a 3.7794994,3.7794994 0 0 0 -3.73229,4.1102 c 0.47244,7.0393 2.45669,20.5982 14.74016,32.9289 8.16831,8.1259 14.35729,19.2282 15.21222,34.6768 z m 128.50318,18.9448 H 557.03131 a 15.117997,15.117997 0 0 0 -15.11811,15.1179 c 0,44.7399 24.35868,83.6971 60.47206,104.6214 v 16.3227 a 15.117997,15.117997 0 0 0 15.11773,15.118 h 90.70791 a 15.117997,15.117997 0 0 0 15.11849,-15.118 v -16.3227 c 36.11263,-20.9243 60.47169,-59.8815 60.47169,-104.6214 a 15.117997,15.117997 0 0 0 -15.11774,-15.1179 z m -60.47244,111.0228 v 25.0392 h -90.70791 v -25.0392 c -62.52207,-36.2218 -60.47168,-87.7695 -60.47168,-95.9049 h 211.65203 c 0,8.2488 1.59194,59.9476 -60.47244,95.9049 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#303030;fill-opacity:1;stroke-width:0.472437" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 24 KiB |
1129
cookbook/static/css/app.min.css
vendored
Normal file
1129
cookbook/static/css/app.min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
230
cookbook/static/themes/tandoor.min.css
vendored
230
cookbook/static/themes/tandoor.min.css
vendored
@@ -1,83 +1,91 @@
|
||||
/* devanagari */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_devanagari_400.woff2) format('woff2');
|
||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_devanagari_400.woff2) format('woff2');
|
||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_ext_400.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_ext_400.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_400.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_400.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* devanagari */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_devanagari_500.woff2) format('woff2');
|
||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_devanagari_500.woff2) format('woff2');
|
||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_ext_500.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_ext_500.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_500.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_500.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* devanagari */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_devanagari_700.woff2) format('woff2');
|
||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_devanagari_700.woff2) format('woff2');
|
||||
unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_ext_700.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_ext_700.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_700.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/static/webfonts/poppins_latin_700.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +100,7 @@
|
||||
--indigo: #6610f2;
|
||||
--purple: #6f42c1;
|
||||
--pink: #e83e8c;
|
||||
--#a7240e: #dc3545;
|
||||
-- #a7240e: #dc3545;
|
||||
--orange: #fd7e14;
|
||||
--yellow: #ffc107;
|
||||
--green: #28a745;
|
||||
@@ -1812,7 +1820,9 @@ pre code {
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.form-control {
|
||||
transition: none
|
||||
}
|
||||
@@ -2275,7 +2285,9 @@ select.form-control[multiple], select.form-control[size], textarea.form-control
|
||||
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.btn {
|
||||
transition: none
|
||||
}
|
||||
@@ -2807,7 +2819,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
transition: opacity .15s linear
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.fade {
|
||||
transition: none
|
||||
}
|
||||
@@ -2828,7 +2842,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
transition: height .35s ease
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.collapsing {
|
||||
transition: none
|
||||
}
|
||||
@@ -3439,7 +3455,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
transition: transform .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.custom-switch .custom-control-label:after {
|
||||
transition: none
|
||||
}
|
||||
@@ -3621,7 +3639,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
appearance: none
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.custom-range::-webkit-slider-thumb {
|
||||
transition: none
|
||||
}
|
||||
@@ -3652,7 +3672,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
appearance: none
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.custom-range::-moz-range-thumb {
|
||||
transition: none
|
||||
}
|
||||
@@ -3685,7 +3707,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
appearance: none
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.custom-range::-ms-thumb {
|
||||
transition: none
|
||||
}
|
||||
@@ -3738,7 +3762,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.custom-control-label:before, .custom-file-label, .custom-select {
|
||||
transition: none
|
||||
}
|
||||
@@ -4164,12 +4190,12 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-toggler {
|
||||
color: hsla(0, 0%, 18%, .5);
|
||||
border-color: hsla(0, 0%, 18%, .1)
|
||||
color: rgba(46, 46, 46, 0.5);
|
||||
border-color: rgba(46, 46, 46, 0.5);
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(46, 46, 46, 1)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-text {
|
||||
@@ -4235,7 +4261,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
.card-header {
|
||||
padding: .75rem 1.25rem;
|
||||
margin-bottom: 0;
|
||||
background-color: rgba(0, 0, 0, .03);
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .125)
|
||||
}
|
||||
|
||||
@@ -4249,7 +4275,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
|
||||
.card-footer {
|
||||
padding: .75rem 1.25rem;
|
||||
background-color: rgba(0, 0, 0, .03);
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid rgba(0, 0, 0, .125)
|
||||
}
|
||||
|
||||
@@ -4551,7 +4577,9 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
|
||||
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.badge {
|
||||
transition: none
|
||||
}
|
||||
@@ -4772,9 +4800,9 @@ a.badge-dark.focus, a.badge-dark:focus {
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #316f5d;
|
||||
background-color: #dff7f0;
|
||||
border-color: #d2f4ea
|
||||
color: #2e2e2e;
|
||||
background-color: #82aa8b;
|
||||
border-color: #82aa8b
|
||||
}
|
||||
|
||||
.alert-success hr {
|
||||
@@ -4893,7 +4921,9 @@ a.badge-dark.focus, a.badge-dark:focus {
|
||||
transition: width .6s ease
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.progress-bar {
|
||||
transition: none
|
||||
}
|
||||
@@ -4909,7 +4939,9 @@ a.badge-dark.focus, a.badge-dark:focus {
|
||||
animation: progress-bar-stripes 1s linear infinite
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.progress-bar-animated {
|
||||
-webkit-animation: none;
|
||||
animation: none
|
||||
@@ -5358,7 +5390,9 @@ a.close.disabled {
|
||||
transform: translateY(-50px)
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.modal.fade .modal-dialog {
|
||||
transition: none
|
||||
}
|
||||
@@ -5838,7 +5872,9 @@ a.close.disabled {
|
||||
transition: transform .6s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.carousel-item {
|
||||
transition: none
|
||||
}
|
||||
@@ -5873,7 +5909,9 @@ a.close.disabled {
|
||||
transition: opacity 0s .6s
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.carousel-fade .active.carousel-item-left, .carousel-fade .active.carousel-item-right {
|
||||
transition: none
|
||||
}
|
||||
@@ -5894,7 +5932,9 @@ a.close.disabled {
|
||||
transition: opacity .15s ease
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.carousel-control-next, .carousel-control-prev {
|
||||
transition: none
|
||||
}
|
||||
@@ -5961,7 +6001,9 @@ a.close.disabled {
|
||||
transition: opacity .6s ease
|
||||
}
|
||||
|
||||
@media (prefers-#a7240euced-motion: #a7240euce) {
|
||||
@media (prefers-#a7240euced-motion: #a7240euce
|
||||
|
||||
) {
|
||||
.carousel-indicators li {
|
||||
transition: none
|
||||
}
|
||||
@@ -10117,9 +10159,9 @@ footer a:hover {
|
||||
}
|
||||
|
||||
.btn-light:hover {
|
||||
background: transparent;
|
||||
background-color: hsla(0, 0%, 18%, .5);
|
||||
color: #cfd5cd;
|
||||
border: 1px solid #cfd5cd
|
||||
border: 1px solid hsla(0, 0%, 18%, .5)
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
@@ -10360,16 +10402,6 @@ footer a:hover {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 500px
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.modal-content {
|
||||
width: 300px
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content .modal-header {
|
||||
justify-content: center;
|
||||
border: none
|
||||
@@ -10385,6 +10417,12 @@ footer a:hover {
|
||||
padding: 5px 0 20px 39px
|
||||
}
|
||||
|
||||
.modal-content .modal-footer {
|
||||
border: none;
|
||||
justify-content: flex-start;
|
||||
padding: 5px 0 20px 39px
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=maps/style.min.css.map */
|
||||
|
||||
.bg-header {
|
||||
@@ -10402,7 +10440,7 @@ footer a:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
textarea, input:not([type="submit"]):not([class="multiselect__input"]), select {
|
||||
textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([class="select2-search__field"]), select {
|
||||
background-color: white !important;
|
||||
border-radius: .25rem !important;
|
||||
border: 1px solid #ced4da !important;
|
||||
@@ -10413,10 +10451,14 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]), select {
|
||||
color: #212529 !important;
|
||||
}
|
||||
|
||||
.multiselect__tag-icon:hover,.multiselect__tag-icon:focus {
|
||||
.multiselect__tag-icon:hover, .multiselect__tag-icon:focus {
|
||||
background-color: #a7240e !important;
|
||||
}
|
||||
|
||||
.multiselect__tag-icon:after {
|
||||
color: #212529 !important
|
||||
}
|
||||
|
||||
.form-control-search {
|
||||
font-size: 20px;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
cookbook/static/vue/js/user_file_view.js
Normal file
1
cookbook/static/vue/js/user_file_view.js
Normal file
File diff suppressed because one or more lines are too long
1
cookbook/static/vue/user_file_view.html
Normal file
1
cookbook/static/vue/user_file_view.html
Normal file
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/user_file_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>
|
||||
@@ -141,17 +141,17 @@ class ShoppingListTable(tables.Table):
|
||||
|
||||
class InviteLinkTable(tables.Table):
|
||||
link = tables.TemplateColumn(
|
||||
"<a href='{% url 'view_signup' record.uuid %}' >" + _('Link') + "</a>"
|
||||
"<input value='{{ request.scheme }}://{{ request.get_host }}{% url 'view_invite' record.uuid %}' class='form-control' />"
|
||||
)
|
||||
delete = tables.TemplateColumn(
|
||||
"<a href='{% url 'delete_invite_link' record.id %}' >" + _('Delete') + "</a>" # noqa: E501
|
||||
delete_link = tables.TemplateColumn(
|
||||
"<a href='{% url 'delete_invite_link' record.pk %}' >" + _('Delete') + "</a>", verbose_name=_('Delete')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InviteLink
|
||||
template_name = 'generic/table_template.html'
|
||||
fields = (
|
||||
'username', 'group', 'valid_until', 'created_by', 'created_at'
|
||||
'username', 'group', 'valid_until',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
{% block title %}{% trans "E-mail Addresses" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans 'Email' %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h3>{% trans "E-mail Addresses" %}</h3>
|
||||
{% if user.emailaddress_set.all %}
|
||||
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% load account socialaccount %}
|
||||
|
||||
@@ -8,6 +9,7 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{% trans "Sign In" %}</h3>
|
||||
@@ -16,7 +18,8 @@
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 offset-3">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<hr>
|
||||
<form class="login" method="POST" action="{% url 'account_login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
@@ -25,12 +28,13 @@
|
||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-primary" type="submit">{% trans "Sign In" %}</button>
|
||||
<a class="btn btn-success" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
|
||||
{% if settings.EMAIL_HOST != '' %}
|
||||
<a class="btn btn-secondary"
|
||||
href="{% url 'account_reset_password' %}">{% trans "Reset Password" %}</a>
|
||||
{% if EMAIL_ENABLED %}
|
||||
<a class="btn btn-warning float-right d-none d-xl-block d-lg-block"
|
||||
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
|
||||
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
@@ -40,13 +44,13 @@
|
||||
|
||||
{% if socialaccount_providers %}
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col-6 offset-3">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<h5>{% trans "Social Login" %}</h5>
|
||||
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<ul class="socialaccount_providers">
|
||||
<ul class="socialaccount_providers list-unstyled">
|
||||
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
|
||||
</ul>
|
||||
|
||||
|
||||
24
cookbook/templates/account/password_change.html
Normal file
24
cookbook/templates/account/password_change.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans 'Password' %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1>{% trans "Change Password" %}</h1>
|
||||
|
||||
<form method="POST" action="{% url 'account_change_password' %}" class="password_change">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<button type="submit" name="action" class="btn btn-success">{% trans "Change Password" %}</button>
|
||||
<a href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -8,25 +8,31 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{% trans "Password Reset" %}</h3>
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{% trans "Password Reset" %}</h3>
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if settings.EMAIL_HOST != '' %}
|
||||
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
|
||||
|
||||
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<input type="submit" class="btn btn-success" value="{% trans 'Reset My Password' %}"/>
|
||||
<a class="btn btn-primary" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
|
||||
<a class="btn btn-info" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% trans 'Password reset is disabled on this instance.' %}</p>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<hr>
|
||||
{% if EMAIL_ENABLED %}
|
||||
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
|
||||
|
||||
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<input type="submit" class="btn btn-warning float-right" value="{% trans 'Reset My Password' %}"/>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% trans 'Password reset is disabled on this instance.' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
23
cookbook/templates/account/password_set.html
Normal file
23
cookbook/templates/account/password_set.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Set Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans 'Password' %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1>{% trans "Set Password" %}</h1>
|
||||
|
||||
<form method="POST" action="{% url 'account_set_password' %}" class="password_set">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<input type="submit" class="btn btn-primary" name="action" value="{% trans 'Set Password' %}"/>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,19 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Register' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{% trans 'Create your Account' %}</h3>
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{% trans "Create an Account" %}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create User' %}</button>
|
||||
</form>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<hr>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<p>{% trans 'Already have an account?' %} <a href="{% url 'account_login' %}">{% trans "Sign In" %}</a></p>
|
||||
<div class="form-group">
|
||||
{{ form.username |as_crispy_field }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.email |as_crispy_field }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.email2 |as_crispy_field }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password1 |as_crispy_field }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password2 |as_crispy_field }}
|
||||
</div>
|
||||
|
||||
{% if TERMS_URL != '' or PRIVACY_URL != '' %}
|
||||
<div class="form-group">
|
||||
{{ form.terms |as_crispy_field }}
|
||||
<small>
|
||||
{% trans 'I accept the follwoing' %}
|
||||
{% if TERMS_URL != '' %}
|
||||
<a href="{{ TERMS_URL }}" target="_blank"
|
||||
rel="noreferrer nofollow">{% trans 'Terms and Conditions' %}</a>
|
||||
{% endif %}
|
||||
{% if TERMS_URL != '' or PRIVACY_URL != '' %}
|
||||
{% trans 'and' %}
|
||||
{% endif %}
|
||||
{% if PRIVACY_URL != '' %}
|
||||
<a href="{{ PRIVACY_URL }}" target="_blank"
|
||||
rel="noreferrer nofollow">{% trans 'Privacy Policy' %}</a>
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if CAPTCHA_ENABLED %}
|
||||
<div class="form-group">
|
||||
{{ form.captcha.errors }}
|
||||
{{ form.captcha }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create User' %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p>{% trans 'Already have an account?' %} <a href="{% url 'account_login' %}">{% trans "Sign In" %}</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user