Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bookmark sharing #311

Merged
merged 16 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion bookmarks/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from bookmarks import queries
from bookmarks.api.serializers import BookmarkSerializer, TagSerializer
from bookmarks.models import Bookmark, Tag
from bookmarks.models import Bookmark, BookmarkFilters, Tag, User
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
from bookmarks.services.website_loader import load_website_metadata

Expand Down Expand Up @@ -42,6 +42,16 @@ def archived(self, request):
data = serializer(page, many=True).data
return self.get_paginated_response(data)

@action(methods=['get'], detail=False)
def shared(self, request):
filters = BookmarkFilters(request)
user = User.objects.filter(username=filters.user).first()
query_set = queries.query_shared_bookmarks(user, filters.query)
page = self.paginate_queryset(query_set)
serializer = self.get_serializer_class()
data = serializer(page, many=True).data
return self.get_paginated_response(data)

@action(methods=['post'], detail=True)
def archive(self, request, pk):
bookmark = self.get_object()
Expand Down
5 changes: 4 additions & 1 deletion bookmarks/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Meta:
'website_description',
'is_archived',
'unread',
'shared',
'tag_names',
'date_added',
'date_modified'
Expand All @@ -37,6 +38,7 @@ class Meta:
description = serializers.CharField(required=False, allow_blank=True, default='')
is_archived = serializers.BooleanField(required=False, default=False)
unread = serializers.BooleanField(required=False, default=False)
shared = serializers.BooleanField(required=False, default=False)
# Override readonly tag_names property to allow passing a list of tag names to create/update
tag_names = TagListField(required=False, default=[])

Expand All @@ -47,12 +49,13 @@ def create(self, validated_data):
bookmark.description = validated_data['description']
bookmark.is_archived = validated_data['is_archived']
bookmark.unread = validated_data['unread']
bookmark.shared = validated_data['shared']
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):
# Update fields if they were provided in the payload
for key in ['url', 'title', 'description', 'unread']:
for key in ['url', 'title', 'description', 'unread', 'shared']:
if key in validated_data:
setattr(instance, key, validated_data[key])

Expand Down
12 changes: 8 additions & 4 deletions bookmarks/components/SearchAutoComplete.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
export let placeholder;
export let value;
export let tags;
export let mode = 'default';
export let mode = '';
export let apiClient;
export let filters;

let isFocus = false;
let isOpen = false;
Expand Down Expand Up @@ -112,9 +113,12 @@
let bookmarks = []

if (value && value.length >= 3) {
const fetchedBookmarks = mode === 'archive'
? await apiClient.getArchivedBookmarks(value, {limit: 5, offset: 0})
: await apiClient.getBookmarks(value, {limit: 5, offset: 0})
const path = mode ? `/${mode}` : ''
const suggestionFilters = {
...filters,
q: value
}
const fetchedBookmarks = await apiClient.listBookmarks(suggestionFilters, {limit: 5, offset: 0, path})
bookmarks = fetchedBookmarks.map(bookmark => {
const fullLabel = bookmark.title || bookmark.website_title || bookmark.url
const label = clampText(fullLabel, 60)
Expand Down
25 changes: 13 additions & 12 deletions bookmarks/components/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ export class ApiClient {
this.baseUrl = baseUrl
}

getBookmarks(query, options = {limit: 100, offset: 0}) {
const encodedQuery = encodeURIComponent(query)
const url = `${this.baseUrl}bookmarks/?q=${encodedQuery}&limit=${options.limit}&offset=${options.offset}`

return fetch(url)
.then(response => response.json())
.then(data => data.results)
}

getArchivedBookmarks(query, options = {limit: 100, offset: 0}) {
const encodedQuery = encodeURIComponent(query)
const url = `${this.baseUrl}bookmarks/archived/?q=${encodedQuery}&limit=${options.limit}&offset=${options.offset}`
listBookmarks(filters, options = {limit: 100, offset: 0, path: ''}) {
const query = [
`limit=${options.limit}`,
`offset=${options.offset}`,
]
Object.keys(filters).forEach(key => {
const value = filters[key]
if (value) {
query.push(`${key}=${encodeURIComponent(value)}`)
}
})
const queryString = query.join('&')
const url = `${this.baseUrl}bookmarks${options.path}/?${queryString}`

return fetch(url)
.then(response => response.json())
Expand Down
18 changes: 18 additions & 0 deletions bookmarks/migrations/0016_bookmark_shared.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-08-02 18:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('bookmarks', '0015_feedtoken'),
]

operations = [
migrations.AddField(
model_name='bookmark',
name='shared',
field=models.BooleanField(default=False),
),
]
18 changes: 18 additions & 0 deletions bookmarks/migrations/0017_userprofile_enable_sharing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-08-04 09:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('bookmarks', '0016_bookmark_shared'),
]

operations = [
migrations.AddField(
model_name='userprofile',
name='enable_sharing',
field=models.BooleanField(default=False),
),
]
14 changes: 12 additions & 2 deletions bookmarks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.core.handlers.wsgi import WSGIRequest
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
Expand Down Expand Up @@ -54,6 +55,7 @@ class Bookmark(models.Model):
web_archive_snapshot_url = models.CharField(max_length=2048, blank=True)
unread = models.BooleanField(default=False)
is_archived = models.BooleanField(default=False)
shared = models.BooleanField(default=False)
date_added = models.DateTimeField()
date_modified = models.DateTimeField()
date_accessed = models.DateTimeField(blank=True, null=True)
Expand Down Expand Up @@ -100,12 +102,19 @@ class BookmarkForm(forms.ModelForm):
description = forms.CharField(required=False,
widget=forms.Textarea())
unread = forms.BooleanField(required=False)
shared = forms.BooleanField(required=False)
# Hidden field that determines whether to close window/tab after saving the bookmark
auto_close = forms.CharField(required=False)

class Meta:
model = Bookmark
fields = ['url', 'tag_string', 'title', 'description', 'unread', 'auto_close']
fields = ['url', 'tag_string', 'title', 'description', 'unread', 'shared', 'auto_close']


class BookmarkFilters:
def __init__(self, request: WSGIRequest):
self.query = request.GET.get('q') or ''
self.user = request.GET.get('user') or ''


class UserProfile(models.Model):
Expand Down Expand Up @@ -145,12 +154,13 @@ class UserProfile(models.Model):
default=BOOKMARK_LINK_TARGET_BLANK)
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
default=WEB_ARCHIVE_INTEGRATION_DISABLED)
enable_sharing = models.BooleanField(default=False, null=False)


class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration']
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'enable_sharing']


@receiver(post_save, sender=get_user_model())
Expand Down
29 changes: 27 additions & 2 deletions bookmarks/queries.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

from django.contrib.auth.models import User
from django.db.models import Q, Count, Aggregate, CharField, Value, BooleanField, QuerySet

Expand Down Expand Up @@ -27,15 +29,22 @@ def query_archived_bookmarks(user: User, query_string: str) -> QuerySet:
.filter(is_archived=True)


def _base_bookmarks_query(user: User, query_string: str) -> QuerySet:
def query_shared_bookmarks(user: Optional[User], query_string: str) -> QuerySet:
return _base_bookmarks_query(user, query_string) \
.filter(shared=True) \
.filter(owner__profile__enable_sharing=True)


def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
# Add aggregated tag info to bookmark instances
query_set = Bookmark.objects \
.annotate(tag_count=Count('tags'),
tag_string=Concat('tags__name'),
tag_projection=Value(True, BooleanField()))

# Filter for user
query_set = query_set.filter(owner=user)
if user:
query_set = query_set.filter(owner=user)

# Split query into search terms and tags
query = _parse_query_string(query_string)
Expand Down Expand Up @@ -88,6 +97,22 @@ def query_archived_bookmark_tags(user: User, query_string: str) -> QuerySet:
return query_set.distinct()


def query_shared_bookmark_tags(user: Optional[User], query_string: str) -> QuerySet:
bookmarks_query = query_shared_bookmarks(user, query_string)

query_set = Tag.objects.filter(bookmark__in=bookmarks_query)

return query_set.distinct()


def query_shared_bookmark_users(query_string: str) -> QuerySet:
bookmarks_query = query_shared_bookmarks(None, query_string)

query_set = User.objects.filter(bookmark__in=bookmarks_query)

return query_set.distinct()


def get_user_tags(user: User):
return Tag.objects.filter(owner=user).all()

Expand Down
1 change: 1 addition & 0 deletions bookmarks/services/bookmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
to_bookmark.title = from_bookmark.title
to_bookmark.description = from_bookmark.description
to_bookmark.unread = from_bookmark.unread
to_bookmark.shared = from_bookmark.shared


def _update_website_metadata(bookmark: Bookmark):
Expand Down
2 changes: 1 addition & 1 deletion bookmarks/templates/bookmarks/archive.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="content-area-header">
<h2>Archived bookmarks</h2>
<div class="spacer"></div>
{% bookmark_search query tags mode='archive' %}
{% bookmark_search filters tags mode='archived' %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
</div>

Expand Down
36 changes: 22 additions & 14 deletions bookmarks/templates/bookmarks/bookmark_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,29 @@
</span>
<span class="text-gray text-sm">|</span>
{% endif %}
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
class="btn btn-link btn-sm">Edit</a>
{% if bookmark.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Unarchive</button>
{% if bookmark.owner == request.user %}
{# Bookmark owner actions #}
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
class="btn btn-link btn-sm">Edit</a>
{% if bookmark.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Unarchive</button>
{% else %}
<button type="submit" name="archive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Archive</button>
{% endif %}
<button type="submit" name="remove" value="{{ bookmark.id }}"
class="btn btn-link btn-sm btn-confirmation">Remove</button>
{% if bookmark.unread %}
<span class="text-gray text-sm">|</span>
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Mark as read</button>
{% endif %}
{% else %}
<button type="submit" name="archive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Archive</button>
{% endif %}
<button type="submit" name="remove" value="{{ bookmark.id }}"
class="btn btn-link btn-sm btn-confirmation">Remove</button>
{% if bookmark.unread %}
<span class="text-gray text-sm">|</span>
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Mark as read</button>
{# Shared bookmark actions #}
<span class="text-gray text-sm">Shared by
<a class="text-gray" href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
</span>
{% endif %}
</div>
</li>
Expand Down
12 changes: 12 additions & 0 deletions bookmarks/templates/bookmarks/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@
Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them.
</div>
</div>
{% if request.user.profile.enable_sharing %}
<div class="form-group">
<label for="{{ form.shared.id_for_label }}" class="form-checkbox">
{{ form.shared }}
<i class="form-icon"></i>
<span>Share</span>
</label>
<div class="form-input-hint">
Share this bookmark with other users.
</div>
</div>
{% endif %}
<br/>
<div class="form-group">
{% if auto_close %}
Expand Down
2 changes: 1 addition & 1 deletion bookmarks/templates/bookmarks/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="content-area-header">
<h2>Bookmarks</h2>
<div class="spacer"></div>
{% bookmark_search query tags %}
{% bookmark_search filters tags %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
</div>

Expand Down
10 changes: 10 additions & 0 deletions bookmarks/templates/bookmarks/nav_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
<li>
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
</li>
{% if request.user.profile.enable_sharing %}
<li>
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
</li>
{% endif %}
<li>
<a href="{% url 'bookmarks:index' %}?q=!unread" class="btn btn-link">Unread</a>
</li>
Expand Down Expand Up @@ -47,6 +52,11 @@
<li style="padding-left: 1rem">
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
</li>
{% if request.user.profile.enable_sharing %}
<li style="padding-left: 1rem">
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
</li>
{% endif %}
<li style="padding-left: 1rem">
<a href="{% url 'bookmarks:index' %}?q=!unread" class="btn btn-link">Unread</a>
</li>
Expand Down
Loading