mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-27 03:51:08 +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!! """
|
||||
import uuid
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
@ -43,6 +45,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
|||
through="ListItem",
|
||||
through_fields=("book_list", "book"),
|
||||
)
|
||||
embed_key = models.UUIDField(unique=True, null=True, editable=False)
|
||||
activity_serializer = activitypub.BookList
|
||||
|
||||
def get_remote_id(self):
|
||||
|
@ -105,6 +108,12 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
|||
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):
|
||||
"""ok"""
|
||||
|
|
|
@ -20,6 +20,10 @@ body {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.card.has-border {
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.scroll-x {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
|
|
@ -66,6 +66,9 @@ let BookWyrm = new class {
|
|||
document.querySelectorAll('input[type="file"]').forEach(
|
||||
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(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="navbar-brand">
|
||||
<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>
|
||||
<form class="navbar-item column" action="{% url 'search' %}">
|
||||
<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 %}
|
||||
{% 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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" testing models """
|
||||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
from uuid import UUID
|
||||
|
||||
from bookwyrm import models, settings
|
||||
|
||||
|
@ -80,3 +81,12 @@ class List(TestCase):
|
|||
self.assertEqual(item.book_list.privacy, "public")
|
||||
self.assertEqual(item.privacy, "direct")
|
||||
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 django.contrib.auth.models import AnonymousUser
|
||||
from django.http.response import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
@ -385,3 +386,46 @@ class ListViews(TestCase):
|
|||
|
||||
result = view(request, self.local_user.username)
|
||||
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"^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
|
||||
re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"),
|
||||
re_path(
|
||||
|
|
|
@ -84,7 +84,7 @@ from .inbox import Inbox
|
|||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||
from .isbn import Isbn
|
||||
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 .outbox import Outbox
|
||||
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.models import Avg, Count, DecimalField, Q, Max
|
||||
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.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
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.activitypub import ActivitypubResponse
|
||||
|
@ -167,6 +168,14 @@ class List(View):
|
|||
][: 5 - len(suggestions)]
|
||||
|
||||
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 = {
|
||||
"list": book_list,
|
||||
"items": page,
|
||||
|
@ -180,6 +189,7 @@ class List(View):
|
|||
"sort_form": forms.SortListForm(
|
||||
{"direction": direction, "sort_by": sort_by}
|
||||
),
|
||||
"embed_url": embed_url,
|
||||
}
|
||||
return TemplateResponse(request, "lists/list.html", data)
|
||||
|
||||
|
@ -200,6 +210,60 @@ class List(View):
|
|||
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):
|
||||
"""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:
|
||||
item.order = effective_order
|
||||
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 ""
|
||||
"Project-Id-Version: 0.0.1\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"
|
||||
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
|
||||
"Language-Team: English <LL@li.org>\n"
|
||||
|
@ -1129,6 +1129,25 @@ msgstr ""
|
|||
msgid "Reset your %(site_name)s password"
|
||||
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
|
||||
#, python-format
|
||||
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."
|
||||
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:20
|
||||
msgid "Code of Conduct"
|
||||
|
@ -1860,10 +1874,6 @@ msgstr ""
|
|||
msgid "Error posting status"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/layout.html:234
|
||||
msgid "Contact site admin"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/layout.html:238
|
||||
msgid "Documentation"
|
||||
msgstr ""
|
||||
|
@ -1930,6 +1940,21 @@ msgstr ""
|
|||
msgid "Edit List"
|
||||
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
|
||||
msgid "List curation:"
|
||||
msgstr ""
|
||||
|
@ -1995,10 +2020,6 @@ msgstr ""
|
|||
msgid "You successfully added a book to this list!"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/lists/list.html:29
|
||||
msgid "This list is currently empty"
|
||||
msgstr ""
|
||||
|
||||
#: bookwyrm/templates/lists/list.html:67
|
||||
#, python-format
|
||||
msgid "Added by <a href=\"%(user_path)s\">%(username)s</a>"
|
||||
|
@ -2051,6 +2072,23 @@ msgstr ""
|
|||
msgid "Suggest"
|
||||
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
|
||||
msgid "Saved"
|
||||
msgstr ""
|
||||
|
|
Loading…
Reference in a new issue