From e2b887b44917d5c2c6d3b61a05b89623da082139 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 6 Jun 2022 18:21:15 +0200 Subject: [PATCH] lots of fixes and stuff --- cookbook/admin.py | 36 +------ cookbook/helper/recipe_search.py | 2 +- .../migrations/0084_auto_20200922_1233.py | 11 ++- cookbook/models.py | 50 +++++++++- cookbook/serializer.py | 46 +++++---- cookbook/signals.py | 13 ++- cookbook/templates/base.html | 6 +- cookbook/templates/space.html | 2 +- cookbook/templates/space_manage.html | 2 +- cookbook/templates/space_overview.html | 7 +- cookbook/templatetags/custom_tags.py | 8 +- cookbook/tests/api/test_api_related_recipe.py | 4 +- .../tests/api/test_api_shopping_recipe.py | 4 +- cookbook/urls.py | 9 +- cookbook/views/api.py | 28 +++++- cookbook/views/delete.py | 18 +++- cookbook/views/views.py | 96 ++----------------- .../apps/SpaceManageView/SpaceManageView.vue | 73 ++++++++++++-- vue/src/locales/en.json | 10 +- 19 files changed, 236 insertions(+), 189 deletions(-) diff --git a/cookbook/admin.py b/cookbook/admin.py index b5fa6640b..0e9039997 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -32,41 +32,7 @@ admin.site.unregister(Group) @admin.action(description='Delete all data from a space') def delete_space_action(modeladmin, request, queryset): for space in queryset: - CookLog.objects.filter(space=space).delete() - ViewLog.objects.filter(space=space).delete() - ImportLog.objects.filter(space=space).delete() - BookmarkletImport.objects.filter(space=space).delete() - - Comment.objects.filter(recipe__space=space).delete() - Keyword.objects.filter(space=space).delete() - Ingredient.objects.filter(space=space).delete() - Food.objects.filter(space=space).delete() - Unit.objects.filter(space=space).delete() - Step.objects.filter(space=space).delete() - NutritionInformation.objects.filter(space=space).delete() - RecipeBookEntry.objects.filter(book__space=space).delete() - RecipeBook.objects.filter(space=space).delete() - MealType.objects.filter(space=space).delete() - MealPlan.objects.filter(space=space).delete() - ShareLink.objects.filter(space=space).delete() - Recipe.objects.filter(space=space).delete() - - RecipeImport.objects.filter(space=space).delete() - SyncLog.objects.filter(sync__space=space).delete() - Sync.objects.filter(space=space).delete() - Storage.objects.filter(space=space).delete() - - ShoppingListEntry.objects.filter(shoppinglist__space=space).delete() - ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete() - ShoppingList.objects.filter(space=space).delete() - - SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete() - SupermarketCategory.objects.filter(space=space).delete() - Supermarket.objects.filter(space=space).delete() - - InviteLink.objects.filter(space=space).delete() - UserFile.objects.filter(space=space).delete() - Automation.objects.filter(space=space).delete() + space.save() class SpaceAdmin(admin.ModelAdmin): diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index b0ead870a..fc62d7417 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -760,6 +760,6 @@ def old_search(request): params = dict(request.GET) params['internal'] = None f = RecipeFilter(params, - queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()), + queryset=Recipe.objects.filter(space=request.space).all().order_by(Lower('name').asc()), space=request.space) return f.qs diff --git a/cookbook/migrations/0084_auto_20200922_1233.py b/cookbook/migrations/0084_auto_20200922_1233.py index a2ddf2844..93e08309b 100644 --- a/cookbook/migrations/0084_auto_20200922_1233.py +++ b/cookbook/migrations/0084_auto_20200922_1233.py @@ -4,11 +4,12 @@ from django.db import migrations def create_default_space(apps, schema_editor): - Space = apps.get_model('cookbook', 'Space') - Space.objects.create( - name='Default', - message='' - ) + # Space = apps.get_model('cookbook', 'Space') + # Space.objects.create( + # name='Default', + # message='' + # ) + pass # Beginning with the multi space tenancy version (~something around 1.3) a default space is no longer needed as the first user can create it after setup class Migration(migrations.Migration): diff --git a/cookbook/models.py b/cookbook/models.py index c6918bf3b..e3415754a 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -240,7 +240,49 @@ class Space(ExportModelOperationsMixin('space'), models.Model): demo = models.BooleanField(default=False) food_inherit = models.ManyToManyField(FoodInheritField, blank=True) show_facet_count = models.BooleanField(default=False) + + def safe_delete(self): + """ + Safely deletes a space by deleting all objects belonging to the space first and then deleting the space itself + """ + CookLog.objects.filter(space=self).delete() + ViewLog.objects.filter(space=self).delete() + ImportLog.objects.filter(space=self).delete() + BookmarkletImport.objects.filter(space=self).delete() + CustomFilter.objects.filter(space=self).delete() + Comment.objects.filter(recipe__space=self).delete() + Keyword.objects.filter(space=self).delete() + Ingredient.objects.filter(space=self).delete() + Food.objects.filter(space=self).delete() + Unit.objects.filter(space=self).delete() + Step.objects.filter(space=self).delete() + NutritionInformation.objects.filter(space=self).delete() + RecipeBookEntry.objects.filter(book__space=self).delete() + RecipeBook.objects.filter(space=self).delete() + MealType.objects.filter(space=self).delete() + MealPlan.objects.filter(space=self).delete() + ShareLink.objects.filter(space=self).delete() + Recipe.objects.filter(space=self).delete() + + RecipeImport.objects.filter(space=self).delete() + SyncLog.objects.filter(sync__space=self).delete() + Sync.objects.filter(space=self).delete() + Storage.objects.filter(space=self).delete() + + ShoppingListEntry.objects.filter(shoppinglist__space=self).delete() + ShoppingListRecipe.objects.filter(shoppinglist__space=self).delete() + ShoppingList.objects.filter(space=self).delete() + + SupermarketCategoryRelation.objects.filter(supermarket__space=self).delete() + SupermarketCategory.objects.filter(space=self).delete() + Supermarket.objects.filter(space=self).delete() + + InviteLink.objects.filter(space=self).delete() + UserFile.objects.filter(space=self).delete() + Automation.objects.filter(space=self).delete() + self.delete() + def get_owner(self): return self.created_by @@ -551,14 +593,14 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): tree_filter = Q(space=space) # remove all inherited fields from food - Through = Food.objects.filter(tree_filter).first().inherit_fields.through - Through.objects.all().delete() + trough = Food.objects.filter(tree_filter).first().inherit_fields.through + trough.objects.all().delete() # food is going to inherit attributes if len(inherit) > 0: # ManyToMany cannot be updated through an UPDATE operation for i in inherit: - Through.objects.bulk_create([ - Through(food_id=x, foodinheritfield_id=i['id']) + trough.objects.bulk_create([ + trough(food_id=x, foodinheritfield_id=i['id']) for x in Food.objects.filter(tree_filter).values_list('id', flat=True) ]) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 6a593fb0b..bba902e68 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -151,10 +151,27 @@ class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer): fields = ('id', 'name') -class SpaceSerializer(serializers.ModelSerializer): +class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelSerializer): + name = serializers.CharField(allow_null=True, allow_blank=True, required=False) + field = serializers.CharField(allow_null=True, allow_blank=True, required=False) + + def create(self, validated_data): + raise ValidationError('Cannot create using this endpoint') + + def update(self, instance, validated_data): + return instance + + class Meta: + model = FoodInheritField + fields = ('id', 'name', 'field',) + read_only_fields = ['id'] + + +class SpaceSerializer(WritableNestedModelSerializer): user_count = serializers.SerializerMethodField('get_user_count') recipe_count = serializers.SerializerMethodField('get_recipe_count') file_size_mb = serializers.SerializerMethodField('get_file_size_mb') + food_inherit = FoodInheritFieldSerializer(many=True) def get_user_count(self, obj): return UserSpace.objects.filter(space=obj).count() @@ -174,13 +191,18 @@ class SpaceSerializer(serializers.ModelSerializer): 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',) + read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',) class UserSpaceSerializer(WritableNestedModelSerializer): user = UserNameSerializer(read_only=True) groups = GroupSerializer(many=True) + def validate(self, data): + if self.instance.user == self.context['request'].space.created_by: # cant change space owner permission + raise serializers.ValidationError(_('Cannot modify Space owner permission.')) + return super().validate(data) + def create(self, validated_data): raise ValidationError('Cannot create using this endpoint') @@ -209,24 +231,6 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer): read_only_fields = ('created_by',) -class FoodInheritFieldSerializer(WritableNestedModelSerializer): - name = serializers.CharField(allow_null=True, allow_blank=True, required=False) - field = serializers.CharField(allow_null=True, allow_blank=True, required=False) - - def create(self, validated_data): - # don't allow writing to FoodInheritField via API - return FoodInheritField.objects.get(**validated_data) - - def update(self, instance, validated_data): - # don't allow writing to FoodInheritField via API - return FoodInheritField.objects.get(**validated_data) - - class Meta: - model = FoodInheritField - fields = ('id', 'name', 'field',) - read_only_fields = ['id'] - - class UserPreferenceSerializer(WritableNestedModelSerializer): food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True) @@ -1073,7 +1077,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer): class BookmarkletImportListSerializer(serializers.ModelSerializer): def create(self, validated_data): validated_data['created_by'] = self.context['request'].user - validated_data['space'] = self.context['request'].user.userpreference.space + validated_data['space'] = self.context['request'].space return super().create(validated_data) class Meta: diff --git a/cookbook/signals.py b/cookbook/signals.py index 8032ada9f..f2f4f8b31 100644 --- a/cookbook/signals.py +++ b/cookbook/signals.py @@ -2,6 +2,7 @@ from decimal import Decimal from functools import wraps from django.conf import settings +from django.contrib.auth.models import User from django.contrib.postgres.search import SearchVector from django.db.models.signals import post_save from django.dispatch import receiver @@ -11,7 +12,7 @@ from django_scopes import scope from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.managers import DICTIONARY from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe, - ShoppingListEntry, Step) + ShoppingListEntry, Step, UserPreference) SQLITE = True if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', @@ -28,9 +29,17 @@ def skip_signal(signal_func): if hasattr(instance, 'skip_signal'): return None return signal_func(sender, instance, **kwargs) + return _decorator +@receiver(post_save, sender=User) +@skip_signal +def update_recipe_search_vector(sender, instance=None, created=False, **kwargs): + if created: + UserPreference.objects.get_or_create(user=instance) + + @receiver(post_save, sender=Recipe) @skip_signal def update_recipe_search_vector(sender, instance=None, created=False, **kwargs): @@ -131,5 +140,3 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs print("MEAL_AUTO_ADD Created SLR") except AttributeError: pass - - diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 6ae13f277..221de2809 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -294,7 +294,7 @@ {% trans 'External Recipes' %} {% if request.user == request.space.created_by %} - {% trans 'Space Settings' %} {% endif %} {% if user.is_superuser %} @@ -316,7 +316,7 @@ {% endif %} {{ us.space.name }} {% endfor %} - {% trans 'Create New' %} + {% trans 'Overview' %} {% endif %} -{% message_of_the_day as message_of_the_day %} +{% message_of_the_day request as message_of_the_day %} {% if message_of_the_day %}
{{ message_of_the_day }} diff --git a/cookbook/templates/space.html b/cookbook/templates/space.html index 67bc354a7..6044f1839 100644 --- a/cookbook/templates/space.html +++ b/cookbook/templates/space.html @@ -17,7 +17,7 @@ diff --git a/cookbook/templates/space_manage.html b/cookbook/templates/space_manage.html index e17eeccf0..52db3f4e0 100644 --- a/cookbook/templates/space_manage.html +++ b/cookbook/templates/space_manage.html @@ -12,7 +12,7 @@ diff --git a/cookbook/templates/space_overview.html b/cookbook/templates/space_overview.html index bf4bb3bf3..0966cc76b 100644 --- a/cookbook/templates/space_overview.html +++ b/cookbook/templates/space_overview.html @@ -3,7 +3,7 @@ {% load static %} {% load i18n %} -{% block title %}{% trans "No Space" %}{% endblock %} +{% block title %}{% trans "Overview" %}{% endblock %} {% block content %} @@ -36,6 +36,11 @@
{{ us.space.name }}
+{# {% if us.active %}#} +{# #} +{# {% else %}#} +{# #} +{# {% endif %}#}

{% trans 'Owner' %}: {{ us.space.created_by }} {% if us.space.created_by != us.user %} diff --git a/cookbook/templatetags/custom_tags.py b/cookbook/templatetags/custom_tags.py index f0fedc051..e76dc09a5 100644 --- a/cookbook/templatetags/custom_tags.py +++ b/cookbook/templatetags/custom_tags.py @@ -111,8 +111,12 @@ def page_help(page_name): @register.simple_tag -def message_of_the_day(): - return Space.objects.first().message +def message_of_the_day(request): + try: + if request.space.message: + return request.space.message + except (AttributeError, KeyError, ValueError): + pass @register.simple_tag diff --git a/cookbook/tests/api/test_api_related_recipe.py b/cookbook/tests/api/test_api_related_recipe.py index 1a381ed5c..08c56cf70 100644 --- a/cookbook/tests/api/test_api_related_recipe.py +++ b/cookbook/tests/api/test_api_related_recipe.py @@ -56,9 +56,9 @@ def test_get_related_recipes(request, arg, recipe, related_count, u1_s1, space_2 ({'steps__food_recipe_count': {'step': 0, 'count': 1}}), # shopping list from recipe with food recipe ({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}), # shopping list from recipe with StepRecipe and food recipe ], indirect=['recipe']) -def test_related_mixed_space(request, recipe, u1_s2): +def test_related_mixed_space(request, recipe, u1_s2, space_2): with scopes_disabled(): - recipe.space = auth.get_user(u1_s2).userpreference.space + recipe.space = space_2 recipe.save() assert len(json.loads( u1_s2.get( diff --git a/cookbook/tests/api/test_api_shopping_recipe.py b/cookbook/tests/api/test_api_shopping_recipe.py index bbf3cb8fb..376f5fef7 100644 --- a/cookbook/tests/api/test_api_shopping_recipe.py +++ b/cookbook/tests/api/test_api_shopping_recipe.py @@ -204,11 +204,11 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2): assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1] -def test_shopping_recipe_mixed_authors(u1_s1, u2_s1): +def test_shopping_recipe_mixed_authors(u1_s1, u2_s1,space_1): with scopes_disabled(): user1 = auth.get_user(u1_s1) user2 = auth.get_user(u2_s1) - space = user1.userpreference.space + space = space_1 user3 = UserFactory(space=space) recipe1 = RecipeFactory(created_by=user1, space=space) recipe2 = RecipeFactory(created_by=user2, space=space) diff --git a/cookbook/urls.py b/cookbook/urls.py index ed98e3eab..40917e27b 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -12,7 +12,7 @@ from recipes.version import VERSION_NUMBER from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile, - get_model_name, UserSpace) + get_model_name, UserSpace, Space) from .views import api, data, delete, edit, import_export, lists, new, telegram, views from .views.api import CustomAuthToken @@ -55,15 +55,11 @@ router.register(r'view-log', api.ViewLogViewSet) urlpatterns = [ path('', views.index, name='index'), path('setup/', views.setup, name='view_setup'), - path('space/', views.space, name='view_space'), - path('space/member///', views.space_change_member, - name='change_space_member'), path('no-group', views.no_groups, name='view_no_group'), path('space-overview', views.space_overview, name='view_space_overview'), path('space-manage/', views.space_manage, name='view_space_manage'), path('switch-space/', views.switch_space, name='view_switch_space'), path('no-perm', views.no_perm, name='view_no_perm'), - path('signup/', views.signup, name='view_signup'), # TODO deprecated with 0.16.2 remove at some point path('invite/', views.invite_link, name='view_invite'), path('system/', views.system, name='view_system'), path('search/', views.search, name='view_search'), @@ -120,6 +116,7 @@ urlpatterns = [ path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'), path('api/share-link/', api.share_link, name='api_share_link'), path('api/get_facets/', api.get_facets, name='api_get_facets'), + path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'), path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints @@ -151,7 +148,7 @@ urlpatterns = [ generic_models = ( Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, - Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace + Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space ) for m in generic_models: diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 6903c5bd5..69d52ab8a 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -2,6 +2,7 @@ import io import json import mimetypes import re +import traceback import uuid from collections import OrderedDict @@ -172,9 +173,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin): self.queryset = ( self.queryset - .annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), - default=Value(0))) # put exact matches at the top of the result set - .filter(filter).order_by('-starts', Lower('name').asc()) + .annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), + default=Value(0))) # put exact matches at the top of the result set + .filter(filter).order_by('-starts', Lower('name').asc()) ) updated_at = self.request.query_params.get('updated_at', None) @@ -388,6 +389,11 @@ class UserSpaceViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsSpaceOwner] http_method_names = ['get', 'patch', 'put', 'delete'] + def destroy(self, request, *args, **kwargs): + if request.space.created_by == UserSpace.objects.get(pk=kwargs['pk']).user: + raise APIException('Cannot delete Space owner permission.') + return super().destroy(request, *args, **kwargs) + def get_queryset(self): return self.queryset.filter(space=self.request.space) @@ -1156,6 +1162,22 @@ def recipe_from_source(request): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +@api_view(['GET']) +# @schema(AutoSchema()) #TODO add proper schema +@permission_classes([CustomIsAdmin]) +# TODO add rate limiting +def reset_food_inheritance(request): + """ + function to reset inheritance from api, see food method for docs + """ + try: + Food.reset_inheritance(space=request.space) + return Response({'message': 'success', }, status=status.HTTP_200_OK) + except Exception as e: + traceback.print_exc() + return Response(str(e), status=status.HTTP_400_BAD_REQUEST) + + def get_recipe_provider(recipe): if recipe.storage.method == Storage.DROPBOX: return Dropbox diff --git a/cookbook/views/delete.py b/cookbook/views/delete.py index 153298ccc..eefe6f38b 100644 --- a/cookbook/views/delete.py +++ b/cookbook/views/delete.py @@ -9,7 +9,7 @@ from django.views.generic import DeleteView from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry, - RecipeImport, Storage, Sync, UserSpace) + RecipeImport, Storage, Sync, UserSpace, Space) from cookbook.provider.dropbox import Dropbox from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud @@ -199,3 +199,19 @@ class UserSpaceDelete(OwnerRequiredMixin, DeleteView): context = super(UserSpaceDelete, self).get_context_data(**kwargs) context['title'] = _("Space Membership") return context + + +class SpaceDelete(OwnerRequiredMixin, DeleteView): + template_name = "generic/delete_template.html" + model = Space + success_url = reverse_lazy('view_space_overview') + + def delete(self, request, *args, **kwargs): + self.object = self.get_object() + self.object.safe_delete() + return HttpResponseRedirect(self.get_success_url()) + + def get_context_data(self, **kwargs): + context = super(SpaceDelete, self).get_context_data(**kwargs) + context['title'] = _("Space") + return context diff --git a/cookbook/views/views.py b/cookbook/views/views.py index 46c8acf41..9b73e5207 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -61,7 +61,7 @@ def search(request): if request.user.userpreference.search_style == UserPreference.NEW: return search_v2(request) f = RecipeFilter(request.GET, - queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by( + queryset=Recipe.objects.filter(space=request.space).all().order_by( Lower('name').asc()), space=request.space) if request.user.userpreference.search_style == UserPreference.LARGE: @@ -72,7 +72,7 @@ def search(request): if request.GET == {} and request.user.userpreference.show_recent: qs = Recipe.objects.filter(viewlog__created_by=request.user).filter( - space=request.user.userpreference.space).order_by('-viewlog__created_at').all() + space=request.space).order_by('-viewlog__created_at').all() recent_list = [] for r in qs: @@ -117,20 +117,19 @@ def space_overview(request): allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING, ) - user_space = UserSpace.objects.create(space=created_space, user=request.user, active=True) + user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False) user_space.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('view_switch_space', args=[user_space.pk])) + return HttpResponseRedirect(reverse('view_switch_space', args=[user_space.space.pk])) if join_form.is_valid(): return HttpResponseRedirect(reverse('view_invite', 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)) + user_space = UserSpace.objects.create(space=Space.objects.first(), user=request.user, active=True) + user_space.groups.add(Group.objects.filter(name=settings.SOCIAL_DEFAULT_GROUP).get()) return HttpResponseRedirect(reverse('index')) if 'signup_token' in request.session: return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')])) @@ -476,14 +475,6 @@ def setup(request): user.set_password(form.cleaned_data['password']) user.save() - user.groups.add(Group.objects.get(name='admin')) - - user.userpreference.space = Space.objects.first() - user.userpreference.save() - - for x in Space.objects.all(): - x.created_by = user - x.save() messages.add_message(request, messages.SUCCESS, _('User has been created, please login!')) return HttpResponseRedirect(reverse('account_login')) except ValidationError as e: @@ -504,7 +495,7 @@ def invite_link(request, token): return HttpResponseRedirect(reverse('index')) 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 and not request.user.userspace_set.filter(space=link.space).exists(): link.used_by = request.user link.save() @@ -526,85 +517,12 @@ def invite_link(request, token): return HttpResponseRedirect(reverse('view_space_overview')) -# TODO deprecated with 0.16.2 remove at some point -def signup(request, 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') -def space(request): - space_users = UserSpace.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() - - 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) - - space_form = SpacePreferenceForm(instance=request.space) - - space_form.base_fields['food_inherit'].queryset = Food.inheritable_fields - if request.method == "POST" and 'space_form' in request.POST: - form = SpacePreferenceForm(request.POST, prefix='space') - if form.is_valid(): - request.space.food_inherit.set(form.cleaned_data['food_inherit']) - request.space.show_facet_count = form.cleaned_data['show_facet_count'] - request.space.save() - if form.cleaned_data['reset_food_inherit']: - Food.reset_inheritance(space=request.space) - - return render(request, 'space.html', { - 'space_users': space_users, - 'counts': counts, - 'invite_links': invite_links, - 'space_form': space_form - }) - - -# TODO super hacky and quick solution, safe but needs rework -# TODO move group settings to space to prevent permissions from one space to move to another -@group_required('admin') -def space_change_member(request, user_id, space_id, group): - m_space = get_object_or_404(Space, pk=space_id) - m_user = get_object_or_404(User, pk=user_id) - if request.user == m_space.created_by and m_user != m_space.created_by: - if m_user.userpreference.space == m_space: - if group == 'admin': - m_user.groups.clear() - m_user.groups.add(Group.objects.get(name='admin')) - return HttpResponseRedirect(reverse('view_space')) - if group == 'user': - m_user.groups.clear() - m_user.groups.add(Group.objects.get(name='user')) - return HttpResponseRedirect(reverse('view_space')) - if group == 'guest': - m_user.groups.clear() - m_user.groups.add(Group.objects.get(name='guest')) - return HttpResponseRedirect(reverse('view_space')) - if group == 'remove': - m_user.groups.clear() - m_user.userpreference.space = None - m_user.userpreference.save() - return HttpResponseRedirect(reverse('view_space')) - return HttpResponseRedirect(reverse('view_space')) - def report_share_abuse(request, token): if not settings.SHARING_ABUSE: diff --git a/vue/src/apps/SpaceManageView/SpaceManageView.vue b/vue/src/apps/SpaceManageView/SpaceManageView.vue index b59209126..442602dba 100644 --- a/vue/src/apps/SpaceManageView/SpaceManageView.vue +++ b/vue/src/apps/SpaceManageView/SpaceManageView.vue @@ -6,7 +6,7 @@

{{ $t('Recipes') }}
- + {{ space.recipe_count }} / @@ -15,7 +15,7 @@
{{ $t('Users') }}
- + {{ space.user_count }} / @@ -24,7 +24,7 @@
{{ $t('Files') }}
- + {{ space.file_size_mb }} / @@ -85,7 +85,7 @@ - + {{ il.id }} {{ il.email }} @@ -134,6 +134,44 @@
+
+
+

{{ $t('Settings') }}

+ + + + + Facet Count + {{ $t('facet_count_info') }}
+ + + + + {{ $t('food_inherit_info') }}
+ + {{ $t('Update') }}
+ {{ $t('reset_food_inheritance') }}
+ {{ $t('reset_food_inheritance_info') }} +
+
+ +
+
+

{{ $t('Delete') }}

+ + {{ $t('warning_space_delete') }} +
+ {{ + $t('Delete') + }} +
+
+ +
+
+ @@ -146,11 +184,12 @@ import {BootstrapVue} from "bootstrap-vue" import "bootstrap-vue/dist/bootstrap-vue.css" -import {ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils" +import {ApiMixin, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils" import {ApiApiFactory} from "@/utils/openapi/api.ts" import GenericMultiselect from "@/components/GenericMultiselect"; import GenericModalForm from "@/components/Modals/GenericModalForm"; +import axios from "axios"; Vue.use(BootstrapVue) @@ -160,17 +199,23 @@ export default { components: {GenericMultiselect, GenericModalForm}, data() { return { + ACTIVE_SPACE_ID: window.ACTIVE_SPACE_ID, space: undefined, user_spaces: [], invite_links: [], show_invite_create: false } }, + computed: { + active_invite_links: function () { + return this.invite_links.filter(il => il.used_by === null) + }, + }, mounted() { this.$i18n.locale = window.CUSTOM_LOCALE let apiFactory = new ApiApiFactory() - apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => { + apiFactory.retrieveSpace(this.ACTIVE_SPACE_ID).then(r => { this.space = r.data }) apiFactory.listUserSpaces().then(r => { @@ -192,6 +237,14 @@ export default { this.invite_links = r.data }) }, + updateSpace: function () { + let apiFactory = new ApiApiFactory() + apiFactory.partialUpdateSpace(this.ACTIVE_SPACE_ID, this.space).then(r => { + StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE) + }).catch(err => { + StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) + }) + }, updateUserSpace: function (userSpace) { let apiFactory = new ApiApiFactory() apiFactory.partialUpdateUserSpace(userSpace.id, userSpace).then(r => { @@ -219,7 +272,13 @@ export default { }).catch(err => { StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err) }) - + }, + resetInheritance: function () { + axios.get(resolveDjangoUrl('api_reset_food_inheritance')).then(r => { + StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE) + }).catch(err => { + StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) + }) }, }, } diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 327ad4c3b..e2baea258 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -14,6 +14,9 @@ "success_moving_resource": "Successfully moved a resource!", "success_merging_resource": "Successfully merged a resource!", "file_upload_disabled": "File upload is not enabled for your space.", + "warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?", + "food_inherit_info": "Fields on food that should be inherited by default.", + "facet_count_info": "Show recipe counts on search filters.", "step_time_minutes": "Step time in minutes", "confirm_delete": "Are you sure you want to delete this {object}?", "import_running": "Import running, please wait!", @@ -344,6 +347,7 @@ "filter": "Filter", "Website": "Website", "App": "App", + "Message": "Message", "Bookmarklet": "Bookmarklet", "click_image_import": "Click the image you want to import for this recipe", "no_more_images_found": "No additional images found on Website.", @@ -355,7 +359,9 @@ "search_create_help_text": "Create a new recipe directly in Tandoor.", "warning_duplicate_filter": "Warning: Due to technical limitations having multiple filters of the same combination (and/or/not) might yield unexpected results.", "reset_children": "Reset Child Inheritance", - "reset_children_help": "Overwrite all children with values from inherited fields. Inheritted fields of children will be set to Inherit Fields unless Children Inherit Fields is set.", + "reset_children_help": "Overwrite all children with values from inherited fields. Inherited fields of children will be set to Inherit Fields unless Children Inherit Fields is set.", + "reset_food_inheritance": "Reset Inheritance", + "reset_food_inheritance_info": "Reset all foods to default inherited fields and their parent values.", "substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.", "substitute_siblings_help": "All food that share a parent of this food are considered substitutes.", "substitute_children_help": "All food that are children of this food are considered substitutes.", @@ -364,7 +370,7 @@ "SubstituteOnHand": "You have a substitute on hand.", "ChildInheritFields": "Children Inherit Fields", "ChildInheritFields_help": "Children will inherit these fields by default.", - "InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)", + "InheritFields_help": "The values of these fields will be inherited from parent (Exception: blank shopping categories are not inherited)", "last_viewed": "Last Viewed", "created_on": "Created On", "updatedon": "Updated On",