From c8054349b2ad91ee2114993ee2fca3afe2683f5b Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 28 May 2021 16:49:03 +0200 Subject: [PATCH] reworked invite system --- .env.template | 3 +- cookbook/forms.py | 63 +++++++-- cookbook/helper/AllAuthCustomAdapter.py | 2 +- cookbook/helper/scope_middleware.py | 3 + .../migrations/0122_auto_20210527_1712.py | 23 +++ cookbook/migrations/0123_invitelink_email.py | 18 +++ cookbook/models.py | 3 + .../templates/account/password_reset.html | 2 +- cookbook/templates/account/signup.html | 5 +- cookbook/templates/account/signup_closed.html | 14 ++ cookbook/templates/base.html | 3 + cookbook/templates/no_space_info.html | 56 +++++++- cookbook/templates/space.html | 93 +++++++++++++ cookbook/urls.py | 1 + cookbook/views/new.py | 41 +++++- cookbook/views/views.py | 131 +++++++++++++----- recipes/settings.py | 1 + 17 files changed, 410 insertions(+), 52 deletions(-) create mode 100644 cookbook/migrations/0122_auto_20210527_1712.py create mode 100644 cookbook/migrations/0123_invitelink_email.py create mode 100644 cookbook/templates/account/signup_closed.html create mode 100644 cookbook/templates/space.html diff --git a/.env.template b/.env.template index 5f4793fdd..745a5fd52 100644 --- a/.env.template +++ b/.env.template @@ -68,7 +68,8 @@ GUNICORN_MEDIA=0 # EMAIL_HOST_PASSWORD= # EMAIL_USE_TLS=0 # EMAIL_USE_SSL=0 -# ACCOUNT_EMAIL_SUBJECT_PREFIX +# DEFAULT_FROM_EMAIL= # email sender address (default 'webmaster@localhost') +# ACCOUNT_EMAIL_SUBJECT_PREFIX= # prefix used for account related emails (default "[Tandoor Recipes] ") # allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing # see docs for more information https://vabene1111.github.io/recipes/features/authentication/ diff --git a/cookbook/forms.py b/cookbook/forms.py index 7c40a1486..bf4675433 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -1,6 +1,8 @@ from django import forms +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 @@ -42,10 +44,15 @@ class UserPreferenceForm(forms.ModelForm): ) help_texts = { - 'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'), # noqa: E501 + 'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'), + # noqa: E501 'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'), # noqa: E501 - 'use_fractions': _('Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'), # noqa: E501 - 'plan_share': _('Users with whom newly created meal plan/shopping list entries should be shared by default.'), # noqa: E501 + 'use_fractions': _( + 'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'), + # noqa: E501 + 'plan_share': _( + 'Users with whom newly created meal plan/shopping list entries should be shared by default.'), + # noqa: E501 'show_recent': _('Show recently viewed recipes on search page.'), # noqa: E501 'ingredient_decimals': _('Number of decimals to round ingredients.'), # noqa: E501 'comments': _('If you want to be able to create and see comments underneath recipes.'), # noqa: E501 @@ -69,7 +76,8 @@ class UserNameForm(forms.ModelForm): fields = ('first_name', 'last_name') help_texts = { - 'first_name': _('Both fields are optional. If none are given the username will be displayed instead') # noqa: E501 + 'first_name': _('Both fields are optional. If none are given the username will be displayed instead') + # noqa: E501 } @@ -128,7 +136,9 @@ class ImportExportBase(forms.Form): class ImportForm(ImportExportBase): files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True})) - duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), required=False) + duplicates = forms.BooleanField(help_text=_( + 'To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), + required=False) class ExportForm(ImportExportBase): @@ -251,7 +261,8 @@ class StorageForm(forms.ModelForm): fields = ('name', 'method', 'username', 'password', 'token', 'url', 'path') help_texts = { - 'url': _('Leave empty for dropbox and enter only base url for nextcloud (/remote.php/webdav/ is added automatically)'), + 'url': _( + 'Leave empty for dropbox and enter only base url for nextcloud (/remote.php/webdav/ is added automatically)'), } @@ -366,7 +377,8 @@ class MealPlanForm(forms.ModelForm): help_texts = { 'shared': _('You can list default users to share recipes with in the settings.'), # noqa: E501 - 'note': _('You can use markdown to format this field. See the docs here') # noqa: E501 + 'note': _('You can use markdown to format this field. See the docs here') + # noqa: E501 } widgets = { @@ -387,17 +399,50 @@ class InviteLinkForm(forms.ModelForm): super().__init__(*args, **kwargs) self.fields['space'].queryset = Space.objects.filter(created_by=user).all() + def clean_email(self): + email = self.cleaned_data['email'] + with scopes_disabled(): + if User.objects.filter(email=email).exists(): + raise ValidationError(_('Email address already taken!')) + + return email + + def clean_username(self): + username = self.cleaned_data['username'] + with scopes_disabled(): + if 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', 'group', 'valid_until', 'space') + fields = ('username', 'email', 'group', 'valid_until', 'space') help_texts = { - 'username': _('A username is not required, if left blank the new user can choose one.') # noqa: E501 + '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.') } field_classes = { 'space': SafeModelChoiceField, } +class SpaceCreateForm(forms.Form): + prefix = 'create' + name = forms.CharField() + + def clean_name(self): + name = self.cleaned_data['name'] + with scopes_disabled(): + if Space.objects.filter(name=name).exists(): + raise ValidationError(_('Name already taken.')) + return name + + +class SpaceJoinForm(forms.Form): + prefix = 'join' + token = forms.CharField() + + class UserCreateForm(forms.Form): name = forms.CharField(label='Username') password = forms.CharField( diff --git a/cookbook/helper/AllAuthCustomAdapter.py b/cookbook/helper/AllAuthCustomAdapter.py index 1823265f0..47629283c 100644 --- a/cookbook/helper/AllAuthCustomAdapter.py +++ b/cookbook/helper/AllAuthCustomAdapter.py @@ -16,7 +16,7 @@ class AllAuthCustomAdapter(DefaultAccountAdapter): # disable password reset for now def send_mail(self, template_prefix, email, context): - if settings.EMAIL_HOST != '': + if settings.EMAIL_HOST == '': super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context) else: pass diff --git a/cookbook/helper/scope_middleware.py b/cookbook/helper/scope_middleware.py index 6b5191df0..c7de19dd4 100644 --- a/cookbook/helper/scope_middleware.py +++ b/cookbook/helper/scope_middleware.py @@ -16,6 +16,9 @@ class ScopeMiddleware: with scopes_disabled(): return self.get_response(request) + if request.path.startswith('/signup/'): + return self.get_response(request) + with scopes_disabled(): if request.user.userpreference.space is None and not reverse('account_logout') in request.path: return views.no_space(request) diff --git a/cookbook/migrations/0122_auto_20210527_1712.py b/cookbook/migrations/0122_auto_20210527_1712.py new file mode 100644 index 000000000..796225ca5 --- /dev/null +++ b/cookbook/migrations/0122_auto_20210527_1712.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.3 on 2021-05-27 15:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0121_auto_20210518_1638'), + ] + + operations = [ + migrations.AddField( + model_name='space', + name='allow_files', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='space', + name='max_users', + field=models.IntegerField(default=0), + ), + ] diff --git a/cookbook/migrations/0123_invitelink_email.py b/cookbook/migrations/0123_invitelink_email.py new file mode 100644 index 000000000..96d59b4e4 --- /dev/null +++ b/cookbook/migrations/0123_invitelink_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.3 on 2021-05-28 12:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0122_auto_20210527_1712'), + ] + + operations = [ + migrations.AddField( + model_name='invitelink', + name='email', + field=models.EmailField(blank=True, max_length=254), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 7bbda1b30..44ca3609a 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -62,6 +62,8 @@ class Space(models.Model): 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) def __str__(self): return self.name @@ -607,6 +609,7 @@ def default_valid_until(): class InviteLink(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) used_by = models.ForeignKey( diff --git a/cookbook/templates/account/password_reset.html b/cookbook/templates/account/password_reset.html index 74c6012b2..514a5aeef 100644 --- a/cookbook/templates/account/password_reset.html +++ b/cookbook/templates/account/password_reset.html @@ -19,7 +19,7 @@ {% csrf_token %} {{ form | crispy}} - {% trans "Sign In" %} + {% trans "Sign In" %} {% trans "Sign Up" %} diff --git a/cookbook/templates/account/signup.html b/cookbook/templates/account/signup.html index e774cfeb1..88e6a8ea0 100644 --- a/cookbook/templates/account/signup.html +++ b/cookbook/templates/account/signup.html @@ -11,8 +11,9 @@
{% csrf_token %} {{ form|crispy }} - +
+

{% trans 'Already have an account?' %} {% trans "Sign In" %}

+ {% endblock %} \ No newline at end of file diff --git a/cookbook/templates/account/signup_closed.html b/cookbook/templates/account/signup_closed.html new file mode 100644 index 000000000..2f48ea4d6 --- /dev/null +++ b/cookbook/templates/account/signup_closed.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} + +{% block content %} +

{% trans "Sign Up Closed" %}

+ +

{% trans "We are sorry, but the sign up is currently closed." %}

+ + {% trans "Sign In" %} + {% trans "Reset Password" %} +{% endblock %} \ No newline at end of file diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index b0b49cfb1..3ef3b4a8c 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -144,6 +144,9 @@ class="fas fa-user-cog fa-fw"> {% trans 'Settings' %} {% trans 'History' %} + {% if request.user == request.space.created_by %} + {% trans 'Space Settings' %} + {% endif %} {% if user.is_superuser %} -

{% trans 'No Space' %}

-
+

{% trans 'No Space' %}

- {% trans 'You are not a member of any space.' %} {% trans 'Please contact your administrator.' %}
+
+
+ {% trans 'Recipes, foods, shopping lists and more are organized in spaces of one or more people.' %} + {% trans 'You can either be invited into an existing space or create your own one.' %} +
+
+ + +
+
+ +
+ + +
+
+ {% trans 'Join Space' %} +
+
+
{% trans 'Join an existing space.' %}
+

{% trans 'To join an existing space either enter your invite token or click on the invite link the space owner send you.' %}

+ +
+ {% csrf_token %} + {{ join_form | crispy }} + +
+ +
+
+ +
+
+ {% trans 'Create Space' %} +
+
+
{% trans 'Create your own recipe space.' %}
+

{% trans 'Start your own recipe space and invite other users to it.' %}

+
+ {% csrf_token %} + {{ create_form | crispy }} + +
+
+
+ + +
+
+ +
diff --git a/cookbook/templates/space.html b/cookbook/templates/space.html new file mode 100644 index 000000000..f37d3fdc3 --- /dev/null +++ b/cookbook/templates/space.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} +{% load crispy_forms_filters %} +{% load static %} +{% load i18n %} + +{% block title %}{% trans "Space Settings" %}{% endblock %} + +{% block extra_head %} + {{ form.media }} +{% endblock %} + +{% block content %} + +

{{ request.space.name }}

+
+ +
+
+
+
+ {% trans 'Number of objects' %} +
+
    +
  • {% trans 'Recipes' %} : {{ counts.recipes }} / + {% if request.space.max_recipes > 0 %} + {{ request.space.max_recipes }}{% else %}∞{% endif %}
  • +
  • {% trans 'Keywords' %} : {{ counts.keywords }}
  • +
  • {% trans 'Units' %} : {{ counts.units }}
  • +
  • {% trans 'Ingredients' %} : {{ counts.ingredients }}
  • +
  • {% trans 'Recipe Imports' %} : {{ counts.recipe_import }}
  • +
+
+
+
+
+
+ {% trans 'Objects stats' %} +
+
    +
  • {% trans 'Recipes without Keywords' %} : {{ counts.recipes_no_keyword }}
  • +
  • {% trans 'External Recipes' %} : {{ counts.recipes_external }}
  • +
  • {% trans 'Internal Recipes' %} : {{ counts.recipes_internal }}
  • +
  • {% trans 'Comments' %} : {{ counts.comments }}
  • +
+
+
+
+
+
+
+ +
+
+ {% if space_users %} + + {% for u in space_users %} + + + + + {% endfor %} +
+ {{ u.user.username }} + + {% trans 'Remove' %} +
+ + {% else %} +

{% trans 'There are no members in your space yet!' %}

+ {% endif %} +
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/cookbook/urls.py b/cookbook/urls.py index d9ab12e83..cc2bff178 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -41,6 +41,7 @@ router.register(r'bookmarklet-import', api.BookmarkletImportViewSet) urlpatterns = [ path('', views.index, name='index'), path('setup/', views.setup, name='view_setup'), + path('space/', views.space, name='view_space'), 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'), diff --git a/cookbook/views/new.py b/cookbook/views/new.py index f9c790137..d5ce4a07d 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -1,7 +1,11 @@ import re -from datetime import datetime +from datetime import datetime, timedelta +from html import escape +from smtplib import SMTPException from django.contrib import messages +from django.contrib.auth.models import Group +from django.core.mail import send_mail, BadHeaderError from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy @@ -154,7 +158,8 @@ class MealPlanCreate(GroupRequiredMixin, CreateView, SpaceFormMixing): def get_form(self, form_class=None): form = self.form_class(**self.get_form_kwargs()) - form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user, space=self.request.space).all() + form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user, + space=self.request.space).all() return form def get_initial(self): @@ -207,6 +212,32 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView): obj.created_by = self.request.user obj.space = self.request.space obj.save() + if obj.email: + try: + 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 += _('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 += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/' + + send_mail( + _('Tandoor Recipes Invite'), + message, + None, + [obj.email], + fail_silently=False, + + ) + messages.add_message(self.request, messages.SUCCESS, + _('Invite link successfully send to user.')) + else: + messages.add_message(self.request, messages.ERROR, + _('You have send to many emails, please share the link manually or wait a few hours.')) + 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')) def get_context_data(self, **kwargs): @@ -218,3 +249,9 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView): kwargs = super().get_form_kwargs() kwargs.update({'user': self.request.user}) return kwargs + + def get_initial(self): + return dict( + space=self.request.space, + group=Group.objects.get(name='user') + ) diff --git a/cookbook/views/views.py b/cookbook/views/views.py index efb160e33..569a7ae1a 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -6,6 +6,7 @@ from uuid import UUID from django.conf import settings from django.contrib import messages from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.models import Group from django.contrib.auth.password_validation import validate_password @@ -24,12 +25,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) + UserPreferenceForm, SpaceJoinForm, SpaceCreateForm) 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) + RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit, + Food) from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall, ViewLogTable) +from cookbook.views.data import Object from recipes.settings import DEMO from recipes.version import BUILD_REF, VERSION_NUMBER @@ -99,13 +102,38 @@ def no_groups(request): return render(request, 'no_groups_info.html') +@login_required def no_space(request): - if settings.SOCIAL_DEFAULT_ACCESS: - request.user.userpreference.space = Space.objects.first() - request.user.userpreference.save() - request.user.groups.add(Group.objects.get(name=settings.SOCIAL_DEFAULT_GROUP)) + if request.user.userpreference.space: return HttpResponseRedirect(reverse('index')) - return render(request, 'no_space_info.html') + + if request.POST: + create_form = SpaceCreateForm(request.POST, prefix='create') + join_form = SpaceJoinForm(request.POST, prefix='join') + if create_form.is_valid(): + created_space = Space.objects.create(name=create_form.cleaned_data['name'], created_by=request.user) + request.user.userpreference.space = created_space + request.user.userpreference.save() + request.user.groups.add(Group.objects.filter(name='admin').get()) + + messages.add_message(request, messages.SUCCESS, _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.')) + return HttpResponseRedirect(reverse('index')) + + if join_form.is_valid(): + return HttpResponseRedirect(reverse('view_signup', args=[join_form.cleaned_data['token']])) + else: + if settings.SOCIAL_DEFAULT_ACCESS: + request.user.userpreference.space = Space.objects.first() + request.user.userpreference.save() + 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', '')])) + + create_form = SpaceCreateForm() + join_form = SpaceJoinForm() + + return render(request, 'no_space_info.html', {'create_form': create_form, 'join_form': join_form}) def no_perm(request): @@ -390,36 +418,53 @@ def signup(request, token): return HttpResponseRedirect(reverse('index')) if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first(): - if request.method == 'POST': - updated_request = request.POST.copy() - if link.username != '': - updated_request.update({'name': link.username}) + if request.user.is_authenticated: + if request.user.userpreference.space: + messages.add_message(request, messages.WARNING, _('You are already member of a space and therefore cannot join this one.')) + return HttpResponseRedirect(reverse('index')) - form = UserCreateForm(updated_request) + link.used_by = request.user + link.save() + request.user.groups.add(link.group) - 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!')) + request.user.userpreference.space = link.space + request.user.userpreference.save() - link.used_by = user - link.save() - 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) + messages.add_message(request, messages.SUCCESS, _('Successfully joined space.')) + return HttpResponseRedirect(reverse('index')) else: - form = UserCreateForm() + request.session['signup_token'] = token + + if request.method == 'POST': + updated_request = request.POST.copy() + if link.username != '': + updated_request.update({'name': link.username}) + + 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() + 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 @@ -430,6 +475,26 @@ def signup(request, token): return HttpResponseRedirect(reverse('index')) +@group_required('admin') +def space(request): + space_users = UserPreference.objects.filter(space=request.space).all() + + counts = Object() + counts.recipes = Recipe.objects.filter(space=request.space).count() + counts.keywords = Keyword.objects.filter(space=request.space).count() + counts.recipe_import = RecipeImport.objects.filter(space=request.space).count() + counts.units = Unit.objects.filter(space=request.space).count() + counts.ingredients = Food.objects.filter(space=request.space).count() + counts.comments = Comment.objects.filter(recipe__space=request.space).count() + + counts.recipes_internal = Recipe.objects.filter(internal=True, space=request.space).count() + counts.recipes_external = counts.recipes - counts.recipes_internal + + counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count() + + return render(request, 'space.html', {'space_users': space_users, 'counts': counts}) + + def markdown_info(request): return render(request, 'markdown_info.html', {}) diff --git a/recipes/settings.py b/recipes/settings.py index 7a07ff96f..1d9ecff6d 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -330,4 +330,5 @@ EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False))) EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False))) +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost') ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix