mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-08 03:08:29 +02:00
@@ -16,8 +16,16 @@ from bookmarks.api.serializers import (
|
|||||||
BookmarkAssetSerializer,
|
BookmarkAssetSerializer,
|
||||||
TagSerializer,
|
TagSerializer,
|
||||||
UserProfileSerializer,
|
UserProfileSerializer,
|
||||||
|
BookmarkBundleSerializer,
|
||||||
|
)
|
||||||
|
from bookmarks.models import (
|
||||||
|
Bookmark,
|
||||||
|
BookmarkAsset,
|
||||||
|
BookmarkSearch,
|
||||||
|
Tag,
|
||||||
|
User,
|
||||||
|
BookmarkBundle,
|
||||||
)
|
)
|
||||||
from bookmarks.models import Bookmark, BookmarkAsset, BookmarkSearch, Tag, User
|
|
||||||
from bookmarks.services import assets, bookmarks, auto_tagging, website_loader
|
from bookmarks.services import assets, bookmarks, auto_tagging, website_loader
|
||||||
from bookmarks.type_defs import HttpRequest
|
from bookmarks.type_defs import HttpRequest
|
||||||
from bookmarks.views import access
|
from bookmarks.views import access
|
||||||
@@ -264,6 +272,25 @@ class UserViewSet(viewsets.GenericViewSet):
|
|||||||
return Response(UserProfileSerializer(request.user.profile).data)
|
return Response(UserProfileSerializer(request.user.profile).data)
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkBundleViewSet(
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
):
|
||||||
|
request: HttpRequest
|
||||||
|
serializer_class = BookmarkBundleSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
return BookmarkBundle.objects.filter(owner=user).order_by("order")
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {"user": self.request.user}
|
||||||
|
|
||||||
|
|
||||||
# DRF routers do not support nested view sets such as /bookmarks/<id>/assets/<id>/
|
# DRF routers do not support nested view sets such as /bookmarks/<id>/assets/<id>/
|
||||||
# Instead create separate routers for each view set and manually register them in urls.py
|
# Instead create separate routers for each view set and manually register them in urls.py
|
||||||
# The default router is only used to allow reversing a URL for the API root
|
# The default router is only used to allow reversing a URL for the API root
|
||||||
@@ -278,5 +305,8 @@ tag_router.register("", TagViewSet, basename="tag")
|
|||||||
user_router = SimpleRouter()
|
user_router = SimpleRouter()
|
||||||
user_router.register("", UserViewSet, basename="user")
|
user_router.register("", UserViewSet, basename="user")
|
||||||
|
|
||||||
|
bundle_router = SimpleRouter()
|
||||||
|
bundle_router.register("", BookmarkBundleViewSet, basename="bundle")
|
||||||
|
|
||||||
bookmark_asset_router = SimpleRouter()
|
bookmark_asset_router = SimpleRouter()
|
||||||
bookmark_asset_router.register("", BookmarkAssetViewSet, basename="bookmark_asset")
|
bookmark_asset_router.register("", BookmarkAssetViewSet, basename="bookmark_asset")
|
||||||
|
@@ -1,9 +1,16 @@
|
|||||||
from django.db.models import prefetch_related_objects
|
from django.db.models import Max, prefetch_related_objects
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.serializers import ListSerializer
|
from rest_framework.serializers import ListSerializer
|
||||||
|
|
||||||
from bookmarks.models import Bookmark, BookmarkAsset, Tag, build_tag_string, UserProfile
|
from bookmarks.models import (
|
||||||
|
Bookmark,
|
||||||
|
BookmarkAsset,
|
||||||
|
Tag,
|
||||||
|
build_tag_string,
|
||||||
|
UserProfile,
|
||||||
|
BookmarkBundle,
|
||||||
|
)
|
||||||
from bookmarks.services import bookmarks
|
from bookmarks.services import bookmarks
|
||||||
from bookmarks.services.tags import get_or_create_tag
|
from bookmarks.services.tags import get_or_create_tag
|
||||||
from bookmarks.services.wayback import generate_fallback_webarchive_url
|
from bookmarks.services.wayback import generate_fallback_webarchive_url
|
||||||
@@ -27,6 +34,40 @@ class EmtpyField(serializers.ReadOnlyField):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkBundleSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BookmarkBundle
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"search",
|
||||||
|
"any_tags",
|
||||||
|
"all_tags",
|
||||||
|
"excluded_tags",
|
||||||
|
"order",
|
||||||
|
"date_created",
|
||||||
|
"date_modified",
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"date_created",
|
||||||
|
"date_modified",
|
||||||
|
]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Set owner to the authenticated user
|
||||||
|
validated_data["owner"] = self.context["user"]
|
||||||
|
|
||||||
|
# Set order to the next available position if not provided
|
||||||
|
if "order" not in validated_data:
|
||||||
|
max_order = BookmarkBundle.objects.filter(
|
||||||
|
owner=self.context["user"]
|
||||||
|
).aggregate(Max("order", default=-1))["order__max"]
|
||||||
|
validated_data["order"] = max_order + 1
|
||||||
|
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class BookmarkSerializer(serializers.ModelSerializer):
|
class BookmarkSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bookmark
|
model = Bookmark
|
||||||
|
303
bookmarks/tests/test_bundles_api.py
Normal file
303
bookmarks/tests/test_bundles_api.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from bookmarks.models import BookmarkBundle
|
||||||
|
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
|
||||||
|
|
||||||
|
|
||||||
|
class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||||
|
def assertBundle(self, bundle: BookmarkBundle, data: dict):
|
||||||
|
self.assertEqual(bundle.id, data["id"])
|
||||||
|
self.assertEqual(bundle.name, data["name"])
|
||||||
|
self.assertEqual(bundle.search, data["search"])
|
||||||
|
self.assertEqual(bundle.any_tags, data["any_tags"])
|
||||||
|
self.assertEqual(bundle.all_tags, data["all_tags"])
|
||||||
|
self.assertEqual(bundle.excluded_tags, data["excluded_tags"])
|
||||||
|
self.assertEqual(bundle.order, data["order"])
|
||||||
|
self.assertEqual(
|
||||||
|
bundle.date_created.isoformat().replace("+00:00", "Z"), data["date_created"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
bundle.date_modified.isoformat().replace("+00:00", "Z"),
|
||||||
|
data["date_modified"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_bundle_list(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundles = [
|
||||||
|
self.setup_bundle(name="Bundle 1", order=0),
|
||||||
|
self.setup_bundle(name="Bundle 2", order=1),
|
||||||
|
self.setup_bundle(name="Bundle 3", order=2),
|
||||||
|
]
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.get(url, expected_status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data["results"]), 3)
|
||||||
|
self.assertBundle(bundles[0], response.data["results"][0])
|
||||||
|
self.assertBundle(bundles[1], response.data["results"][1])
|
||||||
|
self.assertBundle(bundles[2], response.data["results"][2])
|
||||||
|
|
||||||
|
def test_bundle_list_only_returns_own_bundles(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
user_bundles = [
|
||||||
|
self.setup_bundle(name="User Bundle 1"),
|
||||||
|
self.setup_bundle(name="User Bundle 2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
other_user = self.setup_user()
|
||||||
|
self.setup_bundle(name="Other User Bundle 1", user=other_user)
|
||||||
|
self.setup_bundle(name="Other User Bundle 2", user=other_user)
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.get(url, expected_status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data["results"]), 2)
|
||||||
|
self.assertBundle(user_bundles[0], response.data["results"][0])
|
||||||
|
self.assertBundle(user_bundles[1], response.data["results"][1])
|
||||||
|
|
||||||
|
def test_bundle_list_requires_authentication(self):
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_bundle_detail(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle = self.setup_bundle(
|
||||||
|
name="Test Bundle",
|
||||||
|
search="test search",
|
||||||
|
any_tags="tag1 tag2",
|
||||||
|
all_tags="required-tag",
|
||||||
|
excluded_tags="excluded-tag",
|
||||||
|
order=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
response = self.get(url, expected_status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
self.assertBundle(bundle, response.data)
|
||||||
|
|
||||||
|
def test_bundle_detail_only_returns_own_bundles(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
other_user = self.setup_user()
|
||||||
|
other_bundle = self.setup_bundle(name="Other User Bundle", user=other_user)
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": other_bundle.id})
|
||||||
|
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_bundle_detail_requires_authentication(self):
|
||||||
|
bundle = self.setup_bundle()
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_create_bundle(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle_data = {
|
||||||
|
"name": "New Bundle",
|
||||||
|
"search": "test search",
|
||||||
|
"any_tags": "tag1 tag2",
|
||||||
|
"all_tags": "required-tag",
|
||||||
|
"excluded_tags": "excluded-tag",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.post(
|
||||||
|
url, bundle_data, expected_status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = BookmarkBundle.objects.get(id=response.data["id"])
|
||||||
|
self.assertEqual(bundle.name, bundle_data["name"])
|
||||||
|
self.assertEqual(bundle.search, bundle_data["search"])
|
||||||
|
self.assertEqual(bundle.any_tags, bundle_data["any_tags"])
|
||||||
|
self.assertEqual(bundle.all_tags, bundle_data["all_tags"])
|
||||||
|
self.assertEqual(bundle.excluded_tags, bundle_data["excluded_tags"])
|
||||||
|
self.assertEqual(bundle.owner, self.user)
|
||||||
|
self.assertEqual(bundle.order, 0)
|
||||||
|
|
||||||
|
self.assertBundle(bundle, response.data)
|
||||||
|
|
||||||
|
def test_create_bundle_auto_increments_order(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
self.setup_bundle(name="Existing Bundle", order=2)
|
||||||
|
|
||||||
|
bundle_data = {"name": "New Bundle", "search": "test search"}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.post(
|
||||||
|
url, bundle_data, expected_status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = BookmarkBundle.objects.get(id=response.data["id"])
|
||||||
|
self.assertEqual(bundle.order, 3)
|
||||||
|
|
||||||
|
def test_create_bundle_with_custom_order(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle_data = {"name": "New Bundle", "order": 10}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.post(
|
||||||
|
url, bundle_data, expected_status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = BookmarkBundle.objects.get(id=response.data["id"])
|
||||||
|
self.assertEqual(bundle.order, 10)
|
||||||
|
|
||||||
|
def test_create_bundle_requires_name(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle_data = {"search": "test search"}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
self.post(url, bundle_data, expected_status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_create_bundle_fields_can_be_empty(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle_data = {
|
||||||
|
"name": "Minimal Bundle",
|
||||||
|
"search": "",
|
||||||
|
"any_tags": "",
|
||||||
|
"all_tags": "",
|
||||||
|
"excluded_tags": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.post(
|
||||||
|
url, bundle_data, expected_status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = BookmarkBundle.objects.get(id=response.data["id"])
|
||||||
|
self.assertEqual(bundle.name, "Minimal Bundle")
|
||||||
|
self.assertEqual(bundle.search, "")
|
||||||
|
self.assertEqual(bundle.any_tags, "")
|
||||||
|
self.assertEqual(bundle.all_tags, "")
|
||||||
|
self.assertEqual(bundle.excluded_tags, "")
|
||||||
|
|
||||||
|
def test_create_bundle_requires_authentication(self):
|
||||||
|
bundle_data = {"name": "New Bundle"}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
self.post(url, bundle_data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_update_bundle_put(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle = self.setup_bundle(
|
||||||
|
name="Original Bundle",
|
||||||
|
search="original search",
|
||||||
|
any_tags="original-tag",
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_data = {
|
||||||
|
"name": "Updated Bundle",
|
||||||
|
"search": "updated search",
|
||||||
|
"any_tags": "updated-tag1 updated-tag2",
|
||||||
|
"all_tags": "required-updated-tag",
|
||||||
|
"excluded_tags": "excluded-updated-tag",
|
||||||
|
"order": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
response = self.put(url, updated_data, expected_status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
bundle.refresh_from_db()
|
||||||
|
self.assertEqual(bundle.name, updated_data["name"])
|
||||||
|
self.assertEqual(bundle.search, updated_data["search"])
|
||||||
|
self.assertEqual(bundle.any_tags, updated_data["any_tags"])
|
||||||
|
self.assertEqual(bundle.all_tags, updated_data["all_tags"])
|
||||||
|
self.assertEqual(bundle.excluded_tags, updated_data["excluded_tags"])
|
||||||
|
self.assertEqual(bundle.order, updated_data["order"])
|
||||||
|
|
||||||
|
self.assertBundle(bundle, response.data)
|
||||||
|
|
||||||
|
def test_update_bundle_patch(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle = self.setup_bundle(
|
||||||
|
name="Original Bundle", search="original search", any_tags="original-tag"
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_data = {
|
||||||
|
"name": "Partially Updated Bundle",
|
||||||
|
"search": "partially updated search",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
response = self.patch(
|
||||||
|
url, updated_data, expected_status_code=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle.refresh_from_db()
|
||||||
|
self.assertEqual(bundle.name, updated_data["name"])
|
||||||
|
self.assertEqual(bundle.search, updated_data["search"])
|
||||||
|
self.assertEqual(bundle.any_tags, "original-tag") # Should remain unchanged
|
||||||
|
|
||||||
|
self.assertBundle(bundle, response.data)
|
||||||
|
|
||||||
|
def test_update_bundle_only_allows_own_bundles(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
other_user = self.setup_user()
|
||||||
|
other_bundle = self.setup_bundle(name="Other User Bundle", user=other_user)
|
||||||
|
|
||||||
|
updated_data = {"name": "Updated Bundle"}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": other_bundle.id})
|
||||||
|
self.put(url, updated_data, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_update_bundle_requires_authentication(self):
|
||||||
|
bundle = self.setup_bundle()
|
||||||
|
updated_data = {"name": "Updated Bundle"}
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
self.put(url, updated_data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_delete_bundle(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
bundle = self.setup_bundle(name="Bundle to Delete")
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
self.assertFalse(BookmarkBundle.objects.filter(id=bundle.id).exists())
|
||||||
|
|
||||||
|
def test_delete_bundle_only_allows_own_bundles(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
other_user = self.setup_user()
|
||||||
|
other_bundle = self.setup_bundle(name="Other User Bundle", user=other_user)
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": other_bundle.id})
|
||||||
|
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
self.assertTrue(BookmarkBundle.objects.filter(id=other_bundle.id).exists())
|
||||||
|
|
||||||
|
def test_delete_bundle_requires_authentication(self):
|
||||||
|
bundle = self.setup_bundle()
|
||||||
|
url = reverse("linkding:bundle-detail", kwargs={"pk": bundle.id})
|
||||||
|
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
self.assertTrue(BookmarkBundle.objects.filter(id=bundle.id).exists())
|
||||||
|
|
||||||
|
def test_bundles_ordered_by_order_field(self):
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
self.setup_bundle(name="Third Bundle", order=2)
|
||||||
|
self.setup_bundle(name="First Bundle", order=0)
|
||||||
|
self.setup_bundle(name="Second Bundle", order=1)
|
||||||
|
|
||||||
|
url = reverse("linkding:bundle-list")
|
||||||
|
response = self.get(url, expected_status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data["results"]), 3)
|
||||||
|
self.assertEqual(response.data["results"][0]["name"], "First Bundle")
|
||||||
|
self.assertEqual(response.data["results"][1]["name"], "Second Bundle")
|
||||||
|
self.assertEqual(response.data["results"][2]["name"], "Third Bundle")
|
@@ -70,6 +70,7 @@ urlpatterns = [
|
|||||||
include(api_routes.bookmark_asset_router.urls),
|
include(api_routes.bookmark_asset_router.urls),
|
||||||
),
|
),
|
||||||
path("api/tags/", include(api_routes.tag_router.urls)),
|
path("api/tags/", include(api_routes.tag_router.urls)),
|
||||||
|
path("api/bundles/", include(api_routes.bundle_router.urls)),
|
||||||
path("api/user/", include(api_routes.user_router.urls)),
|
path("api/user/", include(api_routes.user_router.urls)),
|
||||||
# Feeds
|
# Feeds
|
||||||
path("feeds/<str:feed_key>/all", feeds.AllBookmarksFeed(), name="feeds.all"),
|
path("feeds/<str:feed_key>/all", feeds.AllBookmarksFeed(), name="feeds.all"),
|
||||||
|
@@ -355,6 +355,115 @@ Example payload:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bundles
|
||||||
|
|
||||||
|
**List**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/bundles/
|
||||||
|
```
|
||||||
|
|
||||||
|
List bundles.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- `limit` - Limits the max. number of results. Default is `100`.
|
||||||
|
- `offset` - Index from which to start returning results
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Work Resources",
|
||||||
|
"search": "productivity tools",
|
||||||
|
"any_tags": "work productivity",
|
||||||
|
"all_tags": "",
|
||||||
|
"excluded_tags": "personal",
|
||||||
|
"order": 0,
|
||||||
|
"date_created": "2020-09-26T09:46:23.006313Z",
|
||||||
|
"date_modified": "2020-09-26T16:01:14.275335Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Tech Articles",
|
||||||
|
"search": "",
|
||||||
|
"any_tags": "programming development",
|
||||||
|
"all_tags": "",
|
||||||
|
"excluded_tags": "outdated",
|
||||||
|
"order": 1,
|
||||||
|
"date_created": "2020-09-27T10:15:30.123456Z",
|
||||||
|
"date_modified": "2020-09-27T10:15:30.123456Z"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retrieve**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/bundles/<id>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieves a single bundle by ID.
|
||||||
|
|
||||||
|
**Create**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/bundles/
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a new bundle. If no `order` is specified, the bundle will be automatically assigned the next available order position.
|
||||||
|
|
||||||
|
Example payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "My Bundle",
|
||||||
|
"search": "search terms",
|
||||||
|
"any_tags": "tag1 tag2",
|
||||||
|
"all_tags": "required-tag",
|
||||||
|
"excluded_tags": "excluded-tag",
|
||||||
|
"order": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update**
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/bundles/<id>/
|
||||||
|
PATCH /api/bundles/<id>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Updates a bundle.
|
||||||
|
When using `PUT`, all fields except read-only ones should be provided.
|
||||||
|
When using `PATCH`, only the fields that should be updated need to be provided.
|
||||||
|
|
||||||
|
Example payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Updated Bundle Name",
|
||||||
|
"search": "updated search terms",
|
||||||
|
"any_tags": "new-tag1 new-tag2",
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Delete**
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/bundles/<id>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Deletes a bundle by ID.
|
||||||
|
|
||||||
### User
|
### User
|
||||||
|
|
||||||
**Profile**
|
**Profile**
|
||||||
|
Reference in New Issue
Block a user