mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-27 12:01:14 +00:00
commit
2ffddeaa1f
14 changed files with 384 additions and 17 deletions
29
bookwyrm/migrations/0120_list_embed_key.py
Normal file
29
bookwyrm/migrations/0120_list_embed_key.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-12-04 10:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def gen_uuid(apps, schema_editor):
|
||||||
|
"""sets an unique UUID for embed_key"""
|
||||||
|
book_lists = apps.get_model("bookwyrm", "List")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for book_list in book_lists.objects.using(db_alias).all():
|
||||||
|
book_list.embed_key = uuid.uuid4()
|
||||||
|
book_list.save(broadcast=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0119_user_feed_status_types"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="list",
|
||||||
|
name="embed_key",
|
||||||
|
field=models.UUIDField(editable=False, null=True, unique=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
|
@ -1,4 +1,6 @@
|
||||||
""" make a list of books!! """
|
""" make a list of books!! """
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -43,6 +45,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
through="ListItem",
|
through="ListItem",
|
||||||
through_fields=("book_list", "book"),
|
through_fields=("book_list", "book"),
|
||||||
)
|
)
|
||||||
|
embed_key = models.UUIDField(unique=True, null=True, editable=False)
|
||||||
activity_serializer = activitypub.BookList
|
activity_serializer = activitypub.BookList
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
|
@ -105,6 +108,12 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
group=None, curation="closed"
|
group=None, curation="closed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""on save, update embed_key and avoid clash with existing code"""
|
||||||
|
if not self.embed_key:
|
||||||
|
self.embed_key = uuid.uuid4()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ListItem(CollectionItemMixin, BookWyrmModel):
|
class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
"""ok"""
|
"""ok"""
|
||||||
|
|
|
@ -20,6 +20,10 @@ body {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card.has-border {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
.scroll-x {
|
.scroll-x {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
|
@ -66,6 +66,9 @@ let BookWyrm = new class {
|
||||||
document.querySelectorAll('input[type="file"]').forEach(
|
document.querySelectorAll('input[type="file"]').forEach(
|
||||||
bookwyrm.disableIfTooLarge.bind(bookwyrm)
|
bookwyrm.disableIfTooLarge.bind(bookwyrm)
|
||||||
);
|
);
|
||||||
|
document.querySelectorAll('[data-copytext]').forEach(
|
||||||
|
bookwyrm.copyText.bind(bookwyrm)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,4 +448,38 @@ let BookWyrm = new class {
|
||||||
parent.appendChild(label)
|
parent.appendChild(label)
|
||||||
parent.appendChild(input)
|
parent.appendChild(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up a "click-to-copy" component from a textarea element
|
||||||
|
* with `data-copytext`, `data-copytext-label`, `data-copytext-success`
|
||||||
|
* attributes.
|
||||||
|
*
|
||||||
|
* @param {object} node - DOM node of the text container
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
|
||||||
|
copyText(textareaEl) {
|
||||||
|
const text = textareaEl.textContent;
|
||||||
|
|
||||||
|
const copyButtonEl = document.createElement('button');
|
||||||
|
|
||||||
|
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
|
||||||
|
copyButtonEl.classList.add(
|
||||||
|
"mt-2",
|
||||||
|
"button",
|
||||||
|
"is-small",
|
||||||
|
"is-fullwidth",
|
||||||
|
"is-primary",
|
||||||
|
"is-light"
|
||||||
|
);
|
||||||
|
copyButtonEl.addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
textareaEl.classList.add('is-success');
|
||||||
|
copyButtonEl.classList.replace('is-primary', 'is-success');
|
||||||
|
copyButtonEl.textContent = textareaEl.dataset.copytextSuccess;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
textareaEl.parentNode.appendChild(copyButtonEl)
|
||||||
|
}
|
||||||
}();
|
}();
|
||||||
|
|
53
bookwyrm/templates/embed-layout.html
Normal file
53
bookwyrm/templates/embed-layout.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{% load layout %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{% get_lang %}">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
|
||||||
|
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
||||||
|
<link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
|
||||||
|
|
||||||
|
<base target="_blank">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="section py-3">
|
||||||
|
<a href="/" class="is-flex is-align-items-center">
|
||||||
|
<img class="image logo is-flex-shrink-0" style="height: 32px" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
||||||
|
<span class="title is-5 ml-2">{{ site.name }}</span>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="section py-3">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="section py-3">
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'about' %}">
|
||||||
|
{% blocktrans with site_name=site.name %}About {{ site_name }}{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% if site.admin_email %}
|
||||||
|
<p>
|
||||||
|
<a href="mailto:{{ site.admin_email }}">
|
||||||
|
{% trans "Contact site admin" %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
<a href="https://joinbookwyrm.com/">
|
||||||
|
{% trans "Join Bookwyrm" %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -34,7 +34,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="/">
|
<a class="navbar-item" href="/">
|
||||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
|
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
||||||
</a>
|
</a>
|
||||||
<form class="navbar-item column" action="{% url 'search' %}">
|
<form class="navbar-item column" action="{% url 'search' %}">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
|
|
59
bookwyrm/templates/lists/embed-list.html
Normal file
59
bookwyrm/templates/lists/embed-list.html
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{% extends 'embed-layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load bookwyrm_group_tags %}
|
||||||
|
{% load markdown %}
|
||||||
|
|
||||||
|
{% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<h1 class="title is-4">
|
||||||
|
{{ list.name }}
|
||||||
|
<span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="subtitle is-size-6">
|
||||||
|
{% include 'lists/created_text.html' with list=list %}
|
||||||
|
{% blocktrans with site_name=site.name %}on <a href="/">{{ site_name }}</a>{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="block content">
|
||||||
|
{% include 'snippets/trimmed_text.html' with full=list.description %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{% if not items.object_list.exists %}
|
||||||
|
<p>{% trans "This list is currently empty" %}</p>
|
||||||
|
{% else %}
|
||||||
|
<ol start="{{ items.start_index }}" class="ordered-list">
|
||||||
|
{% for item in items %}
|
||||||
|
{% with book=item.book %}
|
||||||
|
<li class="mb-5 card is-shadowless has-border">
|
||||||
|
<div class="card-content p-0 mb-0 columns is-gapless is-mobile">
|
||||||
|
<div class="column is-3-mobile is-2-tablet is-cover align to-t">
|
||||||
|
<a href="{{ item.book.local_path }}" aria-hidden="true">
|
||||||
|
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' size='medium' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column mx-3 my-2">
|
||||||
|
<h2 class="title is-6 mb-1">
|
||||||
|
{% include 'snippets/book_titleby.html' %}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% endif %}
|
||||||
|
{% include "snippets/pagination.html" with page=items %}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -186,6 +186,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<h2 class="title is-5 mt-6" id="embed-label">
|
||||||
|
{% trans "Embed this list on a website" %}
|
||||||
|
</h2>
|
||||||
|
<textarea readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}"><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans with list_name=list.name site_name=site.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}} on {{ site_name }}{% endblocktrans %}" src="{{ embed_url }}"></iframe></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" testing models """
|
""" testing models """
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from bookwyrm import models, settings
|
from bookwyrm import models, settings
|
||||||
|
|
||||||
|
@ -80,3 +81,12 @@ class List(TestCase):
|
||||||
self.assertEqual(item.book_list.privacy, "public")
|
self.assertEqual(item.book_list.privacy, "public")
|
||||||
self.assertEqual(item.privacy, "direct")
|
self.assertEqual(item.privacy, "direct")
|
||||||
self.assertEqual(item.recipients, [])
|
self.assertEqual(item.recipients, [])
|
||||||
|
|
||||||
|
def test_embed_key(self, _):
|
||||||
|
"""embed_key should never be empty"""
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
|
book_list = models.List.objects.create(
|
||||||
|
name="Test List", user=self.local_user
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(book_list.embed_key, UUID)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.http.response import Http404
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -385,3 +386,46 @@ class ListViews(TestCase):
|
||||||
|
|
||||||
result = view(request, self.local_user.username)
|
result = view(request, self.local_user.username)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
|
||||||
|
def test_embed_call_without_key(self):
|
||||||
|
"""there are so many views, this just makes sure it DOESN’T load"""
|
||||||
|
view = views.unsafe_embed_list
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
with self.assertRaises(Http404):
|
||||||
|
result = view(request, self.list.id, "")
|
||||||
|
|
||||||
|
def test_embed_call_with_key(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.unsafe_embed_list
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
|
models.ListItem.objects.create(
|
||||||
|
book_list=self.list,
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
approved=True,
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
embed_key = str(self.list.embed_key.hex)
|
||||||
|
|
||||||
|
with patch("bookwyrm.views.list.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.list.id, embed_key)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
|
@ -337,6 +337,11 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
|
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
|
||||||
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
|
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
|
||||||
|
re_path(
|
||||||
|
r"^list/(?P<list_id>\d+)/embed/(?P<list_key>[0-9a-f]+)?$",
|
||||||
|
views.unsafe_embed_list,
|
||||||
|
name="embed-list",
|
||||||
|
),
|
||||||
# User books
|
# User books
|
||||||
re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"),
|
re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"),
|
||||||
re_path(
|
re_path(
|
||||||
|
|
|
@ -84,7 +84,7 @@ from .inbox import Inbox
|
||||||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||||
from .isbn import Isbn
|
from .isbn import Isbn
|
||||||
from .list import Lists, SavedLists, List, Curate, UserLists
|
from .list import Lists, SavedLists, List, Curate, UserLists
|
||||||
from .list import save_list, unsave_list, delete_list
|
from .list import save_list, unsave_list, delete_list, unsafe_embed_list
|
||||||
from .notifications import Notifications
|
from .notifications import Notifications
|
||||||
from .outbox import Outbox
|
from .outbox import Outbox
|
||||||
from .reading import create_readthrough, delete_readthrough, delete_progressupdate
|
from .reading import create_readthrough, delete_readthrough, delete_progressupdate
|
||||||
|
|
|
@ -7,13 +7,14 @@ from django.core.paginator import Paginator
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
from django.db.models import Avg, Count, DecimalField, Q, Max
|
from django.db.models import Avg, Count, DecimalField, Q, Max
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import HttpResponseBadRequest, HttpResponse
|
from django.http import HttpResponseBadRequest, HttpResponse, Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
|
||||||
from bookwyrm import book_search, forms, models
|
from bookwyrm import book_search, forms, models
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
|
@ -167,6 +168,14 @@ class List(View):
|
||||||
][: 5 - len(suggestions)]
|
][: 5 - len(suggestions)]
|
||||||
|
|
||||||
page = paginated.get_page(request.GET.get("page"))
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
|
|
||||||
|
embed_key = str(book_list.embed_key.hex)
|
||||||
|
embed_url = reverse("embed-list", args=[book_list.id, embed_key])
|
||||||
|
embed_url = request.build_absolute_uri(embed_url)
|
||||||
|
|
||||||
|
if request.GET:
|
||||||
|
embed_url = f"{embed_url}?{request.GET.urlencode()}"
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"list": book_list,
|
"list": book_list,
|
||||||
"items": page,
|
"items": page,
|
||||||
|
@ -180,6 +189,7 @@ class List(View):
|
||||||
"sort_form": forms.SortListForm(
|
"sort_form": forms.SortListForm(
|
||||||
{"direction": direction, "sort_by": sort_by}
|
{"direction": direction, "sort_by": sort_by}
|
||||||
),
|
),
|
||||||
|
"embed_url": embed_url,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "lists/list.html", data)
|
return TemplateResponse(request, "lists/list.html", data)
|
||||||
|
|
||||||
|
@ -200,6 +210,60 @@ class List(View):
|
||||||
return redirect(book_list.local_path)
|
return redirect(book_list.local_path)
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedList(View):
|
||||||
|
"""embeded book list page"""
|
||||||
|
|
||||||
|
def get(self, request, list_id, list_key):
|
||||||
|
"""display a book list"""
|
||||||
|
book_list = get_object_or_404(models.List, id=list_id)
|
||||||
|
|
||||||
|
embed_key = str(book_list.embed_key.hex)
|
||||||
|
|
||||||
|
if list_key != embed_key:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
# sort_by shall be "order" unless a valid alternative is given
|
||||||
|
sort_by = request.GET.get("sort_by", "order")
|
||||||
|
if sort_by not in ("order", "title", "rating"):
|
||||||
|
sort_by = "order"
|
||||||
|
|
||||||
|
# direction shall be "ascending" unless a valid alternative is given
|
||||||
|
direction = request.GET.get("direction", "ascending")
|
||||||
|
if direction not in ("ascending", "descending"):
|
||||||
|
direction = "ascending"
|
||||||
|
|
||||||
|
directional_sort_by = {
|
||||||
|
"order": "order",
|
||||||
|
"title": "book__title",
|
||||||
|
"rating": "average_rating",
|
||||||
|
}[sort_by]
|
||||||
|
if direction == "descending":
|
||||||
|
directional_sort_by = "-" + directional_sort_by
|
||||||
|
|
||||||
|
items = book_list.listitem_set.prefetch_related("user", "book", "book__authors")
|
||||||
|
if sort_by == "rating":
|
||||||
|
items = items.annotate(
|
||||||
|
average_rating=Avg(
|
||||||
|
Coalesce("book__review__rating", 0.0),
|
||||||
|
output_field=DecimalField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
items = items.filter(approved=True).order_by(directional_sort_by)
|
||||||
|
|
||||||
|
paginated = Paginator(items, PAGE_LENGTH)
|
||||||
|
|
||||||
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"list": book_list,
|
||||||
|
"items": page,
|
||||||
|
"page_range": paginated.get_elided_page_range(
|
||||||
|
page.number, on_each_side=2, on_ends=1
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "lists/embed-list.html", data)
|
||||||
|
|
||||||
|
|
||||||
class Curate(View):
|
class Curate(View):
|
||||||
"""approve or discard list suggestsions"""
|
"""approve or discard list suggestsions"""
|
||||||
|
|
||||||
|
@ -447,3 +511,11 @@ def normalize_book_list_ordering(book_list_id, start=0, add_offset=0):
|
||||||
if item.order != effective_order:
|
if item.order != effective_order:
|
||||||
item.order = effective_order
|
item.order = effective_order
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
@xframe_options_exempt
|
||||||
|
def unsafe_embed_list(request, *args, **kwargs):
|
||||||
|
"""allows the EmbedList view to be loaded through unsafe iframe origins"""
|
||||||
|
|
||||||
|
embed_list_view = EmbedList.as_view()
|
||||||
|
return embed_list_view(request, *args, **kwargs)
|
||||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: 0.0.1\n"
|
"Project-Id-Version: 0.0.1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-12-07 22:16+0000\n"
|
"POT-Creation-Date: 2021-12-08 15:40+0000\n"
|
||||||
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
|
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
|
||||||
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
|
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
|
||||||
"Language-Team: English <LL@li.org>\n"
|
"Language-Team: English <LL@li.org>\n"
|
||||||
|
@ -1129,6 +1129,25 @@ msgstr ""
|
||||||
msgid "Reset your %(site_name)s password"
|
msgid "Reset your %(site_name)s password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/embed-layout.html:21 bookwyrm/templates/layout.html:37
|
||||||
|
#, python-format
|
||||||
|
msgid "%(site_name)s home page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/embed-layout.html:34
|
||||||
|
#: bookwyrm/templates/landing/about.html:7 bookwyrm/templates/layout.html:230
|
||||||
|
#, python-format
|
||||||
|
msgid "About %(site_name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/embed-layout.html:40 bookwyrm/templates/layout.html:234
|
||||||
|
msgid "Contact site admin"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/embed-layout.html:46
|
||||||
|
msgid "Join Bookwyrm"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/feed/direct_messages.html:8
|
#: bookwyrm/templates/feed/direct_messages.html:8
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Direct Messages with <a href=\"%(path)s\">%(username)s</a>"
|
msgid "Direct Messages with <a href=\"%(path)s\">%(username)s</a>"
|
||||||
|
@ -1684,11 +1703,6 @@ msgstr ""
|
||||||
msgid "Contact your admin or <a href='https://github.com/bookwyrm-social/bookwyrm/issues'>open an issue</a> if you are seeing unexpected failed items."
|
msgid "Contact your admin or <a href='https://github.com/bookwyrm-social/bookwyrm/issues'>open an issue</a> if you are seeing unexpected failed items."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/landing/about.html:7 bookwyrm/templates/layout.html:230
|
|
||||||
#, python-format
|
|
||||||
msgid "About %(site_name)s"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: bookwyrm/templates/landing/about.html:10
|
#: bookwyrm/templates/landing/about.html:10
|
||||||
#: bookwyrm/templates/landing/about.html:20
|
#: bookwyrm/templates/landing/about.html:20
|
||||||
msgid "Code of Conduct"
|
msgid "Code of Conduct"
|
||||||
|
@ -1860,10 +1874,6 @@ msgstr ""
|
||||||
msgid "Error posting status"
|
msgid "Error posting status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/layout.html:234
|
|
||||||
msgid "Contact site admin"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: bookwyrm/templates/layout.html:238
|
#: bookwyrm/templates/layout.html:238
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1930,6 +1940,21 @@ msgstr ""
|
||||||
msgid "Edit List"
|
msgid "Edit List"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/embed-list.html:7
|
||||||
|
#, python-format
|
||||||
|
msgid "%(list_name)s, a list by %(owner)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/embed-list.html:17
|
||||||
|
#, python-format
|
||||||
|
msgid "on <a href=\"/\">%(site_name)s</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/embed-list.html:26
|
||||||
|
#: bookwyrm/templates/lists/list.html:29
|
||||||
|
msgid "This list is currently empty"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/lists/form.html:19
|
#: bookwyrm/templates/lists/form.html:19
|
||||||
msgid "List curation:"
|
msgid "List curation:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1995,10 +2020,6 @@ msgstr ""
|
||||||
msgid "You successfully added a book to this list!"
|
msgid "You successfully added a book to this list!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/lists/list.html:29
|
|
||||||
msgid "This list is currently empty"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: bookwyrm/templates/lists/list.html:67
|
#: bookwyrm/templates/lists/list.html:67
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Added by <a href=\"%(user_path)s\">%(username)s</a>"
|
msgid "Added by <a href=\"%(user_path)s\">%(username)s</a>"
|
||||||
|
@ -2051,6 +2072,23 @@ msgstr ""
|
||||||
msgid "Suggest"
|
msgid "Suggest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/list.html:191
|
||||||
|
msgid "Embed this list on a website"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/list.html:193
|
||||||
|
msgid "Copy embed code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/list.html:193
|
||||||
|
msgid "Copied!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bookwyrm/templates/lists/list.html:193
|
||||||
|
#, python-format
|
||||||
|
msgid "%(list_name)s, a list by %(owner)s on %(site_name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: bookwyrm/templates/lists/list_items.html:15
|
#: bookwyrm/templates/lists/list_items.html:15
|
||||||
msgid "Saved"
|
msgid "Saved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
Loading…
Reference in a new issue