mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-11 20:57:49 +02:00
* #24 Implement readonly bookmark API * #24 Implement create/update bookmark API * #24 Fix title, description not allowing blank values * #24 Code cleanup * #24 Add modification dates to response * #24 Add API docs * #24 Implement delete bookmark API * #24 Fix API docs link * #24 Fix API docs link * #24 Implement tag API Co-authored-by: Sascha Ißbrücker <sissbruecker@lyska.io>
This commit is contained in:
0
bookmarks/api/__init__.py
Normal file
0
bookmarks/api/__init__.py
Normal file
47
bookmarks/api/routes.py
Normal file
47
bookmarks/api/routes.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from rest_framework import viewsets, mixins
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.api.serializers import BookmarkSerializer, TagSerializer
|
||||
from bookmarks.models import Bookmark, Tag
|
||||
|
||||
|
||||
class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin):
|
||||
serializer_class = BookmarkSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
# For list action, use query set that applies search and tag projections
|
||||
if self.action == 'list':
|
||||
query_string = self.request.GET.get('q')
|
||||
return queries.query_bookmarks(user, query_string)
|
||||
|
||||
# For single entity actions use default query set without projections
|
||||
return Bookmark.objects.all().filter(owner=user)
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {'user': self.request.user}
|
||||
|
||||
|
||||
class TagViewSet(viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.CreateModelMixin):
|
||||
serializer_class = TagSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return Tag.objects.all().filter(owner=user)
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {'user': self.request.user}
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'bookmarks', BookmarkViewSet, basename='bookmark')
|
||||
router.register(r'tags', TagViewSet, basename='tag')
|
44
bookmarks/api/serializers.py
Normal file
44
bookmarks/api/serializers.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from bookmarks.models import Bookmark, Tag, build_tag_string
|
||||
from bookmarks.services.bookmarks import create_bookmark, update_bookmark
|
||||
from bookmarks.services.tags import get_or_create_tag
|
||||
|
||||
|
||||
class TagListField(serializers.ListField):
|
||||
child = serializers.CharField()
|
||||
|
||||
|
||||
class BookmarkSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
fields = ['id', 'url', 'title', 'description', 'tag_names', 'date_added', 'date_modified']
|
||||
read_only_fields = ['date_added', 'date_modified']
|
||||
|
||||
# Override readonly tag_names property to allow passing a list of tag names to create/update
|
||||
tag_names = TagListField()
|
||||
|
||||
def create(self, validated_data):
|
||||
bookmark = Bookmark()
|
||||
bookmark.url = validated_data['url']
|
||||
bookmark.title = validated_data['title']
|
||||
bookmark.description = validated_data['description']
|
||||
tag_string = build_tag_string(validated_data['tag_names'], ' ')
|
||||
return create_bookmark(bookmark, tag_string, self.context['user'])
|
||||
|
||||
def update(self, instance: Bookmark, validated_data):
|
||||
instance.url = validated_data['url']
|
||||
instance.title = validated_data['title']
|
||||
instance.description = validated_data['description']
|
||||
tag_string = build_tag_string(validated_data['tag_names'], ' ')
|
||||
return update_bookmark(instance, tag_string, self.context['user'])
|
||||
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'name', 'date_added']
|
||||
read_only_fields = ['date_added']
|
||||
|
||||
def create(self, validated_data):
|
||||
return get_or_create_tag(validated_data['name'], self.context['user'])
|
23
bookmarks/migrations/0004_auto_20200926_1028.py
Normal file
23
bookmarks/migrations/0004_auto_20200926_1028.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.13 on 2020-09-26 10:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0003_auto_20200913_0656'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bookmark',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, max_length=512),
|
||||
),
|
||||
]
|
@@ -30,8 +30,8 @@ def build_tag_string(tag_names: List[str], delimiter: str = ','):
|
||||
|
||||
class Bookmark(models.Model):
|
||||
url = models.URLField(max_length=2048)
|
||||
title = models.CharField(max_length=512)
|
||||
description = models.TextField()
|
||||
title = models.CharField(max_length=512, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
website_title = models.CharField(max_length=512, blank=True, null=True)
|
||||
website_description = models.TextField(blank=True, null=True)
|
||||
unread = models.BooleanField(default=True)
|
||||
|
@@ -1,21 +1,19 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
from bookmarks.models import Bookmark, BookmarkForm, parse_tag_string
|
||||
from bookmarks.models import Bookmark, parse_tag_string
|
||||
from bookmarks.services.tags import get_or_create_tags
|
||||
from bookmarks.services.website_loader import load_website_metadata
|
||||
|
||||
|
||||
def create_bookmark(form: BookmarkForm, current_user: User):
|
||||
def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User):
|
||||
# If URL is already bookmarked, then update it
|
||||
existing_bookmark = Bookmark.objects.filter(owner=current_user, url=form.data['url']).first()
|
||||
existing_bookmark: Bookmark = Bookmark.objects.filter(owner=current_user, url=bookmark.url).first()
|
||||
|
||||
if existing_bookmark is not None:
|
||||
update_form = BookmarkForm(data=form.data, instance=existing_bookmark)
|
||||
update_bookmark(update_form, current_user)
|
||||
return
|
||||
_merge_bookmark_data(bookmark, existing_bookmark)
|
||||
return update_bookmark(existing_bookmark, tag_string, current_user)
|
||||
|
||||
bookmark = form.save(commit=False)
|
||||
# Update website info
|
||||
_update_website_metadata(bookmark)
|
||||
# Set currently logged in user as owner
|
||||
@@ -25,19 +23,25 @@ def create_bookmark(form: BookmarkForm, current_user: User):
|
||||
bookmark.date_modified = timezone.now()
|
||||
bookmark.save()
|
||||
# Update tag list
|
||||
_update_bookmark_tags(bookmark, form.data['tag_string'], current_user)
|
||||
_update_bookmark_tags(bookmark, tag_string, current_user)
|
||||
bookmark.save()
|
||||
return bookmark
|
||||
|
||||
|
||||
def update_bookmark(form: BookmarkForm, current_user: User):
|
||||
bookmark = form.save(commit=False)
|
||||
def update_bookmark(bookmark: Bookmark, tag_string, current_user: User):
|
||||
# Update website info
|
||||
_update_website_metadata(bookmark)
|
||||
# Update tag list
|
||||
_update_bookmark_tags(bookmark, form.data['tag_string'], current_user)
|
||||
_update_bookmark_tags(bookmark, tag_string, current_user)
|
||||
# Update dates
|
||||
bookmark.date_modified = timezone.now()
|
||||
bookmark.save()
|
||||
return bookmark
|
||||
|
||||
|
||||
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
||||
to_bookmark.title = from_bookmark.title
|
||||
to_bookmark.description = from_bookmark.description
|
||||
|
||||
|
||||
def _update_website_metadata(bookmark: Bookmark):
|
||||
|
@@ -51,6 +51,22 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{# API token section #}
|
||||
<section class="content-area">
|
||||
<div class="content-area-header">
|
||||
<h2>API Token</h2>
|
||||
</div>
|
||||
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
|
||||
<div class="form-group">
|
||||
<div class="columns">
|
||||
<div class="column col-6 col-md-12">
|
||||
<input class="form-input" value="{{ api_token }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>Please treat this token as you would any other credential.</strong> Any party with access to this token can access and manage all your bookmarks.</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from bookmarks.api.routes import router
|
||||
from bookmarks import views
|
||||
|
||||
app_name = 'bookmarks'
|
||||
@@ -21,4 +22,5 @@ urlpatterns = [
|
||||
path('settings/export', views.settings.bookmark_export, name='settings.export'),
|
||||
# API
|
||||
path('api/check_url', views.api.check_url, name='api.check_url'),
|
||||
url(r'^api/', include(router.urls))
|
||||
]
|
||||
|
@@ -61,7 +61,7 @@ def new(request):
|
||||
auto_close = form.data['auto_close']
|
||||
if form.is_valid():
|
||||
current_user = request.user
|
||||
create_bookmark(form, current_user)
|
||||
create_bookmark(form.save(commit=False), form.data['tag_string'], current_user)
|
||||
if auto_close:
|
||||
return HttpResponseRedirect(reverse('bookmarks:close'))
|
||||
else:
|
||||
@@ -92,7 +92,7 @@ def edit(request, bookmark_id: int):
|
||||
form = BookmarkForm(request.POST, instance=bookmark)
|
||||
return_url = form.data['return_url']
|
||||
if form.is_valid():
|
||||
update_bookmark(form, request.user)
|
||||
update_bookmark(form.save(commit=False), form.data['tag_string'], request.user)
|
||||
return HttpResponseRedirect(return_url)
|
||||
else:
|
||||
return_url = request.GET.get('return_url')
|
||||
|
@@ -5,6 +5,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from bookmarks.queries import query_bookmarks
|
||||
from bookmarks.services.exporter import export_netscape_html
|
||||
@@ -17,9 +18,11 @@ logger = logging.getLogger(__name__)
|
||||
def index(request):
|
||||
import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success')
|
||||
import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors')
|
||||
api_token = Token.objects.get_or_create(user=request.user)[0]
|
||||
return render(request, 'settings/index.html', {
|
||||
'import_success_message': import_success_message,
|
||||
'import_errors_message': import_errors_message,
|
||||
'api_token': api_token.key
|
||||
})
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user