join and leave spaces

This commit is contained in:
vabene1111 2022-05-31 20:38:53 +02:00
parent b3fcfdfc96
commit 007b7294d9
12 changed files with 149 additions and 105 deletions

View File

@ -37,12 +37,9 @@ class UserPreferenceForm(forms.ModelForm):
prefix = 'preference' prefix = 'preference'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if x := kwargs.get('instance', None):
space = x.space
else:
space = kwargs.pop('space') space = kwargs.pop('space')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all() self.fields['plan_share'].queryset = User.objects.filter(userspace__space=space).all()
class Meta: class Meta:
model = UserPreference model = UserPreference

View File

@ -166,7 +166,7 @@ class OwnerRequiredMixin(object):
try: try:
obj = self.get_object() obj = self.get_object()
if obj.get_space() != request.space: if not request.user.userspace.filter(space=obj.get_space()).exists():
messages.add_message(request, messages.ERROR, messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!')) _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index')) return HttpResponseRedirect(reverse_lazy('index'))

View File

@ -26,15 +26,18 @@ class ScopeMiddleware:
if request.path.startswith(prefix + '/accounts/'): if request.path.startswith(prefix + '/accounts/'):
return self.get_response(request) return self.get_response(request)
if request.path.startswith(prefix + '/switch-space/'):
return self.get_response(request)
with scopes_disabled(): with scopes_disabled():
if request.user.userspace_set.count() == 0 and not reverse('account_logout') in request.path: if request.user.userspace_set.count() == 0 and not reverse('account_logout') in request.path:
return views.no_space(request) return views.space_overview(request)
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point) # get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
user_space = request.user.userspace_set.filter(active=True).first() user_space = request.user.userspace_set.filter(active=True).first()
if not user_space: if not user_space:
pass # TODO show space selection page (maybe include in no space page) return views.space_overview(request)
if user_space.groups.count() == 0 and not reverse('account_logout') in request.path: if user_space.groups.count() == 0 and not reverse('account_logout') in request.path:
return views.no_groups(request) return views.no_groups(request)

View File

@ -343,7 +343,7 @@ class UserPreference(models.Model, PermissionModelMixin):
return str(self.user) return str(self.user)
class UserSpace(models.Model): class UserSpace(models.Model, PermissionModelMixin):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE)
groups = models.ManyToManyField(Group) groups = models.ManyToManyField(Group)

View File

@ -1,3 +1,4 @@
import django_tables2 as tables import django_tables2 as tables
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _

View File

@ -304,7 +304,7 @@
<a class="dropdown-item" href="{% url 'admin:index' %}"><i <a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a> class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
{% endif %} {% endif %}
{% if request.user.is_authenticated and request.user.userspace_set.all|length > 1 %} {% if request.user.is_authenticated %}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<h6 class="dropdown-header">{% trans 'Your Spaces' %}</h6> <h6 class="dropdown-header">{% trans 'Your Spaces' %}</h6>
{% for us in request.user.userspace_set.all %} {% for us in request.user.userspace_set.all %}
@ -316,7 +316,7 @@
{% endif %} {% endif %}
{{ us.space.name }}</a> {{ us.space.name }}</a>
{% endfor %} {% endfor %}
<a class="dropdown-item" href="{% url 'view_space_overview' %}"><i class="fas fa-plus fa-fw"></i> {% trans 'Create New' %}</a>
{% endif %} {% endif %}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i <a class="dropdown-item" href="{% url 'docs_markdown' %}"><i

View File

@ -19,6 +19,7 @@
{% csrf_token %} {% csrf_token %}
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
{% blocktrans %}Are you sure you want to delete the {{ title }}: <b>{{ object }}</b> {% endblocktrans %} {% blocktrans %}Are you sure you want to delete the {{ title }}: <b>{{ object }}</b> {% endblocktrans %}
{% trans 'This cannot be undone!' %}
</div> </div>
{{ form|crispy }} {{ form|crispy }}

View File

@ -1,70 +0,0 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "No Space" %}{% endblock %}
{% block content %}
<div style="text-align: center">
<h3 class="">{% trans 'No Space' %}</h3>
<div class="row">
<div class="col col-md-12">
{% 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.' %}
</div>
</div>
<div class="row" style="margin-top: 2vh">
<div class="col col-md-12">
<div class="card-group">
<div class="card">
<div class="card-header">
{% trans 'Join Space' %}
</div>
<div class="card-body">
<h5 class="card-title">{% trans 'Join an existing space.' %}</h5>
<p class="card-text" style="height: 64px">{% trans 'To join an existing space either enter your invite token or click on the invite link the space owner send you.' %}</p>
<form method="POST" action="{% url 'view_no_space' %}">
{% csrf_token %}
{{ join_form | crispy }}
<input type="submit" class="btn btn-primary" value="{% trans 'Join Space' %}"/>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
{% trans 'Create Space' %}
</div>
<div class="card-body">
<h5 class="card-title">{% trans 'Create your own recipe space.' %}</h5>
<p class="card-text" style="height: 64px">{% trans 'Start your own recipe space and invite other users to it.' %}</p>
<form method="POST" action="{% url 'view_no_space' %}">
{% csrf_token %}
{{ create_form | crispy }}
<input type="submit" class="btn btn-primary" value="{% trans 'Create Space' %}"/>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,106 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "No Space" %}{% endblock %}
{% block content %}
<div style="text-align: center">
<h3 class="">{% trans 'Space' %}</h3>
<div class="row">
<div class="col col-md-12">
{% 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.' %}
</div>
</div>
{% if request.user.userspace_set.all|length > 0 %}
<div class="row mt-2">
<div class="col col-12">
<h5>{% trans 'Your Spaces' %}</h5>
</div>
</div>
<div class="row mt-2">
<div class="col col-12">
<div class="card-deck">
{% for us in request.user.userspace_set.all %}
<div class="card">
<div class="card-body">
<h5 class="card-title"><a
href="{% url 'view_switch_space' us.space.id %}">{{ us.space.name }}</a>
</h5>
<p class="card-text"><small
class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small>
{% if us.space.created_by != us.user %}
<p class="card-text"><small
class="text-muted"><a
href="{% url 'delete_user_space' us.pk %}">{% trans 'Leave Space' %}</a></small>
{% endif %}
<!--TODO add direct link to management page -->
</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="row" style="margin-top: 2vh">
<div class="col col-md-12">
<div class="card-group">
<div class="card">
<div class="card-header">
{% trans 'Join Space' %}
</div>
<div class="card-body">
<h5 class="card-title">{% trans 'Join an existing space.' %}</h5>
<p class="card-text"
style="height: 64px">{% trans 'To join an existing space either enter your invite token or click on the invite link the space owner send you.' %}</p>
<form method="POST" action="{% url 'view_space_overview' %}">
{% csrf_token %}
{{ join_form | crispy }}
<input type="submit" class="btn btn-primary" value="{% trans 'Join Space' %}"/>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
{% trans 'Create Space' %}
</div>
<div class="card-body">
<h5 class="card-title">{% trans 'Create your own recipe space.' %}</h5>
<p class="card-text"
style="height: 64px">{% trans 'Start your own recipe space and invite other users to it.' %}</p>
<form method="POST" action="{% url 'view_space_overview' %}">
{% csrf_token %}
{{ create_form | crispy }}
<input type="submit" class="btn btn-primary" value="{% trans 'Create Space' %}"/>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -12,7 +12,7 @@ from recipes.version import VERSION_NUMBER
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe, from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile,
get_model_name) get_model_name, UserSpace)
from .views import api, data, delete, edit, import_export, lists, new, telegram, views from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken from .views.api import CustomAuthToken
@ -55,7 +55,7 @@ urlpatterns = [
path('space/member/<int:user_id>/<int:space_id>/<slug:group>', views.space_change_member, path('space/member/<int:user_id>/<int:space_id>/<slug:group>', views.space_change_member,
name='change_space_member'), name='change_space_member'),
path('no-group', views.no_groups, name='view_no_group'), path('no-group', views.no_groups, name='view_no_group'),
path('no-space', views.no_space, name='view_no_space'), path('space-overview', views.space_overview, name='view_space_overview'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'), path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm', views.no_perm, name='view_no_perm'), path('no-perm', views.no_perm, name='view_no_perm'),
path('signup/<slug:token>', views.signup, name='view_signup'), # TODO deprecated with 0.16.2 remove at some point path('signup/<slug:token>', views.signup, name='view_signup'), # TODO deprecated with 0.16.2 remove at some point
@ -146,7 +146,7 @@ urlpatterns = [
generic_models = ( generic_models = (
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
Comment, RecipeBookEntry, ShoppingList, InviteLink Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace
) )
for m in generic_models: for m in generic_models:

View File

@ -9,7 +9,7 @@ from django.views.generic import DeleteView
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required
from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry, from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, Storage, Sync) RecipeImport, Storage, Sync, UserSpace)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -188,3 +188,14 @@ class InviteLinkDelete(OwnerRequiredMixin, DeleteView):
context = super(InviteLinkDelete, self).get_context_data(**kwargs) context = super(InviteLinkDelete, self).get_context_data(**kwargs)
context['title'] = _("Invite Link") context['title'] = _("Invite Link")
return context return context
class UserSpaceDelete(OwnerRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = UserSpace
success_url = reverse_lazy('view_space_overview')
def get_context_data(self, **kwargs):
context = super(UserSpaceDelete, self).get_context_data(**kwargs)
context['title'] = _("Space Membership")
return context

View File

@ -103,10 +103,7 @@ def no_groups(request):
@login_required @login_required
def no_space(request): def space_overview(request):
if request.user.userspace_set.count() > 0:
return HttpResponseRedirect(reverse('index'))
if request.POST: if request.POST:
create_form = SpaceCreateForm(request.POST, prefix='create') create_form = SpaceCreateForm(request.POST, prefix='create')
join_form = SpaceJoinForm(request.POST, prefix='join') join_form = SpaceJoinForm(request.POST, prefix='join')
@ -125,7 +122,7 @@ def no_space(request):
messages.add_message(request, messages.SUCCESS, 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.')) _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('view_switch_space', args=[user_space.pk]))
if join_form.is_valid(): if join_form.is_valid():
return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']])) return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']]))
@ -141,7 +138,7 @@ def no_space(request):
create_form = SpaceCreateForm(initial={'name': f'{request.user.username}\'s Space'}) create_form = SpaceCreateForm(initial={'name': f'{request.user.username}\'s Space'})
join_form = SpaceJoinForm() join_form = SpaceJoinForm()
return render(request, 'no_space_info.html', {'create_form': create_form, 'join_form': join_form}) return render(request, 'space_overview.html', {'create_form': create_form, 'join_form': join_form})
@login_required @login_required
@ -391,7 +388,7 @@ def user_settings(request):
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
up.save() up.save()
if up: if up:
preference_form = UserPreferenceForm(instance=up) preference_form = UserPreferenceForm(instance=up, space=request.space)
shopping_form = ShoppingPreferenceForm(instance=up) shopping_form = ShoppingPreferenceForm(instance=up)
else: else:
preference_form = UserPreferenceForm(space=request.space) preference_form = UserPreferenceForm(space=request.space)
@ -510,27 +507,25 @@ def invite_link(request, token):
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first(): if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.user.is_authenticated: 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'))
link.used_by = request.user link.used_by = request.user
link.save() link.save()
request.user.groups.clear()
request.user.groups.add(link.group)
request.user.userpreference.space = link.space user_space = UserSpace.objects.create(user=request.user, space=link.space, active=False)
request.user.userpreference.save()
if request.user.userspace_set.count() == 1:
user_space.active = True
user_space.save()
user_space.groups.add(link.group)
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.')) messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('view_space_overview'))
else: else:
request.session['signup_token'] = str(token) request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup')) return HttpResponseRedirect(reverse('account_signup'))
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!')) messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('view_space_overview'))
# TODO deprecated with 0.16.2 remove at some point # TODO deprecated with 0.16.2 remove at some point
@ -540,7 +535,7 @@ def signup(request, token):
@group_required('admin') @group_required('admin')
def space(request): def space(request):
space_users = UserPreference.objects.filter(space=request.space).all() space_users = UserSpace.objects.filter(space=request.space).all()
counts = Object() counts = Object()
counts.recipes = Recipe.objects.filter(space=request.space).count() counts.recipes = Recipe.objects.filter(space=request.space).count()