mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-08 03:08:29 +02:00
Add RSS feeds (#305)
* Add basic unread bookmarks feed * Generate user-specific feed * Add feed tests * Add all bookmarks feed * Add feed token admin * Add note about renewing URLs * Add support for query parameter * Fix rebase issues * Improve docs on feeds integration Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
This commit is contained in:
@@ -9,7 +9,7 @@ from django.utils.translation import ngettext, gettext
|
|||||||
from rest_framework.authtoken.admin import TokenAdmin
|
from rest_framework.authtoken.admin import TokenAdmin
|
||||||
from rest_framework.authtoken.models import TokenProxy
|
from rest_framework.authtoken.models import TokenProxy
|
||||||
|
|
||||||
from bookmarks.models import Bookmark, Tag, UserProfile, Toast
|
from bookmarks.models import Bookmark, Tag, UserProfile, Toast, FeedToken
|
||||||
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
||||||
|
|
||||||
|
|
||||||
@@ -121,11 +121,18 @@ class AdminToast(admin.ModelAdmin):
|
|||||||
list_filter = ('owner__username',)
|
list_filter = ('owner__username',)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminFeedToken(admin.ModelAdmin):
|
||||||
|
list_display = ('key', 'user')
|
||||||
|
search_fields = ['key']
|
||||||
|
list_filter = ('user__username',)
|
||||||
|
|
||||||
|
|
||||||
linkding_admin_site = LinkdingAdminSite()
|
linkding_admin_site = LinkdingAdminSite()
|
||||||
linkding_admin_site.register(Bookmark, AdminBookmark)
|
linkding_admin_site.register(Bookmark, AdminBookmark)
|
||||||
linkding_admin_site.register(Tag, AdminTag)
|
linkding_admin_site.register(Tag, AdminTag)
|
||||||
linkding_admin_site.register(User, AdminCustomUser)
|
linkding_admin_site.register(User, AdminCustomUser)
|
||||||
linkding_admin_site.register(TokenProxy, TokenAdmin)
|
linkding_admin_site.register(TokenProxy, TokenAdmin)
|
||||||
linkding_admin_site.register(Toast, AdminToast)
|
linkding_admin_site.register(Toast, AdminToast)
|
||||||
|
linkding_admin_site.register(FeedToken, AdminFeedToken)
|
||||||
linkding_admin_site.register(Task, TaskAdmin)
|
linkding_admin_site.register(Task, TaskAdmin)
|
||||||
linkding_admin_site.register(CompletedTask, CompletedTaskAdmin)
|
linkding_admin_site.register(CompletedTask, CompletedTaskAdmin)
|
||||||
|
56
bookmarks/feeds.py
Normal file
56
bookmarks/feeds.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from django.contrib.syndication.views import Feed
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from bookmarks.models import Bookmark, FeedToken
|
||||||
|
from bookmarks import queries
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FeedContext:
|
||||||
|
feed_token: FeedToken
|
||||||
|
query_set: QuerySet[Bookmark]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBookmarksFeed(Feed):
|
||||||
|
def get_object(self, request, feed_key: str):
|
||||||
|
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
||||||
|
query_string = request.GET.get('q')
|
||||||
|
query_set = queries.query_bookmarks(feed_token.user, query_string)
|
||||||
|
return FeedContext(feed_token, query_set)
|
||||||
|
|
||||||
|
def item_title(self, item: Bookmark):
|
||||||
|
return item.resolved_title
|
||||||
|
|
||||||
|
def item_description(self, item: Bookmark):
|
||||||
|
return item.resolved_description
|
||||||
|
|
||||||
|
def item_link(self, item: Bookmark):
|
||||||
|
return item.url
|
||||||
|
|
||||||
|
def item_pubdate(self, item: Bookmark):
|
||||||
|
return item.date_added
|
||||||
|
|
||||||
|
|
||||||
|
class AllBookmarksFeed(BaseBookmarksFeed):
|
||||||
|
title = 'All bookmarks'
|
||||||
|
description = 'All bookmarks'
|
||||||
|
|
||||||
|
def link(self, context: FeedContext):
|
||||||
|
return reverse('bookmarks:feeds.all', args=[context.feed_token.key])
|
||||||
|
|
||||||
|
def items(self, context: FeedContext):
|
||||||
|
return context.query_set
|
||||||
|
|
||||||
|
|
||||||
|
class UnreadBookmarksFeed(BaseBookmarksFeed):
|
||||||
|
title = 'Unread bookmarks'
|
||||||
|
description = 'All unread bookmarks'
|
||||||
|
|
||||||
|
def link(self, context: FeedContext):
|
||||||
|
return reverse('bookmarks:feeds.unread', args=[context.feed_token.key])
|
||||||
|
|
||||||
|
def items(self, context: FeedContext):
|
||||||
|
return context.query_set.filter(unread=True)
|
24
bookmarks/migrations/0015_feedtoken.py
Normal file
24
bookmarks/migrations/0015_feedtoken.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2.13 on 2022-07-23 20:35
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('bookmarks', '0014_alter_bookmark_unread'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FeedToken',
|
||||||
|
fields=[
|
||||||
|
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feed_token', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@@ -1,3 +1,5 @@
|
|||||||
|
import binascii
|
||||||
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@@ -167,3 +169,27 @@ class Toast(models.Model):
|
|||||||
message = models.TextField()
|
message = models.TextField()
|
||||||
acknowledged = models.BooleanField(default=False)
|
acknowledged = models.BooleanField(default=False)
|
||||||
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedToken(models.Model):
|
||||||
|
"""
|
||||||
|
Adapted from authtoken.models.Token
|
||||||
|
"""
|
||||||
|
key = models.CharField(max_length=40, primary_key=True)
|
||||||
|
user = models.OneToOneField(get_user_model(),
|
||||||
|
related_name='feed_token',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.key:
|
||||||
|
self.key = self.generate_key()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_key(cls):
|
||||||
|
return binascii.hexlify(os.urandom(20)).decode()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.key
|
||||||
|
@@ -38,9 +38,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p><strong>Please treat this token as you would any other credential.</strong> Any party with access to this
|
<p>
|
||||||
token can access and manage all your bookmarks.</p>
|
<strong>Please treat this token as you would any other credential.</strong>
|
||||||
<p>If you think that a token was compromised you can revoke (delete) it in the <a href="{% url 'admin:authtoken_tokenproxy_changelist' %}">admin panel</a>. After deleting the token, a new one will be generated when you reload this settings page.</p>
|
Any party with access to this token can access and manage all your bookmarks.
|
||||||
|
If you think that a token was compromised you can revoke (delete) it in the <a href="{% url 'admin:authtoken_tokenproxy_changelist' %}">admin panel</a>.
|
||||||
|
After deleting the token, a new one will be generated when you reload this settings page.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="content-area">
|
||||||
|
<h2>RSS Feeds</h2>
|
||||||
|
<p>The following URLs provide RSS feeds for your bookmarks:</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ all_feed_url }}">All bookmarks</a></li>
|
||||||
|
<li><a href="{{ unread_feed_url }}">Unread bookmarks</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
All URLs support appending a <code>q</code> URL parameter for specifying a search query.
|
||||||
|
You can get an example by doing a search in the bookmarks view and then copying the parameter from the URL.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Please note that these URLs include an authentication token that should be treated like any other credential.</strong>
|
||||||
|
Any party with access to these URLs can read all your bookmarks.
|
||||||
|
If you think that a URL was compromised you can delete the feed token for your user in the <a href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>.
|
||||||
|
After deleting the feed token, new URLs will be generated when you reload this settings page.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from typing import List
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -33,6 +32,8 @@ class BookmarkFactoryMixin:
|
|||||||
website_description: str = '',
|
website_description: str = '',
|
||||||
web_archive_snapshot_url: str = '',
|
web_archive_snapshot_url: str = '',
|
||||||
):
|
):
|
||||||
|
if not title:
|
||||||
|
title = get_random_string(length=32)
|
||||||
if tags is None:
|
if tags is None:
|
||||||
tags = []
|
tags = []
|
||||||
if user is None:
|
if user is None:
|
||||||
|
191
bookmarks/tests/test_feeds.py
Normal file
191
bookmarks/tests/test_feeds.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import datetime
|
||||||
|
import email
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
from bookmarks.models import FeedToken, User
|
||||||
|
|
||||||
|
|
||||||
|
def rfc2822_date(date):
|
||||||
|
if not isinstance(date, datetime.datetime):
|
||||||
|
date = datetime.datetime.combine(date, datetime.time())
|
||||||
|
return email.utils.format_datetime(date)
|
||||||
|
|
||||||
|
|
||||||
|
class FeedsTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
user = self.get_or_create_test_user()
|
||||||
|
self.client.force_login(user)
|
||||||
|
self.token = FeedToken.objects.get_or_create(user=user)[0]
|
||||||
|
|
||||||
|
def test_all_returns_404_for_unknown_feed_token(self):
|
||||||
|
response = self.client.get(reverse('bookmarks:feeds.all', args=['foo']))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_all_metadata(self):
|
||||||
|
feed_url = reverse('bookmarks:feeds.all', args=[self.token.key])
|
||||||
|
response = self.client.get(feed_url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, '<title>All bookmarks</title>')
|
||||||
|
self.assertContains(response, '<description>All bookmarks</description>')
|
||||||
|
self.assertContains(response, f'<link>http://testserver{feed_url}</link>')
|
||||||
|
self.assertContains(response, f'<atom:link href="http://testserver{feed_url}" rel="self"></atom:link>')
|
||||||
|
|
||||||
|
def test_all_returns_all_unarchived_bookmarks(self):
|
||||||
|
bookmarks = [
|
||||||
|
self.setup_bookmark(),
|
||||||
|
self.setup_bookmark(),
|
||||||
|
self.setup_bookmark(unread=True),
|
||||||
|
]
|
||||||
|
self.setup_bookmark(is_archived=True)
|
||||||
|
self.setup_bookmark(is_archived=True)
|
||||||
|
self.setup_bookmark(is_archived=True)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:feeds.all', args=[self.token.key]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, '<item>', count=len(bookmarks))
|
||||||
|
|
||||||
|
for bookmark in bookmarks:
|
||||||
|
expected_item = '<item>' \
|
||||||
|
f'<title>{bookmark.resolved_title}</title>' \
|
||||||
|
f'<link>{bookmark.url}</link>' \
|
||||||
|
f'<description>{bookmark.resolved_description}</description>' \
|
||||||
|
f'<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>' \
|
||||||
|
f'<guid>{bookmark.url}</guid>' \
|
||||||
|
'</item>'
|
||||||
|
self.assertContains(response, expected_item, count=1)
|
||||||
|
|
||||||
|
def test_all_with_query(self):
|
||||||
|
tag1 = self.setup_tag()
|
||||||
|
bookmark1 = self.setup_bookmark()
|
||||||
|
bookmark2 = self.setup_bookmark(tags=[tag1])
|
||||||
|
bookmark3 = self.setup_bookmark(tags=[tag1])
|
||||||
|
|
||||||
|
self.setup_bookmark()
|
||||||
|
self.setup_bookmark()
|
||||||
|
self.setup_bookmark()
|
||||||
|
|
||||||
|
feed_url = reverse('bookmarks:feeds.all', args=[self.token.key])
|
||||||
|
|
||||||
|
url = feed_url + f'?q={bookmark1.title}'
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '<item>', count=1)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark1.url}</guid>', count=1)
|
||||||
|
|
||||||
|
url = feed_url + '?q=' + urllib.parse.quote('#' + tag1.name)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '<item>', count=2)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark3.url}</guid>', count=1)
|
||||||
|
|
||||||
|
url = feed_url + '?q=' + urllib.parse.quote(f'#{tag1.name} {bookmark2.title}')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '<item>', count=1)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1)
|
||||||
|
|
||||||
|
def test_all_returns_only_user_owned_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:feeds.all', args=[self.token.key]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, '<item>', count=0)
|
||||||
|
|
||||||
|
def test_unread_returns_404_for_unknown_feed_token(self):
|
||||||
|
response = self.client.get(reverse('bookmarks:feeds.unread', args=['foo']))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_unread_metadata(self):
|
||||||
|
feed_url = reverse('bookmarks:feeds.unread', args=[self.token.key])
|
||||||
|
response = self.client.get(feed_url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, '<title>Unread bookmarks</title>')
|
||||||
|
self.assertContains(response, '<description>All unread bookmarks</description>')
|
||||||
|
self.assertContains(response, f'<link>http://testserver{feed_url}</link>')
|
||||||
|
self.assertContains(response, f'<atom:link href="http://testserver{feed_url}" rel="self"></atom:link>')
|
||||||
|
|
||||||
|
def test_unread_returns_unread_and_unarchived_bookmarks(self):
|
||||||
|
self.setup_bookmark(unread=False)
|
||||||
|
self.setup_bookmark(unread=False)
|
||||||
|
self.setup_bookmark(unread=False)
|
||||||
|
self.setup_bookmark(unread=True, is_archived=True)
|
||||||
|
self.setup_bookmark(unread=True, is_archived=True)
|
||||||
|
self.setup_bookmark(unread=False, is_archived=True)
|
||||||
|
|
||||||
|
unread_bookmarks = [
|
||||||
|
self.setup_bookmark(unread=True),
|
||||||
|
self.setup_bookmark(unread=True),
|
||||||
|
self.setup_bookmark(unread=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:feeds.unread', args=[self.token.key]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, '<item>', count=len(unread_bookmarks))
|
||||||
|
|
||||||
|
for bookmark in unread_bookmarks:
|
||||||
|
expected_item = '<item>' \
|
||||||
|
f'<title>{bookmark.resolved_title}</title>' \
|
||||||
|
f'<link>{bookmark.url}</link>' \
|
||||||
|
f'<description>{bookmark.resolved_description}</description>' \
|
||||||
|
f'<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>' \
|
||||||
|
f'<guid>{bookmark.url}</guid>' \
|
||||||
|
'</item>'
|
||||||
|
self.assertContains(response, expected_item, count=1)
|
||||||
|
|
||||||
|
def test_unread_with_query(self):
|
||||||
|
tag1 = self.setup_tag()
|
||||||
|
bookmark1 = self.setup_bookmark(unread=True)
|
||||||
|
bookmark2 = self.setup_bookmark(unread=True, tags=[tag1])
|
||||||
|
bookmark3 = self.setup_bookmark(unread=True, tags=[tag1])
|
||||||
|
|
||||||
|
self.setup_bookmark(unread=True)
|
||||||
|
self.setup_bookmark(unread=True)
|
||||||
|
self.setup_bookmark(unread=True)
|
||||||
|
|
||||||
|
feed_url = reverse('bookmarks:feeds.all', args=[self.token.key])
|
||||||
|
|
||||||
|
url = feed_url + f'?q={bookmark1.title}'
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '<item>', count=1)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark1.url}</guid>', count=1)
|
||||||
|
|
||||||
|
url = feed_url + '?q=' + urllib.parse.quote('#' + tag1.name)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '<item>', count=2)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark3.url}</guid>', count=1)
|
||||||
|
|
||||||
|
url = feed_url + '?q=' + urllib.parse.quote(f'#{tag1.name} {bookmark2.title}')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '<item>', count=1)
|
||||||
|
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1)
|
||||||
|
|
||||||
|
def test_unread_returns_only_user_owned_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
self.setup_bookmark(unread=True, user=other_user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:feeds.unread', args=[self.token.key]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, '<item>', count=0)
|
@@ -3,6 +3,7 @@ from django.urls import reverse
|
|||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
from bookmarks.models import FeedToken
|
||||||
|
|
||||||
|
|
||||||
class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
@@ -38,3 +39,28 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.client.get(reverse('bookmarks:settings.integrations'))
|
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||||
|
|
||||||
self.assertEqual(Token.objects.count(), 1)
|
self.assertEqual(Token.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_should_generate_feed_token_if_not_exists(self):
|
||||||
|
self.assertEqual(FeedToken.objects.count(), 0)
|
||||||
|
|
||||||
|
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||||
|
|
||||||
|
self.assertEqual(FeedToken.objects.count(), 1)
|
||||||
|
token = FeedToken.objects.first()
|
||||||
|
self.assertEqual(token.user, self.user)
|
||||||
|
|
||||||
|
def test_should_not_generate_feed_token_if_exists(self):
|
||||||
|
FeedToken.objects.get_or_create(user=self.user)
|
||||||
|
self.assertEqual(FeedToken.objects.count(), 1)
|
||||||
|
|
||||||
|
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||||
|
|
||||||
|
self.assertEqual(FeedToken.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_should_display_feed_urls(self):
|
||||||
|
response = self.client.get(reverse('bookmarks:settings.integrations'))
|
||||||
|
html = response.content.decode()
|
||||||
|
|
||||||
|
token = FeedToken.objects.first()
|
||||||
|
self.assertInHTML(f'<a href="http://testserver/feeds/{token.key}/all">All bookmarks</a>', html)
|
||||||
|
self.assertInHTML(f'<a href="http://testserver/feeds/{token.key}/unread">Unread bookmarks</a>', html)
|
||||||
|
@@ -4,6 +4,7 @@ from django.views.generic import RedirectView
|
|||||||
|
|
||||||
from bookmarks.api.routes import router
|
from bookmarks.api.routes import router
|
||||||
from bookmarks import views
|
from bookmarks import views
|
||||||
|
from bookmarks.feeds import AllBookmarksFeed, UnreadBookmarksFeed
|
||||||
|
|
||||||
app_name = 'bookmarks'
|
app_name = 'bookmarks'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -25,5 +26,8 @@ urlpatterns = [
|
|||||||
# Toasts
|
# Toasts
|
||||||
path('toasts/acknowledge', views.toasts.acknowledge, name='toasts.acknowledge'),
|
path('toasts/acknowledge', views.toasts.acknowledge, name='toasts.acknowledge'),
|
||||||
# API
|
# API
|
||||||
path('api/', include(router.urls), name='api')
|
path('api/', include(router.urls), name='api'),
|
||||||
|
# Feeds
|
||||||
|
path('feeds/<str:feed_key>/all', AllBookmarksFeed(), name='feeds.all'),
|
||||||
|
path('feeds/<str:feed_key>/unread', UnreadBookmarksFeed(), name='feeds.unread'),
|
||||||
]
|
]
|
||||||
|
@@ -10,7 +10,7 @@ from django.shortcuts import render
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from bookmarks.models import UserProfileForm
|
from bookmarks.models import UserProfileForm, FeedToken
|
||||||
from bookmarks.queries import query_bookmarks
|
from bookmarks.queries import query_bookmarks
|
||||||
from bookmarks.services import exporter
|
from bookmarks.services import exporter
|
||||||
from bookmarks.services import importer
|
from bookmarks.services import importer
|
||||||
@@ -75,9 +75,14 @@ def get_ttl_hash(seconds=3600):
|
|||||||
def integrations(request):
|
def integrations(request):
|
||||||
application_url = request.build_absolute_uri("/bookmarks/new")
|
application_url = request.build_absolute_uri("/bookmarks/new")
|
||||||
api_token = Token.objects.get_or_create(user=request.user)[0]
|
api_token = Token.objects.get_or_create(user=request.user)[0]
|
||||||
|
feed_token = FeedToken.objects.get_or_create(user=request.user)[0]
|
||||||
|
all_feed_url = request.build_absolute_uri(reverse('bookmarks:feeds.all', args=[feed_token.key]))
|
||||||
|
unread_feed_url = request.build_absolute_uri(reverse('bookmarks:feeds.unread', args=[feed_token.key]))
|
||||||
return render(request, 'settings/integrations.html', {
|
return render(request, 'settings/integrations.html', {
|
||||||
'application_url': application_url,
|
'application_url': application_url,
|
||||||
'api_token': api_token.key
|
'api_token': api_token.key,
|
||||||
|
'all_feed_url': all_feed_url,
|
||||||
|
'unread_feed_url': unread_feed_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user