From ce1f55ffd186eee36837174f8502bf77ede37bb9 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 15 Jul 2022 17:12:01 +0200 Subject: [PATCH] settings wip --- cookbook/admin.py | 12 +- cookbook/helper/permission_helper.py | 18 + cookbook/integration/integration.py | 2 +- cookbook/models.py | 4 +- cookbook/serializer.py | 29 +- cookbook/templates/base.html | 2 +- ...{test_api_username.py => test_api_user.py} | 68 +++- cookbook/urls.py | 2 +- cookbook/views/api.py | 12 +- cookbook/views/views.py | 2 +- .../apps/RecipeEditView/RecipeEditView.vue | 2 +- vue/src/apps/SettingsView/SettingsView.vue | 36 +- .../ShoppingListView/ShoppingListView.vue | 4 +- vue/src/components/CookbookEditCard.vue | 4 +- vue/src/components/MealPlanEditModal.vue | 2 +- .../Settings/APISettingsComponent.vue | 59 +++ .../Settings/AccountSettingsComponent.vue | 78 ++++ .../Settings/MealPlanSettingsComponent.vue | 59 +++ .../Settings/SearchSettingsComponent.vue | 59 +++ .../Settings/ShoppingSettingsComponent.vue | 59 +++ vue/src/components/ShoppingLineItem.vue | 2 +- vue/src/utils/models.js | 2 +- vue/src/utils/openapi/api.ts | 374 ++++++++++++++++-- 23 files changed, 801 insertions(+), 90 deletions(-) rename cookbook/tests/api/{test_api_username.py => test_api_user.py} (55%) create mode 100644 vue/src/components/Settings/APISettingsComponent.vue create mode 100644 vue/src/components/Settings/AccountSettingsComponent.vue create mode 100644 vue/src/components/Settings/MealPlanSettingsComponent.vue create mode 100644 vue/src/components/Settings/SearchSettingsComponent.vue create mode 100644 vue/src/components/Settings/ShoppingSettingsComponent.vue diff --git a/cookbook/admin.py b/cookbook/admin.py index ce8cfda30..4b0d1acd2 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -62,7 +62,7 @@ class UserPreferenceAdmin(admin.ModelAdmin): @staticmethod def name(obj): - return obj.user.get_user_name() + return obj.user.get_user_display_name() admin.site.register(UserPreference, UserPreferenceAdmin) @@ -75,7 +75,7 @@ class SearchPreferenceAdmin(admin.ModelAdmin): @staticmethod def name(obj): - return obj.user.get_user_name() + return obj.user.get_user_display_name() admin.site.register(SearchPreference, SearchPreferenceAdmin) @@ -177,7 +177,7 @@ class RecipeAdmin(admin.ModelAdmin): @staticmethod def created_by(obj): - return obj.created_by.get_user_name() + return obj.created_by.get_user_display_name() if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']: actions = [rebuild_index] @@ -216,7 +216,7 @@ class CommentAdmin(admin.ModelAdmin): @staticmethod def name(obj): - return obj.created_by.get_user_name() + return obj.created_by.get_user_display_name() admin.site.register(Comment, CommentAdmin) @@ -235,7 +235,7 @@ class RecipeBookAdmin(admin.ModelAdmin): @staticmethod def user_name(obj): - return obj.created_by.get_user_name() + return obj.created_by.get_user_display_name() admin.site.register(RecipeBook, RecipeBookAdmin) @@ -253,7 +253,7 @@ class MealPlanAdmin(admin.ModelAdmin): @staticmethod def user(obj): - return obj.created_by.get_user_name() + return obj.created_by.get_user_display_name() admin.site.register(MealPlan, MealPlanAdmin) diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py index 739650d7c..2f0217f96 100644 --- a/cookbook/helper/permission_helper.py +++ b/cookbook/helper/permission_helper.py @@ -320,6 +320,24 @@ class CustomRecipePermission(permissions.BasePermission): return has_group_permission(request.user, ['guest']) and obj.space == request.space +class CustomUserPermission(permissions.BasePermission): + """ + Custom permission class for user api endpoint + """ + message = _('You do not have the required permissions to view this page!') + + def has_permission(self, request, view): # a space filtered user list is visible for everyone + return has_group_permission(request.user, ['guest']) + + def has_object_permission(self, request, view, obj): # object write permissions are only available for user + if request.method in SAFE_METHODS and 'pk' in view.kwargs and has_group_permission(request.user, ['guest']) and request.space in obj.userspace_set.all(): + return True + elif request.user == obj: + return True + else: + return False + + def above_space_limit(space): # TODO add file storage limit """ Test if the space has reached any limit (e.g. max recipes, users, ..) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index f2153eb0b..b54e7e560 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -43,7 +43,7 @@ class Integration: self.export_type = export_type self.ignored_recipes = [] - description = f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}' + description = f'Imported by {request.user.get_user_display_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}' icon = '📥' try: diff --git a/cookbook/models.py b/cookbook/models.py index bcb7a604e..f9ef06164 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -26,7 +26,7 @@ from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PR SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT) -def get_user_name(self): +def get_user_display_name(self): if not (name := f"{self.first_name} {self.last_name}") == " ": return name else: @@ -58,7 +58,7 @@ def get_shopping_share(self): ])) -auth.models.User.add_to_class('get_user_name', get_user_name) +auth.models.User.add_to_class('get_user_display_name', get_user_display_name) auth.models.User.add_to_class('get_shopping_share', get_shopping_share) auth.models.User.add_to_class('get_active_space', get_active_space) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index a752e79bc..04a7aa77b 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -133,16 +133,17 @@ class SpaceFilterSerializer(serializers.ListSerializer): return super().to_representation(data) -class UserNameSerializer(WritableNestedModelSerializer): - username = serializers.SerializerMethodField('get_user_label') +class UserSerializer(WritableNestedModelSerializer): + display_name = serializers.SerializerMethodField('get_user_label') def get_user_label(self, obj): - return obj.get_user_name() + return obj.get_user_display_name() class Meta: list_serializer_class = SpaceFilterSerializer model = User - fields = ('id', 'username') + fields = ('id', 'username', 'first_name', 'last_name', 'display_name') + read_only_fields = ('username', ) class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer): @@ -279,7 +280,7 @@ class SpaceSerializer(WritableNestedModelSerializer): class UserSpaceSerializer(WritableNestedModelSerializer): - user = UserNameSerializer(read_only=True) + user = UserSerializer(read_only=True) groups = GroupSerializer(many=True) def validate(self, data): @@ -317,8 +318,8 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer): class UserPreferenceSerializer(WritableNestedModelSerializer): food_inherit_default = serializers.SerializerMethodField('get_food_inherit_defaults') - plan_share = UserNameSerializer(many=True, allow_null=True, required=False) - shopping_share = UserNameSerializer(many=True, allow_null=True, required=False) + plan_share = UserSerializer(many=True, allow_null=True, required=False) + shopping_share = UserSerializer(many=True, allow_null=True, required=False) food_children_exist = serializers.SerializerMethodField('get_food_children_exist') image = UserFileViewSerializer(required=False, allow_null=True, many=False) @@ -737,7 +738,7 @@ class RecipeSerializer(RecipeBaseSerializer): keywords = KeywordSerializer(many=True) rating = serializers.SerializerMethodField('get_recipe_rating') last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked') - shared = UserNameSerializer(many=True, required=False) + shared = UserSerializer(many=True, required=False) class Meta: model = Recipe @@ -783,7 +784,7 @@ class CommentSerializer(serializers.ModelSerializer): class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer): - shared = UserNameSerializer(many=True, required=False) + shared = UserSerializer(many=True, required=False) def create(self, validated_data): validated_data['created_by'] = self.context['request'].user @@ -796,7 +797,7 @@ class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerialize class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer): - shared = UserNameSerializer(many=True) + shared = UserSerializer(many=True) filter = CustomFilterSerializer(allow_null=True, required=False) def create(self, validated_data): @@ -840,7 +841,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer): meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed note_markdown = serializers.SerializerMethodField('get_note_markdown') servings = CustomDecimalField() - shared = UserNameSerializer(many=True, required=False, allow_null=True) + shared = UserSerializer(many=True, required=False, allow_null=True) shopping = serializers.SerializerMethodField('in_shopping') def get_note_markdown(self, obj): @@ -904,7 +905,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer): ingredient_note = serializers.ReadOnlyField(source='ingredient.note') recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True) amount = CustomDecimalField() - created_by = UserNameSerializer(read_only=True) + created_by = UserSerializer(read_only=True) completed_at = serializers.DateTimeField(allow_null=True, required=False) def get_fields(self, *args, **kwargs): @@ -972,7 +973,7 @@ class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer): class ShoppingListSerializer(WritableNestedModelSerializer): recipes = ShoppingListRecipeSerializer(many=True, allow_null=True) entries = ShoppingListEntrySerializer(many=True, allow_null=True) - shared = UserNameSerializer(many=True) + shared = UserSerializer(many=True) supermarket = SupermarketSerializer(allow_null=True) def create(self, validated_data): @@ -1085,7 +1086,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer): if obj.email: try: if InviteLink.objects.filter(space=self.context['request'].space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20: - message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.username) + message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.get_user_display_name()) message += _(' to join their Tandoor Recipes space ') + escape(self.context['request'].space.name) + '.\n\n' message += _('Click the following link to activate your account: ') + self.context['request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n' message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n' diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 14caa6887..1e36e630c 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -285,7 +285,7 @@