diff --git a/bookmarks/api/routes.py b/bookmarks/api/routes.py index d8ff0b5e..2cdd66bb 100644 --- a/bookmarks/api/routes.py +++ b/bookmarks/api/routes.py @@ -5,7 +5,7 @@ from rest_framework.routers import DefaultRouter from bookmarks import queries -from bookmarks.api.serializers import BookmarkSerializer, TagSerializer +from bookmarks.api.serializers import BookmarkSerializer, TagSerializer, UserProfileSerializer from bookmarks.models import Bookmark, BookmarkSearch, Tag, User from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark, website_loader from bookmarks.services.website_loader import WebsiteMetadata @@ -108,6 +108,13 @@ def get_serializer_context(self): return {'user': self.request.user} +class UserViewSet(viewsets.GenericViewSet): + @action(methods=['get'], detail=False) + def profile(self, request): + return Response(UserProfileSerializer(request.user.profile).data) + + router = DefaultRouter() router.register(r'bookmarks', BookmarkViewSet, basename='bookmark') router.register(r'tags', TagViewSet, basename='tag') +router.register(r'user', UserViewSet, basename='user') diff --git a/bookmarks/api/serializers.py b/bookmarks/api/serializers.py index b82b4ae3..b1fcee81 100644 --- a/bookmarks/api/serializers.py +++ b/bookmarks/api/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from rest_framework.serializers import ListSerializer -from bookmarks.models import Bookmark, Tag, build_tag_string +from bookmarks.models import Bookmark, Tag, build_tag_string, UserProfile from bookmarks.services.bookmarks import create_bookmark, update_bookmark from bookmarks.services.tags import get_or_create_tag @@ -89,3 +89,21 @@ class Meta: def create(self, validated_data): return get_or_create_tag(validated_data['name'], self.context['user']) + + +class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = [ + "theme", + "bookmark_date_display", + "bookmark_link_target", + "web_archive_integration", + "tag_search", + "enable_sharing", + "enable_public_sharing", + "enable_favicons", + "display_url", + "permanent_notes", + "search_preferences", + ] diff --git a/bookmarks/tests/test_bookmarks_api.py b/bookmarks/tests/test_bookmarks_api.py index b407b3f6..d67c33e5 100644 --- a/bookmarks/tests/test_bookmarks_api.py +++ b/bookmarks/tests/test_bookmarks_api.py @@ -6,8 +6,9 @@ from django.urls import reverse from rest_framework import status from rest_framework.authtoken.models import Token +from rest_framework.response import Response -from bookmarks.models import Bookmark +from bookmarks.models import Bookmark, BookmarkSearch, UserProfile from bookmarks.services import website_loader from bookmarks.services.website_loader import WebsiteMetadata from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin @@ -644,3 +645,49 @@ def test_can_only_access_own_bookmarks(self): check_url = urllib.parse.quote_plus(inaccessible_bookmark.url) response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK) self.assertIsNone(response.data['bookmark']) + + def assertUserProfile(self, response: Response, profile: UserProfile): + self.assertEqual(response.data['theme'], profile.theme) + self.assertEqual(response.data['bookmark_date_display'], profile.bookmark_date_display) + self.assertEqual(response.data['bookmark_link_target'], profile.bookmark_link_target) + self.assertEqual(response.data['web_archive_integration'], profile.web_archive_integration) + self.assertEqual(response.data['tag_search'], profile.tag_search) + self.assertEqual(response.data['enable_sharing'], profile.enable_sharing) + self.assertEqual(response.data['enable_public_sharing'], profile.enable_public_sharing) + self.assertEqual(response.data['enable_favicons'], profile.enable_favicons) + self.assertEqual(response.data['display_url'], profile.display_url) + self.assertEqual(response.data['permanent_notes'], profile.permanent_notes) + self.assertEqual(response.data['search_preferences'], profile.search_preferences) + + def test_user_profile(self): + self.authenticate() + + # default profile + profile = self.user.profile + url = reverse('bookmarks:user-profile') + response = self.get(url, expected_status_code=status.HTTP_200_OK) + + self.assertUserProfile(response, profile) + + # update profile + profile.theme = 'dark' + profile.bookmark_date_display = 'absolute' + profile.bookmark_link_target = '_self' + profile.web_archive_integration = 'enabled' + profile.tag_search = 'lax' + profile.enable_sharing = True + profile.enable_public_sharing = True + profile.enable_favicons = True + profile.display_url = True + profile.permanent_notes = True + profile.search_preferences = { + 'sort': BookmarkSearch.SORT_TITLE_ASC, + 'shared': BookmarkSearch.FILTER_SHARED_OFF, + 'unread': BookmarkSearch.FILTER_UNREAD_YES, + } + profile.save() + + url = reverse('bookmarks:user-profile') + response = self.get(url, expected_status_code=status.HTTP_200_OK) + + self.assertUserProfile(response, profile) diff --git a/bookmarks/tests/test_bookmarks_api_permissions.py b/bookmarks/tests/test_bookmarks_api_permissions.py index e62235cc..5893cbe5 100644 --- a/bookmarks/tests/test_bookmarks_api_permissions.py +++ b/bookmarks/tests/test_bookmarks_api_permissions.py @@ -111,3 +111,11 @@ def test_check_requires_authentication(self): self.authenticate() self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK) + + def test_user_profile_requires_authentication(self): + url = reverse('bookmarks:user-profile') + + self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED) + + self.authenticate() + self.get(url, expected_status_code=status.HTTP_200_OK) diff --git a/docs/API.md b/docs/API.md index 6df34693..533cf43a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -236,3 +236,35 @@ Example payload: "name": "example" } ``` + +### User + +**Profile** + +``` +GET /api/user/profile/ +``` + +User preferences. + +Example response: + +```json +{ + "theme": "auto", + "bookmark_date_display": "relative", + "bookmark_link_target": "_blank", + "web_archive_integration": "enabled", + "tag_search": "lax", + "enable_sharing": true, + "enable_public_sharing": true, + "enable_favicons": false, + "display_url": false, + "permanent_notes": false, + "search_preferences": { + "sort": "title_asc", + "shared": "off", + "unread": "off" + } +} +```