Compare commits

...

18 Commits

Author SHA1 Message Date
vabene1111
ed313cbf9a temporary fix to merge ingredients without recipes 2021-06-07 18:28:16 +02:00
vabene1111
b1db591e9f fixed importer description overflow 2021-06-07 17:17:34 +02:00
vabene1111
94c51f90cd fixed recipe book entry remove 2021-06-07 17:12:00 +02:00
vabene1111
3074d916dc many signup and space management fixes 2021-06-07 16:46:28 +02:00
vabene1111
bf467b1ec0 manage link for hosted version 2021-06-07 16:09:26 +02:00
vabene1111
348c1c78f1 split signup forms working again 2021-06-07 16:03:10 +02:00
vabene1111
913e896906 fixed invite link system 2021-06-05 19:01:05 +02:00
vabene1111
a0a673a0c9 uncoment debug code 2021-06-05 18:41:48 +02:00
Kaibu
3d60379ed0 style fixes 2021-06-05 18:10:10 +02:00
vabene1111
fd7e20a46b signup captcha support + privacy/terms support 2021-06-05 16:40:28 +02:00
vabene1111
a970f0c00e made tandoor theme default 2021-06-05 16:38:38 +02:00
vabene1111
297dd6244a model metrics 2021-06-05 15:06:54 +02:00
vabene1111
c83eb1a42b prometheus basics and aws fix 2021-06-05 14:41:32 +02:00
vabene1111
8181a6d416 s3 signature version default 2021-06-05 14:14:53 +02:00
vabene1111
4a8b50aeba enable caching for signed s3 urls 2021-06-05 13:43:10 +02:00
vabene1111
388ef32475 actually apply limits everywhere 2021-06-04 17:05:03 +02:00
vabene1111
bfe72210df fixed recipe creation broke with new max_recipes setting 2021-06-04 17:01:58 +02:00
vabene1111
02c5aed0a3 moved demo to space setting 2021-06-04 16:56:18 +02:00
45 changed files with 697 additions and 264 deletions

View File

@@ -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
@@ -85,6 +87,20 @@ REVERSE_PROXY_AUTH=0
# 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,

View File

@@ -166,7 +166,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'
)

View File

@@ -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,
@@ -411,19 +413,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 +441,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(

View 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

View 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,
}

View File

@@ -27,7 +27,7 @@ def search_recipes(request, queryset, params):
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)),

View File

@@ -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():

View File

@@ -16,8 +16,10 @@ class Mealie(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, space=self.request.space)
# TODO parse times (given in PT2H3M )
@@ -30,6 +32,9 @@ class Mealie(Integration):
if not ingredients_added:
ingredients_added = True
if len(recipe_json['description'].strip()) > 500:
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)

View File

@@ -16,8 +16,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)
@@ -30,6 +32,9 @@ class NextcloudCookbook(Integration):
instruction=s
)
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']:

View File

@@ -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]
@@ -58,6 +58,9 @@ class Paprika(Integration):
instruction=instructions
)
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)
@@ -79,5 +82,5 @@ class Paprika(Integration):
if recipe_json.get("photo_data", None):
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
return recipe

View 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),
),
]

View 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),
),
]

View 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',
),
]

View File

@@ -1,3 +1,4 @@
import operator
import re
import uuid
from datetime import date, timedelta
@@ -9,6 +10,7 @@ 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 +54,21 @@ 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)
message = models.CharField(max_length=512, default='', blank=True)
max_recipes = models.IntegerField(default=0)
allow_files = models.BooleanField(default=True)
max_users = models.IntegerField(default=0)
demo = models.BooleanField(default=False)
def __str__(self):
return self.name
@@ -78,11 +83,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 +129,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
)
@@ -247,7 +252,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 +272,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 +286,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 +303,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)
@@ -323,7 +328,7 @@ class Ingredient(models.Model, PermissionModelMixin):
ordering = ['order', 'pk']
class Step(models.Model, PermissionModelMixin):
class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixin):
TEXT = 'TEXT'
TIME = 'TIME'
@@ -380,7 +385,7 @@ class NutritionInformation(models.Model, PermissionModelMixin):
return 'Nutrition'
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 +417,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 +451,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 +465,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 +500,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 +525,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 +548,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 +578,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,7 +596,7 @@ 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)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -608,9 +613,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 +645,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 +659,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)
@@ -682,7 +686,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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View 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

17
cookbook/static/css/app.min.css vendored Normal file
View File

@@ -0,0 +1,17 @@
.spinner-tandoor {
animation: rotation 3s infinite linear;
content: url("../assets/spinner.svg");
width: auto;
height: 20vh;
margin: 0;
padding: 0;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}

View File

@@ -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
}
@@ -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 {
@@ -10402,7 +10444,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 +10455,10 @@ 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
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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',
)

View File

@@ -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>
@@ -17,6 +19,7 @@
<div class="row">
<div class="col-6 offset-3">
<hr>
<form class="login" method="POST" action="{% url 'account_login' %}">
{% csrf_token %}
{{ form | crispy }}
@@ -25,12 +28,12 @@
<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"
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
{% endif %}
</form>
</div>

View File

@@ -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-6 offset-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 %}

View File

@@ -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-6 offset-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 %}

View File

@@ -5,9 +5,14 @@
{% block title %}{% trans "Sign Up Closed" %}{% endblock %}
{% block content %}
<h1>{% trans "Sign Up Closed" %}</h1>
<div class="row">
<div class="col-6 offset-3">
<hr>
<h1>{% trans "Sign Up Closed" %}</h1>
<p>{% trans "We are sorry, but the sign up is currently closed." %}</p>
<p>{% trans "We are sorry, but the sign up is currently closed." %}</p>
<a class="btn btn-primary" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
<a class="btn btn-primary" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
</div>
</div>
{% endblock %}

View File

@@ -31,6 +31,7 @@
<!-- Bootstrap 4 -->
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
<link href="{% static 'css/app.min.css' %}" rel="stylesheet">
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
@@ -65,9 +66,9 @@
<span class="navbar-toggler-icon"></span>
</button>
{% if request.user.is_authenticated and request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.svg' %}" alt="" style="height: 5vh;">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a>
{% endif %}
<div class="collapse navbar-collapse" id="navbarText">
@@ -139,7 +140,7 @@
{% page_help request.resolver_match.url_name as help_button %}
{% if help_button %}{{ help_button|safe }}{% endif %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings,view_history,view_system,docs_markdown' %}active{% endif %}">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_space,view_settings,view_history,view_system,docs_markdown' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i
class="fas fa-user-alt"></i> {{ user.get_user_name }}

View File

@@ -14,7 +14,12 @@
<br/>
<br/>
<div class="text-center">
<i class="fas fa-sync fa-spin fa-10x"></i>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<img class="spinner-tandoor"/>
{% else %}
<i class="fas fa-sync fa-spin fa-10x"></i>
{% endif %}
<br/>
<br/>

View File

@@ -35,7 +35,11 @@
<div v-if="!recipe" class="text-center">
<br/>
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<img class="spinner-tandoor"/>
{% else %}
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% endif %}
</div>
<div v-if="recipe">
@@ -668,7 +672,7 @@
this.recipe.steps[step].ingredients[id] = new_unit
},
addKeyword: function (tag) {
let new_keyword = {'label':tag,'name':tag}
let new_keyword = {'label': tag, 'name': tag}
this.recipe.keywords.push(new_keyword)
},
searchKeywords: function (query) {

View File

@@ -37,7 +37,11 @@
<template v-if="shopping_list !== undefined">
<div class="text-center" v-if="loading">
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<img class="spinner-tandoor"/>
{% else %}
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% endif %}
</div>
<div v-else-if="edit_mode">
<div class="row">

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
@@ -13,7 +14,9 @@
{% block content %}
<h3>{{ request.space.name }}</h3>
<h3>{{ request.space.name }} <small>{% if HOSTED %}
<a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>{% endif %}</small></h3>
<br/>
<div class="row">
@@ -90,18 +93,19 @@
</td>
<td>
{% if u.user != request.user %}
<div class="input-group mb-3">
<select v-model="users['{{ u.pk }}']" class="custom-select form-control" style="height: 44px">
<option>{% trans 'admin' %}</option>
<option>{% trans 'user' %}</option>
<option>{% trans 'guest' %}</option>
<option>{% trans 'remove' %}</option>
</select>
<span class="input-group-append">
<div class="input-group mb-3">
<select v-model="users['{{ u.pk }}']" class="custom-select form-control"
style="height: 44px">
<option value="admin">{% trans 'admin' %}</option>
<option value="user">{% trans 'user' %}</option>
<option value="guest">{% trans 'guest' %}</option>
<option value="remove">{% trans 'remove' %}</option>
</select>
<span class="input-group-append">
<a class="btn btn-warning"
:href="editUserUrl({{ u.pk }}, {{ u.space.pk }})" >{% trans 'Update' %}</a>
:href="editUserUrl({{ u.pk }}, {{ u.space.pk }})">{% trans 'Update' %}</a>
</span>
</div>
</div>
{% else %}
{% trans 'You cannot edit yourself.' %}
{% endif %}
@@ -115,6 +119,17 @@
</div>
</div>
<div class="row">
<div class="col col-md-12">
<h4>{% trans 'Invite Links' %}</h4>
{% render_table invite_links %}
</div>
</div>
<br/>
<br/>
<br/>
{% endblock %}
{% block script %}

View File

@@ -29,7 +29,7 @@
style="height:50%"
href="{% bookmarklet request %}"
title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}">
<img src="{% static 'assets/favicon-16x16.png' %}">{% trans 'Bookmark Me!' %} </a>
<img src="{% static 'assets/favicon-16x16.png' %}"> {% trans 'Bookmark Me!' %} </a>
</div>
<nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px">
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url"
@@ -156,7 +156,11 @@
<div v-if="loading" class="text-center">
<br/>
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<img class="spinner-tandoor"/>
{% else %}
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% endif %}
</div>
<!-- recipe preview before Import -->

View File

@@ -9,7 +9,7 @@ register = template.Library()
@register.simple_tag
def theme_url(request):
if not request.user.is_authenticated:
return static('themes/flatly.min.css')
return static('themes/tandoor.min.css')
themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
UserPreference.FLATLY: 'themes/flatly.min.css',

View File

@@ -48,7 +48,8 @@ urlpatterns = [
path('no-group', views.no_groups, name='view_no_group'),
path('no-space', views.no_space, name='view_no_space'),
path('no-perm', views.no_perm, name='view_no_perm'),
path('signup/<slug:token>', views.signup, name='view_signup'),
path('signup/<slug:token>', views.signup, name='view_signup'), # TODO deprecated with 0.16.2 remove at some point
path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'),
path('search/', views.search, name='view_search'),
path('search/v2/', views.search_v2, name='view_search_v2'),

View File

@@ -57,7 +57,6 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer)
from recipes.settings import DEMO
class StandardFilterMixin(ViewSetMixin):
@@ -390,7 +389,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
obj, data=request.data, partial=True
)
if DEMO:
if self.request.space.demo:
raise PermissionDenied(detail='Not available in demo', code=None)
if serializer.is_valid():
@@ -537,7 +536,7 @@ def get_recipe_file(request, recipe_id):
@group_required('user')
def sync_all(request):
if DEMO:
if request.space.demo:
messages.add_message(
request, messages.ERROR, _('This feature is not available in the demo version!')
)

View File

@@ -20,12 +20,20 @@ from cookbook.forms import BatchEditForm, SyncForm
from cookbook.helper.permission_helper import group_required, has_group_permission
from cookbook.helper.recipe_url_import import parse_cooktime
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe,
RecipeImport, Step, Sync, Unit)
RecipeImport, Step, Sync, Unit, UserPreference)
from cookbook.tables import SyncTable
@group_required('user')
def sync(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
if request.method == "POST":
if not has_group_permission(request.user, ['admin']):
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
@@ -109,6 +117,14 @@ def batch_edit(request):
@group_required('user')
@atomic
def import_url(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
if request.method == 'POST':
data = json.loads(request.body)
data['cookTime'] = parse_cooktime(data.get('cookTime', ''))
@@ -132,13 +148,11 @@ def import_url(request):
recipe.steps.add(step)
for kw in data['keywords']:
# if k := Keyword.objects.filter(name=kw['text'], space=request.space).first():
# recipe.keywords.add(k)
# elif data['all_keywords']:
# k = Keyword.objects.create(name=kw['text'], space=request.space)
# recipe.keywords.add(k)
k, created = Keyword.objects.get_or_create(name=kw['text'].strip(), space=request.space)
recipe.keywords.add(k)
if k := Keyword.objects.filter(name=kw['text'], space=request.space).first():
recipe.keywords.add(k)
elif data['all_keywords']:
k = Keyword.objects.create(name=kw['text'], space=request.space)
recipe.keywords.add(k)
for ing in data['recipeIngredient']:
ingredient = Ingredient()
@@ -200,6 +214,7 @@ def import_url(request):
return render(request, 'url_import.html', context)
class Object(object):
pass

View File

@@ -1,6 +1,7 @@
import os
from django.contrib import messages
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
@@ -45,7 +46,7 @@ def convert_recipe(request, pk):
@group_required('user')
def internal_recipe_update(request, pk):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() > request.space.max_recipes:
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() > request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
@@ -287,14 +288,15 @@ def edit_ingredients(request):
new_unit = units_form.cleaned_data['new_unit']
old_unit = units_form.cleaned_data['old_unit']
if new_unit != old_unit:
recipe_ingredients = Ingredient.objects.filter(unit=old_unit, step__recipe__space=request.space).all()
for i in recipe_ingredients:
i.unit = new_unit
i.save()
with scopes_disabled():
recipe_ingredients = Ingredient.objects.filter(unit=old_unit).filter(Q(step__recipe__space=request.space) | Q(step__recipe__isnull=True)).all()
for i in recipe_ingredients:
i.unit = new_unit
i.save()
old_unit.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
old_unit.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
else:
messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!'))
@@ -303,7 +305,7 @@ def edit_ingredients(request):
new_food = food_form.cleaned_data['new_food']
old_food = food_form.cleaned_data['old_food']
if new_food != old_food:
ingredients = Ingredient.objects.filter(food=old_food, step__recipe__space=request.space).all()
ingredients = Ingredient.objects.filter(food=old_food).filter(Q(step__recipe__space=request.space) | Q(step__recipe__isnull=True)).all()
for i in ingredients:
i.food = new_food
i.save()

View File

@@ -23,7 +23,7 @@ from cookbook.integration.recettetek import RecetteTek
from cookbook.integration.recipesage import RecipeSage
from cookbook.integration.rezkonv import RezKonv
from cookbook.integration.safron import Safron
from cookbook.models import Recipe, ImportLog
from cookbook.models import Recipe, ImportLog, UserPreference
def get_integration(request, export_type):
@@ -57,6 +57,14 @@ def get_integration(request, export_type):
@group_required('user')
def import_recipe(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
if request.method == "POST":
form = ImportForm(request.POST, request.FILES)
if form.is_valid():

View File

@@ -28,11 +28,11 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
fields = ('name',)
def form_valid(self, form):
if Recipe.objects.filter(space=self.request.space).count() >= self.request.space.max_recipes:
if self.request.space.max_recipes != 0 and Recipe.objects.filter(space=self.request.space).count() >= self.request.space.max_recipes: # TODO move to central helper function
messages.add_message(self.request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if UserPreference.objects.filter(space=self.request.space).count() > self.request.space.max_users:
if self.request.space.max_users != 0 and UserPreference.objects.filter(space=self.request.space).count() > self.request.space.max_users:
messages.add_message(self.request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
@@ -225,9 +225,9 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
if InviteLink.objects.filter(space=self.request.space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.request.user.username)
message += _(' to join their Tandoor Recipes space ') + escape(self.request.space.name) + '.\n\n'
message += _('Click the following link to activate your account: ') + self.request.build_absolute_uri(reverse('view_signup', args=[str(obj.uuid)])) + '\n\n'
message += _('Click the following link to activate your account: ') + self.request.build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n'
message += _('The invitation is valid until ') + obj.valid_until + '\n\n'
message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n'
message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
send_mail(
@@ -236,7 +236,6 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
None,
[obj.email],
fail_silently=False,
)
messages.add_message(self.request, messages.SUCCESS,
_('Invite link successfully send to user.'))
@@ -246,7 +245,7 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
except (SMTPException, BadHeaderError, TimeoutError):
messages.add_message(self.request, messages.ERROR, _('Email to user could not be send, please share link manually.'))
return HttpResponseRedirect(reverse('list_invite_link'))
return HttpResponseRedirect(reverse('view_space'))
def get_context_data(self, **kwargs):
context = super(InviteLinkCreate, self).get_context_data(**kwargs)

View File

@@ -3,6 +3,7 @@ import re
from datetime import datetime
from uuid import UUID
from allauth.account.forms import SignupForm
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
@@ -25,15 +26,14 @@ from rest_framework.authtoken.models import Token
from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm)
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, AllAuthSignupForm)
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable)
ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object
from recipes.settings import DEMO
from recipes.version import BUILD_REF, VERSION_NUMBER
@@ -126,7 +126,7 @@ def no_space(request):
return HttpResponseRedirect(reverse('index'))
if join_form.is_valid():
return HttpResponseRedirect(reverse('view_signup', args=[join_form.cleaned_data['token']]))
return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']]))
else:
if settings.SOCIAL_DEFAULT_ACCESS:
request.user.userpreference.space = Space.objects.first()
@@ -134,7 +134,7 @@ def no_space(request):
request.user.groups.add(Group.objects.get(name=settings.SOCIAL_DEFAULT_GROUP))
return HttpResponseRedirect(reverse('index'))
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_signup', args=[request.session.pop('signup_token', '')]))
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
create_form = SpaceCreateForm()
join_form = SpaceJoinForm()
@@ -284,7 +284,7 @@ def shopping_list(request, pk=None):
@group_required('guest')
def user_settings(request):
if DEMO:
if request.space.demo:
messages.add_message(request, messages.ERROR, _('This feature is not available in the demo version!'))
return redirect('index')
@@ -420,7 +420,7 @@ def setup(request):
return render(request, 'setup.html', {'form': form})
def signup(request, token):
def invite_link(request, token):
with scopes_disabled():
try:
token = UUID(token, version=4)
@@ -445,48 +445,16 @@ def signup(request, token):
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
return HttpResponseRedirect(reverse('index'))
else:
request.session['signup_token'] = token
request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup'))
if request.method == 'POST':
updated_request = request.POST.copy()
if link.username != '':
updated_request.update({'name': link.username})
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('index'))
form = UserCreateForm(updated_request)
if form.is_valid():
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: # noqa: E501
form.add_error('password', _('Passwords dont match!'))
else:
user = User(username=form.cleaned_data['name'], )
try:
validate_password(form.cleaned_data['password'], user=user)
user.set_password(form.cleaned_data['password'])
user.save()
messages.add_message(request, messages.SUCCESS, _('User has been created, please login!'))
link.used_by = user
link.save()
request.user.groups.clear()
user.groups.add(link.group)
user.userpreference.space = link.space
user.userpreference.save()
return HttpResponseRedirect(reverse('account_login'))
except ValidationError as e:
for m in e:
form.add_error('password', m)
else:
form = UserCreateForm()
if link.username != '':
form.fields['name'].initial = link.username
form.fields['name'].disabled = True
return render(request, 'account/signup.html', {'form': form, 'link': link})
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('index'))
# TODO deprecated with 0.16.2 remove at some point
def signup(request, token):
return HttpResponseRedirect(reverse('view_invite', args=[token]))
@group_required('admin')
@@ -506,7 +474,10 @@ def space(request):
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
return render(request, 'space.html', {'space_users': space_users, 'counts': counts})
invite_links = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
return render(request, 'space.html', {'space_users': space_users, 'counts': counts, 'invite_links': invite_links})
# TODO super hacky and quick solution, safe but needs rework

View File

@@ -27,7 +27,6 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
DEBUG = bool(int(os.getenv('DEBUG', True)))
DEMO = bool(int(os.getenv('DEMO', False)))
SOCIAL_DEFAULT_ACCESS = bool(int(os.getenv('SOCIAL_DEFAULT_ACCESS', False)))
SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
@@ -65,6 +64,17 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4'
DJANGO_TABLES2_TEMPLATE = 'cookbook/templates/generic/table_template.html'
DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
TERMS_URL = os.getenv('TERMS_URL', '')
PRIVACY_URL = os.getenv('PRIVACY_URL', '')
IMPRINT_URL = os.getenv('IMPRINT_URL', '')
HOSTED = bool(int(os.getenv('HOSTED', False)))
MESSAGE_TAGS = {
messages.ERROR: 'danger'
}
@@ -82,6 +92,7 @@ INSTALLED_APPS = [
'django.contrib.sites',
'django.contrib.staticfiles',
'django.contrib.postgres',
'django_prometheus',
'django_tables2',
'corsheaders',
'django_filters',
@@ -92,6 +103,7 @@ INSTALLED_APPS = [
'django_cleanup.apps.CleanupConfig',
'webpack_loader',
'django_js_reverse',
'hcaptcha',
'allauth',
'allauth.account',
'allauth.socialaccount',
@@ -101,11 +113,18 @@ INSTALLED_APPS = [
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True
ACCOUNT_MAX_EMAIL_ADDRESSES = 3
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
ENABLE_SIGNUP = bool(int(os.getenv('ENABLE_SIGNUP', False)))
ENABLE_METRICS = bool(int(os.getenv('ENABLE_METRICS', False)))
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
@@ -120,6 +139,9 @@ MIDDLEWARE = [
'cookbook.helper.scope_middleware.ScopeMiddleware',
]
if ENABLE_METRICS:
MIDDLEWARE += 'django_prometheus.middleware.PrometheusAfterMiddleware',
# Auth related settings
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
@@ -180,6 +202,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media',
'cookbook.helper.context_processors.context_settings',
],
},
},
@@ -301,12 +324,15 @@ STATIC_URL = os.getenv('STATIC_URL', '/static/')
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
if os.getenv('S3_ACCESS_KEY', ''):
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
DEFAULT_FILE_STORAGE = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
AWS_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY', '')
AWS_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', '')
AWS_STORAGE_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', '')
AWS_QUERYSTRING_AUTH = bool(int(os.getenv('S3_QUERYSTRING_AUTH', True)))
AWS_QUERYSTRING_EXPIRE = int(os.getenv('S3_QUERYSTRING_EXPIRE', 3600))
AWS_S3_SIGNATURE_VERSION = os.getenv('S3_SIGNATURE_VERSION', 's3v4')
AWS_S3_REGION_NAME = os.getenv('S3_REGION_NAME', None)
if os.getenv('S3_ENDPOINT_URL', ''):
AWS_S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', '')

View File

@@ -15,6 +15,7 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls import url
from django.contrib import admin
from django.urls import include, path, re_path
from django.views.i18n import JavaScriptCatalog
@@ -33,6 +34,9 @@ urlpatterns = [
),
]
if settings.ENABLE_METRICS:
urlpatterns += url('', include('django_prometheus.urls')),
if settings.GUNICORN_MEDIA or settings.DEBUG:
urlpatterns += re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
urlpatterns += re_path(r'^jsreverse.json$', reverse_views.urls_js, name='js_reverse'),

View File

@@ -37,4 +37,6 @@ pytest==6.2.4
pytest-django==4.3.0
django-cors-headers==3.7.0
django-storages==1.11.1
boto3==1.17.84
boto3==1.17.84
django-prometheus==2.1.0
django-hCaptcha==0.1.0

View File

@@ -23,7 +23,7 @@
<loading-spinner></loading-spinner>
<br/>
<br/>
<h5 style="text-align: center">{{ $t('import-running') }}</h5>
<h5 style="text-align: center">{{ $t('Importing') }}...</h5>
</template>
<template v-else>

View File

@@ -1,7 +1,7 @@
<template>
<div class="row">
<div class="col" style="text-align: center">
<i class="fas fa-spinner fa-spin fa-10x"></i>
<img class="spinner-tandoor" alt="loading spinner" src="" style="height: 30vh"/>
</div>
</div>
</template>