#24 Implement REST API (#32)

* #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:
Sascha Ißbrücker
2020-09-27 09:34:56 +02:00
committed by GitHub
parent 7fb73111b2
commit e497bcb5c0
15 changed files with 349 additions and 16 deletions

View File

47
bookmarks/api/routes.py Normal file
View 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')

View 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'])

View 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),
),
]

View File

@@ -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)

View File

@@ -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):

View File

@@ -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 %}

View File

@@ -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))
]

View File

@@ -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')

View File

@@ -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
})