Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-03-31 11:28:52 -07:00
commit 877c90b087
22 changed files with 2416 additions and 1546 deletions

View file

@ -37,9 +37,13 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
""" set the identifier """ """ set the identifier """
super().save(*args, **kwargs) super().save(*args, **kwargs)
if not self.identifier: if not self.identifier:
slug = re.sub(r"[^\w]", "", self.name).lower() self.identifier = self.get_identifier()
self.identifier = "%s-%d" % (slug, self.id) super().save(*args, **kwargs, broadcast=False)
super().save(*args, **kwargs)
def get_identifier(self):
""" custom-shelf-123 for the url """
slug = re.sub(r"[^\w]", "", self.name).lower()
return "{:s}-{:d}".format(slug, self.id)
@property @property
def collection_queryset(self): def collection_queryset(self):
@ -49,7 +53,8 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
def get_remote_id(self): def get_remote_id(self):
""" shelf identifier instead of id """ """ shelf identifier instead of id """
base_path = self.user.remote_id base_path = self.user.remote_id
return "%s/shelf/%s" % (base_path, self.identifier) identifier = self.identifier or self.get_identifier()
return "%s/books/%s" % (base_path, identifier)
class Meta: class Meta:
""" user/shelf unqiueness """ """ user/shelf unqiueness """

View file

@ -165,32 +165,11 @@
{% include 'snippets/readthrough.html' with readthrough=readthrough %} {% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% endfor %} {% endfor %}
</section> </section>
{% endif %}
{% if request.user.is_authenticated %}
<section class="box"> <section class="box">
{% include 'snippets/create_status.html' with book=book hide_cover=True %} {% include 'snippets/create_status.html' with book=book hide_cover=True %}
</section> </section>
<section class="block">
<form name="tag" action="/tag/" method="post">
<label for="tags" class="is-3">{% trans "Tags" %}</label>
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input id="tags" class="input" type="text" name="name">
<button class="button" type="submit">{% trans "Add tag" %}</button>
</form>
</section>
{% endif %} {% endif %}
<div class="block">
<div class="field is-grouped is-grouped-multiline">
{% for tag in tags %}
{% include 'snippets/tag.html' with book=book tag=tag user_tags=user_tags %}
{% endfor %}
</div>
</div>
</div> </div>
<div class="column is-one-fifth"> <div class="column is-one-fifth">
{% if book.subjects %} {% if book.subjects %}

View file

@ -55,7 +55,7 @@
<div class="navbar-start"> <div class="navbar-start">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item"> <a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
{% trans "Your shelves" %} {% trans "Your books" %}
</a> </a>
<a href="/#feed" class="navbar-item"> <a href="/#feed" class="navbar-item">
{% trans "Feed" %} {% trans "Feed" %}

View file

@ -1,7 +1,7 @@
{% extends 'components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% load i18n %} {% load i18n %}
{% block dropdown-trigger %} {% block dropdown-trigger %}
<span>{% trans "Change shelf" %}</span> <span>{% trans "Move book" %}</span>
<span class="icon icon-arrow-down" aria-hidden="true"></span> <span class="icon icon-arrow-down" aria-hidden="true"></span>
{% endblock %} {% endblock %}
@ -25,7 +25,7 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ current.id }}"> <input type="hidden" name="shelf" value="{{ current.id }}">
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% trans "Unshelve" %}</button> <button class="button is-fullwidth is-small is-danger is-light" type="submit">{% trans "Remove" %}</button>
</form> </form>
</li> </li>
{% endblock %} {% endblock %}

View file

@ -45,7 +45,7 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}"> <input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}"> <input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% trans "Unshelve" %}</button> <button class="button is-fullwidth is-small is-danger is-light" type="submit">{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}</button>
</form> </form>
</div> </div>
</li> </li>

View file

@ -0,0 +1,6 @@
{% load i18n %}
{% if is_self %}
{% trans "Your books" %}
{% else %}
{% blocktrans with username=user.display_name %}{{ username }}'s books{% endblocktrans %}
{% endif %}

View file

@ -3,14 +3,14 @@
{% load humanize %} {% load humanize %}
{% load i18n %} {% load i18n %}
{% block title %}
{% include 'user/books_header.html' %}
{% endblock %}
{% block header %} {% block header %}
<header class="columns"> <header class="columns">
<h1 class="title"> <h1 class="title">
{% if is_self %} {% include 'user/books_header.html' %}
{% trans "Your Shelves" %}
{% else %}
{% blocktrans with username=user.display_name %}{{ username }}: Shelves{% endblocktrans %}
{% endif %}
</h1> </h1>
</header> </header>
{% endblock %} {% endblock %}
@ -20,10 +20,13 @@
<div class="column"> <div class="column">
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li class="{% if shelf.identifier == 'all' %}is-active{% endif %}">
<a href="{% url 'user-shelves' user|username %}"{% if shelf.identifier == 'all' %} aria-current="page"{% endif %}>{% trans "All books" %}</a>
</li>
{% for shelf_tab in shelves %} {% for shelf_tab in shelves %}
<li class="{% if shelf_tab.identifier == shelf.identifier %}is-active{% endif %}"> <li class="{% if shelf_tab.identifier == shelf.identifier %}is-active{% endif %}">
<a href="/user/{{ user | username }}/shelf/{{ shelf_tab.identifier }}"{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}>{% if shelf_tab.identifier == 'to-read' %}{% trans "To Read" %}{% elif shelf_tab.identifier == 'reading' %}{% trans "Currently Reading" %}{% elif shelf_tab.identifier == 'read' %}{% trans "Read" %}{% else %}{{ shelf_tab.name }}{% endif %}</a> <a href="{{ shelf_tab.local_path }}"{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}>{% if shelf_tab.identifier == 'to-read' %}{% trans "To Read" %}{% elif shelf_tab.identifier == 'reading' %}{% trans "Currently Reading" %}{% elif shelf_tab.identifier == 'read' %}{% trans "Read" %}{% else %}{{ shelf_tab.name }}{% endif %}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -50,7 +53,7 @@
</span> </span>
</h2> </h2>
</div> </div>
{% if is_self %} {% if is_self and shelf.id %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit shelf" as button_text %} {% trans "Edit shelf" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-shelf-form" focus="edit-shelf-form-header" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-shelf-form" focus="edit-shelf-form-header" %}
@ -81,7 +84,6 @@
{% endif %} {% endif %}
</tr> </tr>
{% for book in books %} {% for book in books %}
{% with book=book.book %}
<tr class="book-preview"> <tr class="book-preview">
<td> <td>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a> <a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
@ -113,13 +115,12 @@
</td> </td>
{% endif %} {% endif %}
</tr> </tr>
{% endwith %}
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% else %} {% else %}
<p>{% trans "This shelf is empty." %}</p> <p>{% trans "This shelf is empty." %}</p>
{% if shelf.editable %} {% if shelf.id and shelf.editable %}
<form name="delete-shelf" action="/delete-shelf/{{ shelf.id }}" method="post"> <form name="delete-shelf" action="/delete-shelf/{{ shelf.id }}" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}"> <input type="hidden" name="user" value="{{ request.user.id }}">

View file

@ -1,5 +1,6 @@
{% extends 'user/user_layout.html' %} {% extends 'user/user_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{{ user.display_name }}{% endblock %} {% block title %}{{ user.display_name }}{% endblock %}
@ -23,12 +24,14 @@
{% block panel %} {% block panel %}
{% if user.bookwyrm_user %} {% if user.bookwyrm_user %}
<div class="block"> <div class="block">
<h2 class="title">{% trans "Shelves" %}</h2> <h2 class="title">
{% include 'user/books_header.html' %}
</h2>
<div class="columns"> <div class="columns">
{% for shelf in shelves %} {% for shelf in shelves %}
<div class="column is-narrow"> <div class="column is-narrow">
<h3>{{ shelf.name }} <h3>{{ shelf.name }}
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}See all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}</h3> {% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}</h3>
<div class="is-mobile field is-grouped"> <div class="is-mobile field is-grouped">
{% for book in shelf.books %} {% for book in shelf.books %}
<div class="control"> <div class="control">
@ -41,7 +44,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<small><a href="{{ user.local_path }}/shelves">{% blocktrans %}See all {{ shelf_count }} shelves{% endblocktrans %}</a></small> <small><a href="{% url 'user-shelves' user|username %}">{% trans "View all books" %}</a></small>
</div> </div>
{% endif %} {% endif %}

View file

@ -42,7 +42,7 @@
{% endif %} {% endif %}
</div> </div>
{% with user|username as username %} {% with user|username as username %}
{% if 'user/'|add:username|add:'/shelf' not in request.path and 'user/'|add:username|add:'/shelves' not in request.path %} {% if 'user/'|add:username|add:'/books' not in request.path and 'user/'|add:username|add:'/shelf' not in request.path %}
<nav class="tabs"> <nav class="tabs">
<ul> <ul>
{% url 'user-feed' user|username as url %} {% url 'user-feed' user|username as url %}
@ -65,7 +65,7 @@
{% if user.shelf_set.exists %} {% if user.shelf_set.exists %}
{% url 'user-shelves' user|username as url %} {% url 'user-shelves' user|username as url %}
<li{% if url in request.path %} class="is-active"{% endif %}> <li{% if url in request.path %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Shelves" %}</a> <a href="{{ url }}">{% trans "Books" %}</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View file

@ -27,7 +27,7 @@ class Shelf(TestCase):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
expected_id = "https://%s/user/mouse/shelf/test-shelf" % settings.DOMAIN expected_id = "https://%s/user/mouse/books/test-shelf" % settings.DOMAIN
self.assertEqual(shelf.get_remote_id(), expected_id) self.assertEqual(shelf.get_remote_id(), expected_id)
models.Shelf.broadcast = real_broadcast models.Shelf.broadcast = real_broadcast

View file

@ -138,16 +138,8 @@ urlpatterns = [
views.Following.as_view(), views.Following.as_view(),
name="user-following", name="user-following",
), ),
re_path(r"%s/shelves/?$" % user_path, views.user_shelves_page, name="user-shelves"),
re_path(r"%s/lists/?$" % user_path, views.UserLists.as_view(), name="user-lists"),
# goals
re_path(
r"%s/goal/(?P<year>\d{4})/?$" % user_path,
views.Goal.as_view(),
name="user-goal",
),
re_path(r"^hide-goal/?$", views.hide_goal, name="hide-goal"),
# lists # lists
re_path(r"%s/lists/?$" % user_path, views.UserLists.as_view(), name="user-lists"),
re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"), re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"), re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"),
@ -159,6 +151,29 @@ urlpatterns = [
re_path( re_path(
r"^list/(?P<list_id>\d+)/curate/?$", views.Curate.as_view(), name="list-curate" r"^list/(?P<list_id>\d+)/curate/?$", views.Curate.as_view(), name="list-curate"
), ),
# Uyser books
re_path(r"%s/books/?$" % user_path, views.Shelf.as_view(), name="user-shelves"),
re_path(
r"^%s/(helf|books)/(?P<shelf_identifier>[\w-]+)(.json)?/?$" % user_path,
views.Shelf.as_view(),
name="shelf",
),
re_path(
r"^%s/(books|shelf)/(?P<shelf_identifier>[\w-]+)(.json)?/?$" % local_user_path,
views.Shelf.as_view(),
name="shelf",
),
re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"),
re_path(r"^delete-shelf/(?P<shelf_id>\d+)?$", views.delete_shelf),
re_path(r"^shelve/?$", views.shelve),
re_path(r"^unshelve/?$", views.unshelve),
# goals
re_path(
r"%s/goal/(?P<year>\d{4})/?$" % user_path,
views.Goal.as_view(),
name="user-goal",
),
re_path(r"^hide-goal/?$", views.hide_goal, name="hide-goal"),
# preferences # preferences
re_path(r"^preferences/profile/?$", views.EditUser.as_view(), name="prefs-profile"), re_path(r"^preferences/profile/?$", views.EditUser.as_view(), name="prefs-profile"),
re_path(r"^preferences/password/?$", views.ChangePassword.as_view()), re_path(r"^preferences/password/?$", views.ChangePassword.as_view()),
@ -199,20 +214,6 @@ urlpatterns = [
re_path(r"^tag/(?P<tag_id>.+)/?$", views.Tag.as_view()), re_path(r"^tag/(?P<tag_id>.+)/?$", views.Tag.as_view()),
re_path(r"^tag/?$", views.AddTag.as_view()), re_path(r"^tag/?$", views.AddTag.as_view()),
re_path(r"^untag/?$", views.RemoveTag.as_view()), re_path(r"^untag/?$", views.RemoveTag.as_view()),
# shelf
re_path(
r"^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$" % user_path,
views.Shelf.as_view(),
name="shelf",
),
re_path(
r"^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$" % local_user_path,
views.Shelf.as_view(),
),
re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"),
re_path(r"^delete-shelf/(?P<shelf_id>\d+)?$", views.delete_shelf),
re_path(r"^shelve/?$", views.shelve),
re_path(r"^unshelve/?$", views.unshelve),
# reading progress # reading progress
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"), re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
re_path(r"^delete-readthrough/?$", views.delete_readthrough), re_path(r"^delete-readthrough/?$", views.delete_readthrough),

View file

@ -27,7 +27,7 @@ from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search from .search import Search
from .shelf import Shelf from .shelf import Shelf
from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import create_shelf, delete_shelf
from .shelf import shelve, unshelve from .shelf import shelve, unshelve
from .site import Site from .site import Site
from .status import CreateStatus, DeleteStatus from .status import CreateStatus, DeleteStatus

View file

@ -1,4 +1,6 @@
""" shelf views""" """ shelf views"""
from collections import namedtuple
from django.db import IntegrityError from django.db import IntegrityError
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
@ -6,6 +8,7 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound
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.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
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
@ -13,14 +16,14 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_edition, get_user_from_username from .helpers import is_api_request, get_edition, get_user_from_username
from .helpers import handle_reading_status from .helpers import handle_reading_status, privacy_filter, object_visible_to_user
# pylint: disable= no-self-use # pylint: disable= no-self-use
class Shelf(View): class Shelf(View):
""" shelf page """ """ shelf page """
def get(self, request, username, shelf_identifier): def get(self, request, username, shelf_identifier=None):
""" display a shelf """ """ display a shelf """
try: try:
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
@ -32,35 +35,30 @@ class Shelf(View):
except ValueError: except ValueError:
page = 1 page = 1
shelves = privacy_filter(request.user, user.shelf_set)
# get the shelf and make sure the logged in user should be able to see it
if shelf_identifier: if shelf_identifier:
shelf = user.shelf_set.get(identifier=shelf_identifier) shelf = user.shelf_set.get(identifier=shelf_identifier)
if not object_visible_to_user(request.user, shelf):
return HttpResponseNotFound()
# this is a constructed "all books" view, with a fake "shelf" obj
else: else:
shelf = user.shelf_set.first() FakeShelf = namedtuple(
"Shelf", ("identifier", "name", "user", "books", "privacy")
)
books = models.Edition.objects.filter(
shelfbook__shelf__in=shelves.all()
).distinct()
shelf = FakeShelf("all", _("All books"), user, books, "public")
is_self = request.user == user is_self = request.user == user
shelves = user.shelf_set
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
# make sure the user has permission to view the shelf
if shelf.privacy == "direct" or (
shelf.privacy == "followers" and not follower
):
return HttpResponseNotFound()
# only show other shelves that should be visible
if follower:
shelves = shelves.filter(privacy__in=["public", "followers"])
else:
shelves = shelves.filter(privacy="public")
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(shelf.to_activity(**request.GET)) return ActivitypubResponse(shelf.to_activity(**request.GET))
paginated = Paginator( paginated = Paginator(
models.ShelfBook.objects.filter(user=user, shelf=shelf) shelf.books.order_by("-updated_date").all(),
.order_by("-updated_date")
.all(),
PAGE_LENGTH, PAGE_LENGTH,
) )
@ -95,11 +93,6 @@ class Shelf(View):
return redirect(shelf.local_path) return redirect(shelf.local_path)
def user_shelves_page(request, username):
""" default shelf """
return Shelf.as_view()(request, username, None)
@login_required @login_required
@require_POST @require_POST
def create_shelf(request): def create_shelf(request):
@ -176,7 +169,8 @@ def shelve(request):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, user=request.user book=book, shelf=desired_shelf, user=request.user
) )
# The book is already on this shelf. Might be good to alert, or reject the action? # The book is already on this shelf.
# Might be good to alert, or reject the action?
except IntegrityError: except IntegrityError:
pass pass
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff