baiscs of space edit page
This commit is contained in:
parent
007b7294d9
commit
ded092ed23
@ -9,7 +9,7 @@ from django.utils.translation import gettext as _
|
|||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
|
|
||||||
from cookbook.models import ShareLink, Recipe, UserPreference
|
from cookbook.models import ShareLink, Recipe, UserPreference, UserSpace
|
||||||
|
|
||||||
|
|
||||||
def get_allowed_groups(groups_required):
|
def get_allowed_groups(groups_required):
|
||||||
@ -53,7 +53,6 @@ def is_object_owner(user, obj):
|
|||||||
Tests if a given user is the owner of a given object
|
Tests if a given user is the owner of a given object
|
||||||
test performed by checking user against the objects user
|
test performed by checking user against the objects user
|
||||||
and create_by field (if exists)
|
and create_by field (if exists)
|
||||||
superusers bypass all checks, unauthenticated users cannot own anything
|
|
||||||
:param user django auth user object
|
:param user django auth user object
|
||||||
:param obj any object that should be tested
|
:param obj any object that should be tested
|
||||||
:return: true if user is owner of object, false otherwise
|
:return: true if user is owner of object, false otherwise
|
||||||
@ -66,11 +65,25 @@ def is_object_owner(user, obj):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_space_owner(user, obj):
|
||||||
|
"""
|
||||||
|
Tests if a given user is the owner the space of a given object
|
||||||
|
:param user django auth user object
|
||||||
|
:param obj any object that should be tested
|
||||||
|
:return: true if user is owner of the objects space, false otherwise
|
||||||
|
"""
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return obj.get_space().get_owner() == user
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_object_shared(user, obj):
|
def is_object_shared(user, obj):
|
||||||
"""
|
"""
|
||||||
Tests if a given user is shared for a given object
|
Tests if a given user is shared for a given object
|
||||||
test performed by checking user against the objects shared table
|
test performed by checking user against the objects shared table
|
||||||
superusers bypass all checks, unauthenticated users cannot own anything
|
|
||||||
:param user django auth user object
|
:param user django auth user object
|
||||||
:param obj any object that should be tested
|
:param obj any object that should be tested
|
||||||
:return: true if user is shared for object, false otherwise
|
:return: true if user is shared for object, false otherwise
|
||||||
@ -184,7 +197,7 @@ class CustomIsOwner(permissions.BasePermission):
|
|||||||
verifies user has ownership over object
|
verifies user has ownership over object
|
||||||
(either user or created_by or user is request user)
|
(either user or created_by or user is request user)
|
||||||
"""
|
"""
|
||||||
message = _('You cannot interact with this object as it is not owned by you!') # noqa: E501
|
message = _('You cannot interact with this object as it is not owned by you!')
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
return request.user.is_authenticated
|
return request.user.is_authenticated
|
||||||
@ -193,6 +206,20 @@ class CustomIsOwner(permissions.BasePermission):
|
|||||||
return is_object_owner(request.user, obj)
|
return is_object_owner(request.user, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomIsSpaceOwner(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
Custom permission class for django rest framework views
|
||||||
|
verifies if the user is the owner of the space the object belongs to
|
||||||
|
"""
|
||||||
|
message = _('You cannot interact with this object as it is not owned by you!')
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return request.user.is_authenticated
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return is_space_owner(request.user, obj)
|
||||||
|
|
||||||
|
|
||||||
# TODO function duplicate/too similar name
|
# TODO function duplicate/too similar name
|
||||||
class CustomIsShared(permissions.BasePermission):
|
class CustomIsShared(permissions.BasePermission):
|
||||||
"""
|
"""
|
||||||
@ -293,7 +320,19 @@ def above_space_user_limit(space):
|
|||||||
:param space: Space to test for limits
|
:param space: Space to test for limits
|
||||||
:return: Tuple (True if above or equal limit else false, message)
|
:return: Tuple (True if above or equal limit else false, message)
|
||||||
"""
|
"""
|
||||||
limit = space.max_users != 0 and UserPreference.objects.filter(space=space).count() > space.max_users
|
limit = space.max_users != 0 and UserSpace.objects.filter(space=space).count() > space.max_users
|
||||||
if limit:
|
if limit:
|
||||||
return True, _('You have more users than allowed in your space.')
|
return True, _('You have more users than allowed in your space.')
|
||||||
return False, ''
|
return False, ''
|
||||||
|
|
||||||
|
|
||||||
|
def switch_user_active_space(user, user_space):
|
||||||
|
"""
|
||||||
|
Switch the currently active space of a user by setting all spaces to inactive and activating the one passed
|
||||||
|
:param user: user to change active space for
|
||||||
|
:param user_space: user space object to activate
|
||||||
|
"""
|
||||||
|
if not user_space.active:
|
||||||
|
UserSpace.objects.filter(user=user).update(active=False) # make sure to deactivate all spaces for a user
|
||||||
|
user_space.active = True
|
||||||
|
user_space.save()
|
||||||
|
@ -241,6 +241,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
|||||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||||
show_facet_count = models.BooleanField(default=False)
|
show_facet_count = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
return self.created_by
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ from datetime import timedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User, Group
|
||||||
from django.db.models import Avg, Q, QuerySet, Sum
|
from django.db.models import Avg, Q, QuerySet, Sum
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -20,7 +20,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
|
|||||||
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
|
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
|
||||||
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
|
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
|
||||||
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
|
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
|
||||||
UserFile, UserPreference, ViewLog)
|
UserFile, UserPreference, ViewLog, Space, UserSpace)
|
||||||
from cookbook.templatetags.custom_tags import markdown
|
from cookbook.templatetags.custom_tags import markdown
|
||||||
from recipes.settings import MEDIA_URL, AWS_ENABLED
|
from recipes.settings import MEDIA_URL, AWS_ENABLED
|
||||||
|
|
||||||
@ -123,6 +123,65 @@ class SpaceFilterSerializer(serializers.ListSerializer):
|
|||||||
return super().to_representation(data)
|
return super().to_representation(data)
|
||||||
|
|
||||||
|
|
||||||
|
class UserNameSerializer(WritableNestedModelSerializer):
|
||||||
|
username = serializers.SerializerMethodField('get_user_label')
|
||||||
|
|
||||||
|
def get_user_label(self, obj):
|
||||||
|
return obj.get_user_name()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
list_serializer_class = SpaceFilterSerializer
|
||||||
|
model = User
|
||||||
|
fields = ('id', 'username')
|
||||||
|
|
||||||
|
|
||||||
|
class GroupSerializer(WritableNestedModelSerializer):
|
||||||
|
def create(self, validated_data):
|
||||||
|
raise ValidationError('Cannot create using this endpoint')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Group
|
||||||
|
fields = ('id', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceSerializer(serializers.ModelSerializer):
|
||||||
|
user_count = serializers.SerializerMethodField('get_user_count')
|
||||||
|
recipe_count = serializers.SerializerMethodField('get_recipe_count')
|
||||||
|
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
|
||||||
|
|
||||||
|
def get_user_count(self, obj):
|
||||||
|
return UserSpace.objects.filter(space=obj).count()
|
||||||
|
|
||||||
|
def get_recipe_count(self, obj):
|
||||||
|
return Recipe.objects.filter(space=obj).count()
|
||||||
|
|
||||||
|
def get_file_size_mb(self, obj):
|
||||||
|
try:
|
||||||
|
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
|
||||||
|
except TypeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
raise ValidationError('Cannot create using this endpoint')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Space
|
||||||
|
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',)
|
||||||
|
read_only_fields = ('id', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSpaceSerializer(serializers.ModelSerializer):
|
||||||
|
user = UserNameSerializer(read_only=True)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
raise ValidationError('Cannot create using this endpoint')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserSpace
|
||||||
|
fields = ('id', 'user', 'space', 'groups', 'created_at', 'updated_at',)
|
||||||
|
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
|
||||||
|
|
||||||
|
|
||||||
class SpacedModelSerializer(serializers.ModelSerializer):
|
class SpacedModelSerializer(serializers.ModelSerializer):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
@ -142,18 +201,6 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
|||||||
read_only_fields = ('created_by',)
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
class UserNameSerializer(WritableNestedModelSerializer):
|
|
||||||
username = serializers.SerializerMethodField('get_user_label')
|
|
||||||
|
|
||||||
def get_user_label(self, obj):
|
|
||||||
return obj.get_user_name()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
list_serializer_class = SpaceFilterSerializer
|
|
||||||
model = User
|
|
||||||
fields = ('id', 'username')
|
|
||||||
|
|
||||||
|
|
||||||
class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
||||||
name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
|
name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
|
||||||
field = serializers.CharField(allow_null=True, allow_blank=True, required=False)
|
field = serializers.CharField(allow_null=True, allow_blank=True, required=False)
|
||||||
|
@ -346,7 +346,7 @@
|
|||||||
|
|
||||||
{% message_of_the_day as message_of_the_day %}
|
{% message_of_the_day as message_of_the_day %}
|
||||||
{% if message_of_the_day %}
|
{% if message_of_the_day %}
|
||||||
<div class="bg-warning" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
|
<div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
|
||||||
{{ message_of_the_day }}
|
{{ message_of_the_day }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
55
cookbook/templates/space_manage.html
Normal file
55
cookbook/templates/space_manage.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load render_bundle from webpack_loader %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans 'Search' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-12">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-12">
|
||||||
|
<h3>
|
||||||
|
<span class="text-muted">{% trans 'Space:' %}</span> {{ request.space.name }}
|
||||||
|
<small>{% if HOSTED %} <a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>
|
||||||
|
{% endif %}</small>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<space-manage-view></space-manage-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{% if debug %}
|
||||||
|
<script src="{% url 'js_reverse' %}"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||||
|
window.ACTIVE_SPACE_ID = {{ request.space.id }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% render_bundle 'space_manage_view' %}
|
||||||
|
{% endblock %}
|
@ -25,6 +25,7 @@ router.register(r'food', api.FoodViewSet)
|
|||||||
router.register(r'food-inherit-field', api.FoodInheritFieldViewSet)
|
router.register(r'food-inherit-field', api.FoodInheritFieldViewSet)
|
||||||
router.register(r'import-log', api.ImportLogViewSet)
|
router.register(r'import-log', api.ImportLogViewSet)
|
||||||
router.register(r'export-log', api.ExportLogViewSet)
|
router.register(r'export-log', api.ExportLogViewSet)
|
||||||
|
router.register(r'group', api.GroupViewSet)
|
||||||
router.register(r'ingredient', api.IngredientViewSet)
|
router.register(r'ingredient', api.IngredientViewSet)
|
||||||
router.register(r'keyword', api.KeywordViewSet)
|
router.register(r'keyword', api.KeywordViewSet)
|
||||||
router.register(r'meal-plan', api.MealPlanViewSet)
|
router.register(r'meal-plan', api.MealPlanViewSet)
|
||||||
@ -35,6 +36,7 @@ router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
|
|||||||
router.register(r'shopping-list', api.ShoppingListViewSet)
|
router.register(r'shopping-list', api.ShoppingListViewSet)
|
||||||
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
|
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
|
||||||
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
|
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
|
||||||
|
router.register(r'space', api.SpaceViewSet)
|
||||||
router.register(r'step', api.StepViewSet)
|
router.register(r'step', api.StepViewSet)
|
||||||
router.register(r'storage', api.StorageViewSet)
|
router.register(r'storage', api.StorageViewSet)
|
||||||
router.register(r'supermarket', api.SupermarketViewSet)
|
router.register(r'supermarket', api.SupermarketViewSet)
|
||||||
@ -46,6 +48,7 @@ router.register(r'unit', api.UnitViewSet)
|
|||||||
router.register(r'user-file', api.UserFileViewSet)
|
router.register(r'user-file', api.UserFileViewSet)
|
||||||
router.register(r'user-name', api.UserNameViewSet, basename='username')
|
router.register(r'user-name', api.UserNameViewSet, basename='username')
|
||||||
router.register(r'user-preference', api.UserPreferenceViewSet)
|
router.register(r'user-preference', api.UserPreferenceViewSet)
|
||||||
|
router.register(r'user-space', api.UserSpaceViewSet)
|
||||||
router.register(r'view-log', api.ViewLogViewSet)
|
router.register(r'view-log', api.ViewLogViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -56,6 +59,7 @@ urlpatterns = [
|
|||||||
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('space-overview', views.space_overview, name='view_space_overview'),
|
path('space-overview', views.space_overview, name='view_space_overview'),
|
||||||
|
path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'),
|
||||||
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
|
||||||
|
@ -11,7 +11,7 @@ from PIL import UnidentifiedImageError
|
|||||||
from annoying.decorators import ajax_request
|
from annoying.decorators import ajax_request
|
||||||
from annoying.functions import get_object_or_None
|
from annoying.functions import get_object_or_None
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User, Group
|
||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
@ -29,26 +29,22 @@ from requests.exceptions import MissingSchema
|
|||||||
from rest_framework import decorators, status, viewsets
|
from rest_framework import decorators, status, viewsets
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.authtoken.views import ObtainAuthToken
|
from rest_framework.authtoken.views import ObtainAuthToken
|
||||||
from rest_framework.decorators import api_view, permission_classes, schema
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.exceptions import APIException, PermissionDenied
|
from rest_framework.exceptions import APIException, PermissionDenied
|
||||||
from rest_framework.generics import CreateAPIView
|
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
|
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.schemas import AutoSchema
|
|
||||||
from rest_framework.throttling import AnonRateThrottle
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.viewsets import ViewSetMixin
|
from rest_framework.viewsets import ViewSetMixin
|
||||||
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
|
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
|
||||||
from validators import ValidationFailure
|
|
||||||
|
|
||||||
from cookbook.helper.HelperFunctions import str2bool
|
from cookbook.helper.HelperFunctions import str2bool
|
||||||
from cookbook.helper.image_processing import handle_image
|
from cookbook.helper.image_processing import handle_image
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
|
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
|
||||||
CustomIsShare, CustomIsShared, CustomIsUser,
|
CustomIsShare, CustomIsShared, CustomIsUser,
|
||||||
group_required)
|
group_required, CustomIsSpaceOwner)
|
||||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
|
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
|
||||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||||
@ -57,7 +53,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
|
|||||||
Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
|
Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
|
||||||
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
|
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
|
||||||
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
|
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
|
||||||
UserFile, UserPreference, ViewLog)
|
UserFile, UserPreference, ViewLog, Space, 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
|
||||||
@ -78,7 +74,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
|
|||||||
SupermarketCategorySerializer, SupermarketSerializer,
|
SupermarketCategorySerializer, SupermarketSerializer,
|
||||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||||
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
||||||
ViewLogSerializer, IngredientSimpleSerializer, BookmarkletImportListSerializer, RecipeFromSourceSerializer)
|
ViewLogSerializer, IngredientSimpleSerializer, BookmarkletImportListSerializer, RecipeFromSourceSerializer, SpaceSerializer, UserSpaceSerializer, GroupSerializer)
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
|
|
||||||
@ -369,6 +365,33 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class GroupViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Group.objects.all()
|
||||||
|
serializer_class = GroupSerializer
|
||||||
|
permission_classes = [CustomIsAdmin]
|
||||||
|
http_method_names = ['get', ]
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Space.objects
|
||||||
|
serializer_class = SpaceSerializer
|
||||||
|
permission_classes = [CustomIsOwner]
|
||||||
|
http_method_names = ['get', 'patch']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.queryset.filter(id=self.request.space.id)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = UserSpace.objects
|
||||||
|
serializer_class = UserSpaceSerializer
|
||||||
|
permission_classes = [CustomIsSpaceOwner]
|
||||||
|
http_method_names = ['get', 'patch', 'delete']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.queryset.filter(space=self.request.space)
|
||||||
|
|
||||||
|
|
||||||
class UserPreferenceViewSet(viewsets.ModelViewSet):
|
class UserPreferenceViewSet(viewsets.ModelViewSet):
|
||||||
queryset = UserPreference.objects
|
queryset = UserPreference.objects
|
||||||
serializer_class = UserPreferenceSerializer
|
serializer_class = UserPreferenceSerializer
|
||||||
|
@ -26,7 +26,7 @@ from cookbook.filters import RecipeFilter
|
|||||||
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
|
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
|
||||||
SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User,
|
SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User,
|
||||||
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
|
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
|
||||||
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid
|
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space
|
||||||
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword,
|
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword,
|
||||||
MealPlan, RecipeImport, SearchFields, SearchPreference, ShareLink,
|
MealPlan, RecipeImport, SearchFields, SearchPreference, ShareLink,
|
||||||
Space, Unit, ViewLog, UserSpace)
|
Space, Unit, ViewLog, UserSpace)
|
||||||
@ -144,9 +144,7 @@ def space_overview(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def switch_space(request, space_id):
|
def switch_space(request, space_id):
|
||||||
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
|
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
|
||||||
UserSpace.objects.filter(user=request.user).update(active=False) # make sure to deactivate all spaces for a user
|
switch_user_active_space(request.user, user_space)
|
||||||
user_space.active = True
|
|
||||||
user_space.save()
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
|
|
||||||
@ -533,6 +531,12 @@ def signup(request, token):
|
|||||||
return HttpResponseRedirect(reverse('view_invite', args=[token]))
|
return HttpResponseRedirect(reverse('view_invite', args=[token]))
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('admin')
|
||||||
|
def space_manage(request, space_id):
|
||||||
|
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
|
||||||
|
switch_user_active_space(request.user, user_space)
|
||||||
|
return render(request, 'space_manage.html', {})
|
||||||
|
|
||||||
@group_required('admin')
|
@group_required('admin')
|
||||||
def space(request):
|
def space(request):
|
||||||
space_users = UserSpace.objects.filter(space=request.space).all()
|
space_users = UserSpace.objects.filter(space=request.space).all()
|
||||||
|
90
vue/src/apps/SpaceManageView/SpaceManageView.vue
Normal file
90
vue/src/apps/SpaceManageView/SpaceManageView.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col col-12">
|
||||||
|
<div v-if="space !== undefined">
|
||||||
|
Recipes {{ space.recipe_count }} / {{ space.max_recipes }}
|
||||||
|
Users {{ space.user_count }} / {{ space.max_users }}
|
||||||
|
Files {{ space.file_size_mb }} / {{ space.max_file_storage_mb }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col col-12">
|
||||||
|
<div v-if="user_spaces !== undefined">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('User') }}</th>
|
||||||
|
<th>{{ $t('Groups') }}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr v-for="us in user_spaces" :key="us.id">
|
||||||
|
<td>{{ us.user.username }}</td>
|
||||||
|
<td>
|
||||||
|
<generic-multiselect
|
||||||
|
class="input-group-text m-0 p-0"
|
||||||
|
@change="us.groups = $event.val"
|
||||||
|
label="name"
|
||||||
|
:initial_selection="us.groups"
|
||||||
|
:model="Models.GROUP"
|
||||||
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
|
:limit="10"
|
||||||
|
:multiple="true"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button @click="alert('')">{{ $t('Delete') }}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from "vue"
|
||||||
|
import {BootstrapVue} from "bootstrap-vue"
|
||||||
|
|
||||||
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
|
|
||||||
|
import {ApiMixin, ResolveUrlMixin, ToastMixin} from "@/utils/utils"
|
||||||
|
|
||||||
|
import {ApiApiFactory} from "@/utils/openapi/api.ts"
|
||||||
|
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||||
|
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SupermarketView",
|
||||||
|
mixins: [ResolveUrlMixin, ToastMixin, ApiMixin],
|
||||||
|
components: {GenericMultiselect},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
space: undefined,
|
||||||
|
user_spaces: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
|
|
||||||
|
let apiFactory = new ApiApiFactory()
|
||||||
|
apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
|
||||||
|
this.space = r.data
|
||||||
|
})
|
||||||
|
apiFactory.listUserSpaces().then(r => {
|
||||||
|
this.user_spaces = r.data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
18
vue/src/apps/SpaceManageView/main.js
Normal file
18
vue/src/apps/SpaceManageView/main.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './SpaceManageView.vue'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||||
|
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
publicPath = 'http://localhost:8080/'
|
||||||
|
}
|
||||||
|
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||||
|
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
i18n,
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
@ -23,7 +23,7 @@ export class Models {
|
|||||||
false: undefined,
|
false: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tree: { default: undefined },
|
tree: {default: undefined},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
@ -50,7 +50,7 @@ export class Models {
|
|||||||
type: "lookup",
|
type: "lookup",
|
||||||
field: "target",
|
field: "target",
|
||||||
list: "self",
|
list: "self",
|
||||||
sticky_options: [{ id: 0, name: "tree_root" }],
|
sticky_options: [{id: 0, name: "tree_root"}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -71,7 +71,7 @@ export class Models {
|
|||||||
food_onhand: true,
|
food_onhand: true,
|
||||||
shopping: true,
|
shopping: true,
|
||||||
},
|
},
|
||||||
tags: [{ field: "supermarket_category", label: "name", color: "info" }],
|
tags: [{field: "supermarket_category", label: "name", color: "info"}],
|
||||||
// REQUIRED: unordered array of fields that can be set during create
|
// REQUIRED: unordered array of fields that can be set during create
|
||||||
create: {
|
create: {
|
||||||
// if not defined partialUpdate will use the same parameters, prepending 'id'
|
// if not defined partialUpdate will use the same parameters, prepending 'id'
|
||||||
@ -159,7 +159,7 @@ export class Models {
|
|||||||
field: "substitute_siblings",
|
field: "substitute_siblings",
|
||||||
label: "substitute_siblings", // form.label always translated in utils.getForm()
|
label: "substitute_siblings", // form.label always translated in utils.getForm()
|
||||||
help_text: "substitute_siblings_help", // form.help_text always translated
|
help_text: "substitute_siblings_help", // form.help_text always translated
|
||||||
condition: { field: "parent", value: true, condition: "field_exists" },
|
condition: {field: "parent", value: true, condition: "field_exists"},
|
||||||
},
|
},
|
||||||
substitute_children: {
|
substitute_children: {
|
||||||
form_field: true,
|
form_field: true,
|
||||||
@ -168,7 +168,7 @@ export class Models {
|
|||||||
field: "substitute_children",
|
field: "substitute_children",
|
||||||
label: "substitute_children",
|
label: "substitute_children",
|
||||||
help_text: "substitute_children_help",
|
help_text: "substitute_children_help",
|
||||||
condition: { field: "numchild", value: 0, condition: "gt" },
|
condition: {field: "numchild", value: 0, condition: "gt"},
|
||||||
},
|
},
|
||||||
inherit_fields: {
|
inherit_fields: {
|
||||||
form_field: true,
|
form_field: true,
|
||||||
@ -178,7 +178,7 @@ export class Models {
|
|||||||
field: "inherit_fields",
|
field: "inherit_fields",
|
||||||
list: "FOOD_INHERIT_FIELDS",
|
list: "FOOD_INHERIT_FIELDS",
|
||||||
label: "InheritFields",
|
label: "InheritFields",
|
||||||
condition: { field: "food_children_exist", value: true, condition: "preference_equals" },
|
condition: {field: "food_children_exist", value: true, condition: "preference_equals"},
|
||||||
help_text: "InheritFields_help",
|
help_text: "InheritFields_help",
|
||||||
},
|
},
|
||||||
child_inherit_fields: {
|
child_inherit_fields: {
|
||||||
@ -189,7 +189,7 @@ export class Models {
|
|||||||
field: "child_inherit_fields",
|
field: "child_inherit_fields",
|
||||||
list: "FOOD_INHERIT_FIELDS",
|
list: "FOOD_INHERIT_FIELDS",
|
||||||
label: "ChildInheritFields", // form.label always translated in utils.getForm()
|
label: "ChildInheritFields", // form.label always translated in utils.getForm()
|
||||||
condition: { field: "numchild", value: 0, condition: "gt" },
|
condition: {field: "numchild", value: 0, condition: "gt"},
|
||||||
help_text: "ChildInheritFields_help", // form.help_text always translated
|
help_text: "ChildInheritFields_help", // form.help_text always translated
|
||||||
},
|
},
|
||||||
reset_inherit: {
|
reset_inherit: {
|
||||||
@ -199,7 +199,7 @@ export class Models {
|
|||||||
field: "reset_inherit",
|
field: "reset_inherit",
|
||||||
label: "reset_children",
|
label: "reset_children",
|
||||||
help_text: "reset_children_help",
|
help_text: "reset_children_help",
|
||||||
condition: { field: "numchild", value: 0, condition: "gt" },
|
condition: {field: "numchild", value: 0, condition: "gt"},
|
||||||
},
|
},
|
||||||
form_function: "FoodCreateDefault",
|
form_function: "FoodCreateDefault",
|
||||||
},
|
},
|
||||||
@ -399,7 +399,7 @@ export class Models {
|
|||||||
static SUPERMARKET = {
|
static SUPERMARKET = {
|
||||||
name: "Supermarket",
|
name: "Supermarket",
|
||||||
apiName: "Supermarket",
|
apiName: "Supermarket",
|
||||||
ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }],
|
ordered_tags: [{field: "category_to_supermarket", label: "category::name", color: "info"}],
|
||||||
create: {
|
create: {
|
||||||
params: [["name", "description", "category_to_supermarket"]],
|
params: [["name", "description", "category_to_supermarket"]],
|
||||||
form: {
|
form: {
|
||||||
@ -469,9 +469,9 @@ export class Models {
|
|||||||
form_field: true,
|
form_field: true,
|
||||||
type: "choice",
|
type: "choice",
|
||||||
options: [
|
options: [
|
||||||
{ value: "FOOD_ALIAS", text: "Food_Alias" },
|
{value: "FOOD_ALIAS", text: "Food_Alias"},
|
||||||
{ value: "UNIT_ALIAS", text: "Unit_Alias" },
|
{value: "UNIT_ALIAS", text: "Unit_Alias"},
|
||||||
{ value: "KEYWORD_ALIAS", text: "Keyword_Alias" },
|
{value: "KEYWORD_ALIAS", text: "Keyword_Alias"},
|
||||||
],
|
],
|
||||||
field: "type",
|
field: "type",
|
||||||
label: "Type",
|
label: "Type",
|
||||||
@ -669,6 +669,12 @@ export class Models {
|
|||||||
paginated: false,
|
paginated: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GROUP = {
|
||||||
|
name: "Group",
|
||||||
|
apiName: "Group",
|
||||||
|
paginated: false,
|
||||||
|
}
|
||||||
|
|
||||||
static STEP = {
|
static STEP = {
|
||||||
name: "Step",
|
name: "Step",
|
||||||
apiName: "Step",
|
apiName: "Step",
|
||||||
@ -694,7 +700,7 @@ export class Actions {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ok_label: { function: "translate", phrase: "Save" },
|
ok_label: {function: "translate", phrase: "Save"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
static UPDATE = {
|
static UPDATE = {
|
||||||
@ -729,7 +735,7 @@ export class Actions {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ok_label: { function: "translate", phrase: "Delete" },
|
ok_label: {function: "translate", phrase: "Delete"},
|
||||||
instruction: {
|
instruction: {
|
||||||
form_field: true,
|
form_field: true,
|
||||||
type: "instruction",
|
type: "instruction",
|
||||||
@ -756,17 +762,17 @@ export class Actions {
|
|||||||
suffix: "s",
|
suffix: "s",
|
||||||
params: ["query", "page", "pageSize", "options"],
|
params: ["query", "page", "pageSize", "options"],
|
||||||
config: {
|
config: {
|
||||||
query: { default: undefined },
|
query: {default: undefined},
|
||||||
page: { default: 1 },
|
page: {default: 1},
|
||||||
pageSize: { default: 25 },
|
pageSize: {default: 25},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
static MERGE = {
|
static MERGE = {
|
||||||
function: "merge",
|
function: "merge",
|
||||||
params: ["source", "target"],
|
params: ["source", "target"],
|
||||||
config: {
|
config: {
|
||||||
source: { type: "string" },
|
source: {type: "string"},
|
||||||
target: { type: "string" },
|
target: {type: "string"},
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
title: {
|
title: {
|
||||||
@ -781,7 +787,7 @@ export class Actions {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ok_label: { function: "translate", phrase: "Merge" },
|
ok_label: {function: "translate", phrase: "Merge"},
|
||||||
instruction: {
|
instruction: {
|
||||||
form_field: true,
|
form_field: true,
|
||||||
type: "instruction",
|
type: "instruction",
|
||||||
@ -815,8 +821,8 @@ export class Actions {
|
|||||||
function: "move",
|
function: "move",
|
||||||
params: ["source", "target"],
|
params: ["source", "target"],
|
||||||
config: {
|
config: {
|
||||||
source: { type: "string" },
|
source: {type: "string"},
|
||||||
target: { type: "string" },
|
target: {type: "string"},
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
title: {
|
title: {
|
||||||
@ -831,7 +837,7 @@ export class Actions {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ok_label: { function: "translate", phrase: "Move" },
|
ok_label: {function: "translate", phrase: "Move"},
|
||||||
instruction: {
|
instruction: {
|
||||||
form_field: true,
|
form_field: true,
|
||||||
type: "instruction",
|
type: "instruction",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,10 @@ const pages = {
|
|||||||
entry: "./src/apps/ShoppingListView/main.js",
|
entry: "./src/apps/ShoppingListView/main.js",
|
||||||
chunks: ["chunk-vendors"],
|
chunks: ["chunk-vendors"],
|
||||||
},
|
},
|
||||||
|
space_manage_view: {
|
||||||
|
entry: "./src/apps/SpaceManageView/main.js",
|
||||||
|
chunks: ["chunk-vendors"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Loading…
Reference in New Issue
Block a user