Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-04-25 11:33:04 -07:00
commit b30fab0597
38 changed files with 1923 additions and 2716 deletions

View file

@ -156,14 +156,6 @@ class UserGroupForm(CustomForm):
fields = ["groups"]
class TagForm(CustomForm):
class Meta:
model = models.Tag
fields = ["name"]
help_texts = {f: None for f in fields}
labels = {"name": "Add a tag"}
class CoverForm(CustomForm):
class Meta:
model = models.Book

View file

@ -0,0 +1,35 @@
# Generated by Django 3.1.8 on 2021-04-23 01:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0069_auto_20210422_1604"),
]
operations = [
migrations.AlterUniqueTogether(
name="usertag",
unique_together=None,
),
migrations.RemoveField(
model_name="usertag",
name="book",
),
migrations.RemoveField(
model_name="usertag",
name="tag",
),
migrations.RemoveField(
model_name="usertag",
name="user",
),
migrations.DeleteModel(
name="Tag",
),
migrations.DeleteModel(
name="UserTag",
),
]

View file

@ -17,8 +17,6 @@ from .favorite import Favorite
from .notification import Notification
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
from .tag import Tag, UserTag
from .user import User, KeyPair, AnnualGoal
from .relationship import UserFollows, UserFollowRequest, UserBlocks
from .report import Report, ReportComment

View file

@ -204,7 +204,9 @@ class ObjectMixin(ActivitypubMixin):
created = created or not bool(self.id)
# first off, we want to save normally no matter what
super().save(*args, **kwargs)
if not broadcast:
if not broadcast or (
hasattr(self, "status_type") and self.status_type == "Announce"
):
return
# this will work for objects owned by a user (lists, shelves)

View file

@ -351,6 +351,16 @@ class Boost(ActivityMixin, Status):
def save(self, *args, **kwargs):
""" save and notify """
# This constraint can't work as it would cross tables.
# class Meta:
# unique_together = ('user', 'boosted_status')
if (
Boost.objects.filter(boosted_status=self.boosted_status, user=self.user)
.exclude(id=self.id)
.exists()
):
return
super().save(*args, **kwargs)
if not self.boosted_status.user.local or self.boosted_status.user == self.user:
return

View file

@ -1,63 +0,0 @@
""" models for storing different kinds of Activities """
import urllib.parse
from django.apps import apps
from django.db import models
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
from . import fields
class Tag(OrderedCollectionMixin, BookWyrmModel):
""" freeform tags for books """
name = fields.CharField(max_length=100, unique=True)
identifier = models.CharField(max_length=100)
@property
def books(self):
""" count of books associated with this tag """
edition_model = apps.get_model("bookwyrm.Edition", require_ready=True)
return (
edition_model.objects.filter(usertag__tag__identifier=self.identifier)
.order_by("-created_date")
.distinct()
)
collection_queryset = books
def get_remote_id(self):
""" tag should use identifier not id in remote_id """
base_path = "https://%s" % DOMAIN
return "%s/tag/%s" % (base_path, self.identifier)
def save(self, *args, **kwargs):
""" create a url-safe lookup key for the tag """
if not self.id:
# add identifiers to new tags
self.identifier = urllib.parse.quote_plus(self.name)
super().save(*args, **kwargs)
class UserTag(CollectionItemMixin, BookWyrmModel):
""" an instance of a tag on a book by a user """
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
)
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="object"
)
tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target")
activity_serializer = activitypub.Add
object_field = "book"
collection_field = "tag"
class Meta:
""" unqiueness constraint """
unique_together = ("user", "book", "tag")

View file

@ -153,7 +153,7 @@ LANGUAGES = [
("de-de", _("German")),
("es", _("Spanish")),
("fr-fr", _("French")),
("zh-cn", _("Simplified Chinese")),
("zh-hans", _("Simplified Chinese")),
]

View file

@ -1,6 +1,5 @@
html {
scroll-behavior: smooth;
scroll-padding-top: 20%;
}
body {
@ -30,6 +29,40 @@ body {
min-width: 75% !important;
}
/** Utilities not covered by Bulma
******************************************************************************/
@media only screen and (max-width: 768px) {
.is-sr-only-mobile {
border: none !important;
clip: rect(0, 0, 0, 0) !important;
height: 0.01em !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
white-space: nowrap !important;
width: 0.01em !important;
}
.m-0-mobile {
margin: 0 !important;
}
}
.button.is-transparent {
background-color: transparent;
}
.card.is-stretchable {
display: flex;
flex-direction: column;
height: 100%;
}
.card.is-stretchable .card-content {
flex-grow: 1;
}
/** Shelving
******************************************************************************/
@ -86,6 +119,13 @@ body {
}
}
/** Stars
******************************************************************************/
.stars {
white-space: nowrap;
}
/** Stars in a review form
*
* Specificity makes hovering taking over checked inputs.
@ -256,3 +296,53 @@ body {
opacity: 0.5;
cursor: not-allowed;
}
/* Book preview table
******************************************************************************/
.book-preview td {
vertical-align: middle;
}
@media only screen and (max-width: 768px) {
table.is-mobile,
table.is-mobile tbody {
display: block;
}
table.is-mobile tr {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
border-top: 1px solid #dbdbdb;
}
table.is-mobile td {
display: block;
box-sizing: border-box;
flex: 1 0 100%;
order: 2;
border-bottom: 0;
}
table.is-mobile td.book-preview-top-row {
order: 1;
flex-basis: auto;
}
table.is-mobile td[data-title]:not(:empty)::before {
content: attr(data-title);
display: block;
font-size: 0.75em;
font-weight: bold;
}
table.is-mobile td:empty {
padding: 0;
}
table.is-mobile th,
table.is-mobile thead {
display: none;
}
}

View file

@ -41,7 +41,7 @@
<div class="columns is-multiline">
{% for user in users %}
<div class="column is-one-third">
<div class="card block">
<div class="card is-stretchable">
<div class="card-content">
<div class="media">
<a href="{{ user.local_path }}" class="media-left">
@ -56,13 +56,13 @@
</div>
</div>
<div class="content">
<div>
{% if user.summary %}
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
{% else %}&nbsp;{% endif %}
</div>
</div>
<footer class="card-footer content">
<footer class="card-footer">
{% if user != request.user %}
{% if user.mutuals %}
<div class="card-footer-item">

View file

@ -1,7 +1,7 @@
{% load bookwyrm_tags %}
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<html lang="{% get_lang %}">
<head>
<title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">

View file

@ -51,7 +51,7 @@
{% for status in report.statuses.select_subclasses.all %}
<li>
{% if status.deleted %}
<em>{% trans "Statuses has been deleted" %}</em>
<em>{% trans "Status has been deleted" %}</em>
{% else %}
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
{% endif %}

View file

@ -1,7 +1,8 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% if book.authors %}
{% blocktrans with path=book.local_path title=book.title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
{% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
{% else %}
<a href="{{ book.local_path }}">{{ book.title }}</a>
<a href="{{ book.local_path }}">{{ book|title }}</a>
{% endif %}

View file

@ -4,18 +4,16 @@
{% with status.id|uuid as uuid %}
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small" type="submit" {% if not status.boostable %}disabled{% endif %}>
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
<button class="button is-small is-light is-transparent" type="submit" {% if not status.boostable %}disabled{% endif %}>
<span class="icon icon-boost m-0-mobile" title="{% trans 'Boost' %}"></span>
<span class="is-sr-only-mobile">{% trans "Boost" %}</span>
</button>
</form>
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}is-hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small is-primary" type="submit">
<span class="icon icon-boost" title="{% trans 'Un-boost status' %}">
<span class="is-sr-only">{% trans "Un-boost status" %}</span>
</span>
<button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-boost has-text-primary m-0-mobile" title="{% trans 'Un-boost' %}"></span>
<span class="is-sr-only-mobile">{% trans "Un-boost" %}</span>
</button>
</form>
{% endwith %}

View file

@ -3,18 +3,17 @@
{% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small" type="submit">
<span class="icon icon-heart" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
<button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}">
</span>
<span class="is-sr-only-mobile">{% trans "Like" %}</span>
</button>
</form>
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}is-hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-primary is-small" type="submit">
<span class="icon icon-heart" title="{% trans 'Un-like status' %}">
<span class="is-sr-only">{% trans "Un-like status" %}</span>
</span>
<button class="button is-light is-transparent is-small" type="submit">
<span class="icon icon-heart has-text-primary m-0-mobile" title="{% trans 'Un-like' %}"></span>
<span class="is-sr-only-mobile">{% trans "Un-like" %}</span>
</button>
</form>
{% endwith %}

View file

@ -1,10 +1,10 @@
{% load i18n %}
{% if rating %}
{% blocktrans with book_title=book.title display_rating=rating|floatformat:"0" review_title=name count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %}
{% blocktrans with book_title=book.title|safe display_rating=rating|floatformat:"0" review_title=name|safe count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %}
{% else %}
{% blocktrans with book_title=book.title review_title=name %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %}
{% blocktrans with book_title=book.title|safe review_title=name|safe %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %}
{% endif %}

View file

@ -14,10 +14,17 @@
{% if not hide_book %}
{% with book=status.book|default:status.mention_books.first %}
{% if book %}
<div class="column is-narrow is-hidden-mobile">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
<div class="column is-narrow">
<div class="columns is-mobile">
<div class="column is-narrow">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
<div class="column is-hidden-tablet">
<p>{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:15 }}</p>
</div>
</div>
</div>
{% endif %}
{% endwith %}

View file

@ -4,16 +4,16 @@
{% load humanize %}
{% block card-header %}
<h3 class="card-header-title has-background-white-ter is-block">
{% include 'snippets/status/status_header.html' with status=status %}
</h3>
<div class="card-header-title has-background-white-ter is-block">
{% include 'snippets/status/status_header.html' with status=status %}
</div>
{% endblock %}
{% block card-content %}{% endblock %}
{% block card-footer %}
{% if moderation_mode and perms.bookwyrm.moderate_post %}
<div class="card-footer-item">
{% if moderation_mode and perms.bookwyrm.moderate_post %}
{# moderation options #}
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
@ -22,54 +22,45 @@
{% trans "Delete status" %}
</button>
</form>
{% elif no_interact %}
{# nothing here #}
{% elif request.user.is_authenticated %}
<div class="field has-addons">
<div class="control">
{% trans "Reply" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text=button_text icon="comment" class="is-small toggle-button" focus="id_content_reply" %}
</div>
<div class="control">
{% include 'snippets/boost_button.html' with status=status %}
</div>
<div class="control">
{% include 'snippets/fav_button.html' with status=status %}
</div>
</div>
{% else %}
<a href="/login">
<span class="icon icon-comment" title="{% trans 'Reply' %}">
<span class="is-sr-only">{% trans "Reply" %}</span>
</span>
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
<span class="icon icon-heart" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span>
</a>
{% endif %}
</div>
{% elif no_interact %}
{# nothing here #}
{% elif request.user.is_authenticated %}
<div class="card-footer-item">
{% include 'snippets/privacy-icons.html' with item=status %}
{% trans "Reply" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date|timesince }}</a>
{% include 'snippets/boost_button.html' with status=status %}
</div>
<div class="card-footer-item">
{% include 'snippets/fav_button.html' with status=status %}
</div>
{% if not moderation_mode %}
<div class="card-footer-item">
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
</div>
{% endif %}
{% else %}
<div class="card-footer-item">
<a href="/login">
<span class="icon icon-comment is-small" title="{% trans 'Reply' %}">
<span class="is-sr-only">{% trans "Reply" %}</span>
</span>
<span class="icon icon-boost is-small ml-4" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
<span class="icon icon-heart is-small ml-4" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span>
</a>
</div>
{% endif %}
{% endblock %}
{% block card-bonus %}
{% if request.user.is_authenticated and not moderation_mode %}
{% with status.id|uuid as uuid %}

View file

@ -1,134 +0,0 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% with status_type=status.status_type %}
<div
class="block"
{% if status_type == 'Review' %}
{% firstof "reviewBody" as body_prop %}
{% firstof 'itemprop="reviewRating" itemscope itemtype="https://schema.org/Rating"' as rating_type %}
{% endif %}
{% if status_type == 'Rating' %}
itemprop="rating"
itemtype="https://schema.org/Rating"
{% endif %}
>
{% if status_type == 'Review' or status_type == 'Rating' %}
<div>
{% if status.name %}
<h3
class="title is-5 has-subtitle"
dir="auto"
itemprop="name"
>
{{ status.name|escape }}
</h3>
{% endif %}
<span
class="is-sr-only"
{{ rating_type }}
>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{% if status_type == 'Rating' %}
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
{% endif %}
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
</div>
{% endif %}
{% if status.content_warning %}
<div>
<p>{{ status.content_warning }}</p>
{% trans "Show more" as button_text %}
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
{% include 'snippets/toggle/open_button.html' %}
{% endwith %}
</div>
{% endif %}
<div
{% if status.content_warning %}
id="show-status-cw-{{ status.id }}"
class="is-hidden"
{% endif %}
>
{% if status.content_warning %}
{% trans "Show less" as button_text %}
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
{% include 'snippets/toggle/close_button.html' %}
{% endwith %}
{% endif %}
{% if status.quote %}
<div class="quote block">
<blockquote dir="auto" class="content mb-2">{{ status.quote | safe }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
</div>
{% endif %}
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
{% with full=status.content|safe no_trim=status.content_warning itemprop=body_prop %}
{% include 'snippets/trimmed_text.html' %}
{% endwith %}
{% endif %}
{% if status.attachments.exists %}
<div class="block">
<div class="columns">
{% for attachment in status.attachments.all %}
<div class="column is-narrow">
<figure class="image is-128x128">
<a
href="/images/{{ attachment.image }}"
target="_blank"
aria-label="{% trans 'Open image in new window' %}"
>
<img
src="/images/{{ attachment.image }}"
{% if attachment.caption %}
alt="{{ attachment.caption }}"
title="{{ attachment.caption }}"
{% endif %}
>
</a>
</figure>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% if not hide_book %}
{% if status.book or status.mention_books.count %}
<div
{% if status_type != 'GeneratedNote' %}
class="box has-background-white-bis"
{% endif %}
>
{% if status.book %}
{% with book=status.book %}
{% include 'snippets/status/book_preview.html' %}
{% endwith %}
{% elif status.mention_books.count %}
{% with book=status.mention_books.first %}
{% include 'snippets/status/book_preview.html' %}
{% endwith %}
{% endif %}
</div>
{% endif %}
{% endif %}
{% endwith %}

View file

@ -1,72 +1,108 @@
{% load bookwyrm_tags %}
{% load i18n %}
<span
itemprop="author"
itemscope
itemtype="https://schema.org/Person"
>
<a
href="{{ status.user.local_path }}"
itemprop="url"
>
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" %}
{% load humanize %}
<span itemprop="name">{{ status.user.display_name }}</span>
</a>
</span>
<div class="media">
<figure class="media-left" aria-hidden="true">
<a class="image is-48x48" href="{{ status.user.local_path }}">
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" medium="true" %}
</a>
</figure>
{% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }}
{% elif status.status_type == 'Rating' %}
{% trans "rated" %}
{% elif status.status_type == 'Review' %}
{% trans "reviewed" %}
{% elif status.status_type == 'Comment' %}
{% trans "commented on" %}
{% elif status.status_type == 'Quotation' %}
{% trans "quoted" %}
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
<div class="media-content">
<h3 class="has-text-weight-bold">
<span
itemprop="author"
itemscope
itemtype="https://schema.org/Person"
>
{% if status.user.avatar %}
<meta itemprop="image" content="/images/{{ status.user.avatar }}">
{% endif %}
{% if parent_status.status_type == 'Review' %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">review</a>{% endblocktrans %}
{% elif parent_status.status_type == 'Comment' %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">comment</a>{% endblocktrans %}
{% elif parent_status.status_type == 'Quotation' %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">quote</a>{% endblocktrans %}
{% else %}
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">status</a>{% endblocktrans %}
{% endif %}
<a
href="{{ status.user.local_path }}"
itemprop="url"
>
<span itemprop="name">{{ status.user.display_name }}</span>
</a>
</span>
{% endwith %}
{% endif %}
{% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }}
{% elif status.status_type == 'Rating' %}
{% trans "rated" %}
{% elif status.status_type == 'Review' %}
{% trans "reviewed" %}
{% elif status.status_type == 'Comment' %}
{% trans "commented on" %}
{% elif status.status_type == 'Quotation' %}
{% trans "quoted" %}
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
{% if status.book %}
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
<a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>{% if status.status_type == 'Rating' %}:
<span
itemprop="reviewRating"
itemscope
itemtype="https://schema.org/Rating"
>
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to <a href="{{ user_path }}">{{ username}}'s</a> <a href="{{ status_path }}">status</a>{% endblocktrans %}
{% endwith %}
{% endif %}
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
</span>
{% if status.book %}
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
<span
itemprop="reviewRating"
itemscope
itemtype="https://schema.org/Rating"
>
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %}
{% else %}
{% include 'snippets/book_titleby.html' with book=status.book %}
{% endif %}
{% elif status.mention_books %}
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first.title }}</a>
{% endif %}
{% if status.book %}
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
<span
itemprop="reviewRating"
itemscope
itemtype="https://schema.org/Rating"
>
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{% if status.progress %}
<p class="help">
({% if status.progress_mode == 'PG' %}{% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}{% else %}{{ status.progress }}%{% endif %})
</p>
{% endif %}
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %}
{% else %}
{% include 'snippets/book_titleby.html' with book=status.book %}
{% endif %}
{% elif status.mention_books %}
<a href="/book/{{ status.mention_books.first.id }}">
{{ status.mention_books.first.title }}
</a>
{% endif %}
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %}
{% else %}
{% include 'snippets/book_titleby.html' with book=status.book %}
{% endif %}
{% elif status.mention_books %}
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first|title }}</a>
{% endif %}
</h3>
<p class="is-size-7 is-flex is-align-items-center">
<a href="{{ status.remote_id }}">{{ status.published_date|timesince }}</a>
{% if status.progress %}
<span class="ml-1">
{% if status.progress_mode == 'PG' %}
({% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %})
{% else %}
({{ status.progress }}%)
{% endif %}
</span>
{% endif %}
{% include 'snippets/privacy-icons.html' with item=status %}
</p>
</div>
</div>

View file

@ -3,9 +3,8 @@
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
<span class="icon icon-dots-three">
<span class="is-sr-only">{% trans "More options" %}</span>
</span>
<span class="icon icon-dots-three m-0-mobile"></span>
<span class="is-sr-only-mobile">{% trans "More options" %}</span>
{% endblock %}
{% block dropdown-list %}

View file

@ -1,23 +0,0 @@
{% load i18n %}
<div class="control">
<form name="tag" action="/{% if tag.tag.identifier in user_tags %}untag{% else %}tag{% endif %}/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.tag.name }}">
<div class="tags has-addons">
<a class="tag" href="{{ tag.tag.local_path }}">
{{ tag.tag.name }}
</a>
{% if tag.tag.identifier in user_tags %}
<button class="tag is-delete" type="submit">
<span class="is-sr-only">{% trans "Remove tag" %}</span>
</button>
{% else %}
<button class="tag" type="submit">+
<span class="is-sr-only">{% trans "Add tag" %}</span>
</button>
{% endif %}
</div>
</form>
</div>

View file

@ -10,9 +10,12 @@
>
{% if icon %}
<span class="icon icon-{{ icon }}" title="{{ text }}">
<span class="icon icon-{{ icon }} m-0-mobile" title="{{ text }}">
<span class="is-sr-only">{{ text }}</span>
</span>
{% elif icon_with_text %}
<span class="icon icon-{{ icon_with_text }} m-0-mobile" title="{{ text }}"></span>
<span class="is-sr-only-mobile">{{ text }}</span>
{% else %}
<span>{{ text }}</span>
{% endif %}

View file

@ -1,14 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{{ tag.name }}{% endblock %}
{% block content %}
<div class="block">
<h1 class="title">{% blocktrans %}Books tagged "{{ tag.name }}"{% endblocktrans %}</h1>
{% include 'snippets/book_tiles.html' with books=books.all %}
</div>
{% endblock %}

View file

@ -68,63 +68,66 @@
<div class="block">
<div>
{% if books|length > 0 %}
<div class="scroll-x">
<table class="table is-striped is-fullwidth">
<tr class="book-preview">
<th>{% trans "Cover" %}</th>
<th>{% trans "Title" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Shelved" %}</th>
<th>{% trans "Started" %}</th>
<th>{% trans "Finished" %}</th>
{% if ratings %}<th>{% trans "Rating" %}</th>{% endif %}
{% if shelf.user == request.user %}
<th aria-hidden="true"></th>
{% endif %}
</tr>
{% for book in books %}
<tr class="book-preview">
<td>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
</td>
<td>
<a href="{{ book.local_path }}">{{ book.title }}</a>
</td>
<td>
{% include 'snippets/authors.html' %}
</td>
<td>
{{ book.created_date | naturalday }}
</td>
{% latest_read_through book user as read_through %}
<td>
{{ read_through.start_date | naturalday |default_if_none:""}}
</td>
<td>
{{ read_through.finish_date | naturalday |default_if_none:""}}
</td>
{% if ratings %}
<td>
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
</td>
{% endif %}
{% if shelf.user == request.user %}
<td>
{% with right=True %}
{% if not shelf.id %}
{% active_shelf book as current %}
{% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %}
{% else %}
{% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %}
{% endif %}
{% endwith %}
</td>
{% endif %}
</tr>
{% endfor %}
<table class="table is-striped is-fullwidth is-mobile">
<thead>
<tr>
<th>{% trans "Cover" %}</th>
<th>{% trans "Title" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Shelved" %}</th>
<th>{% trans "Started" %}</th>
<th>{% trans "Finished" %}</th>
{% if ratings %}<th>{% trans "Rating" %}</th>{% endif %}
{% if shelf.user == request.user %}
<th aria-hidden="true"></th>
{% endif %}
</tr>
</thead>
<tbody>
{% for book in books %}
{% spaceless %}
<tr class="book-preview">
<td class="book-preview-top-row">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
</td>
<td data-title="{% trans "Title" %}">
<a href="{{ book.local_path }}">{{ book.title }}</a>
</td>
<td data-title="{% trans "Author" %}">
{% include 'snippets/authors.html' %}
</td>
<td data-title="{% trans "Shelved" %}">
{{ book.created_date | naturalday }}
</td>
{% latest_read_through book user as read_through %}
<td data-title="{% trans "Started" %}">
{{ read_through.start_date | naturalday |default_if_none:""}}
</td>
<td data-title="{% trans "Finished" %}">
{{ read_through.finish_date | naturalday |default_if_none:""}}
</td>
{% if ratings %}
<td data-title="{% trans "Rating" %}">
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
</td>
{% endif %}
{% if shelf.user == request.user %}
<td class="book-preview-top-row has-text-right">
{% with right=True %}
{% if not shelf.id %}
{% active_shelf book as current %}
{% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %}
{% else %}
{% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %}
{% endif %}
{% endwith %}
</td>
{% endif %}
</tr>
{% endspaceless %}
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p>{% trans "This shelf is empty." %}</p>
{% if shelf.id and shelf.editable %}

View file

@ -1,7 +1,7 @@
""" template filters """
from uuid import uuid4
from django import template
from django import template, utils
from django.db.models import Avg
from bookwyrm import models, views
@ -168,6 +168,17 @@ def get_next_shelf(current_shelf):
return "to-read"
@register.filter(name="title")
def get_title(book):
""" display the subtitle if the title is short """
if not book:
return ""
title = book.title
if len(title) < 6 and book.subtitle:
title = "{:s}: {:s}".format(title, book.subtitle)
return title
@register.simple_tag(takes_context=False)
def related_status(notification):
""" for notifications """
@ -217,3 +228,10 @@ def active_read_through(book, user):
def comparison_bool(str1, str2):
""" idk why I need to write a tag for this, it reutrns a bool """
return str1 == str2
@register.simple_tag(takes_context=False)
def get_lang():
""" get current language, strip to the first two letters """
language = utils.translation.get_language()
return language[0 : language.find("-")]

View file

@ -269,7 +269,7 @@ class Status(TestCase):
def test_review_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Review.objects.create(
name="Review name",
name="Review's name",
content="test content",
rating=3.0,
user=self.local_user,
@ -280,7 +280,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article")
self.assertEqual(
activity["name"],
'Review of "%s" (3 stars): Review name' % self.book.title,
'Review of "%s" (3 stars): Review\'s name' % self.book.title,
)
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document")

View file

@ -51,7 +51,7 @@ class InboxActivities(TestCase):
models.SiteSettings.objects.create()
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_boost(self, _):
def test_boost(self, redis_mock):
""" boost a status """
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
@ -66,16 +66,23 @@ class InboxActivities(TestCase):
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
discarder.return_value = False
views.inbox.activity_task(activity)
# boost added to redis activitystreams
self.assertTrue(redis_mock.called)
# boost created of correct status
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, self.status)
# notification sent to original poster
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_status, self.status)
@responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_boost_remote_status(self, redis_mock):
""" boost a status """
def test_boost_remote_status(self, redis_mock):
""" boost a status from a remote server """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
@ -123,7 +130,7 @@ class InboxActivities(TestCase):
self.assertEqual(boost.boosted_status.comment.book, book)
@responses.activate
def test_handle_discarded_boost(self):
def test_discarded_boost(self):
""" test a boost of a mastodon status that will be discarded """
status = models.Status(
content="hi",
@ -146,7 +153,7 @@ class InboxActivities(TestCase):
views.inbox.activity_task(activity)
self.assertEqual(models.Boost.objects.count(), 0)
def test_handle_unboost(self):
def test_unboost(self):
""" undo a boost """
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create(
@ -175,7 +182,7 @@ class InboxActivities(TestCase):
self.assertTrue(redis_mock.called)
self.assertFalse(models.Boost.objects.exists())
def test_handle_unboost_unknown_boost(self):
def test_unboost_unknown_boost(self):
""" undo a boost """
activity = {
"type": "Undo",

View file

@ -1,4 +1,5 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from django.test import TestCase
from django.test.client import RequestFactory
@ -39,7 +40,7 @@ class InteractionViews(TestCase):
parent_work=work,
)
def test_handle_favorite(self, _):
def test_favorite(self, _):
""" create and broadcast faving a status """
view = views.Favorite.as_view()
request = self.factory.post("")
@ -57,7 +58,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
def test_handle_unfavorite(self, _):
def test_unfavorite(self, _):
""" unfav a status """
view = views.Unfavorite.as_view()
request = self.factory.post("")
@ -74,7 +75,7 @@ class InteractionViews(TestCase):
self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)
def test_handle_boost(self, _):
def test_boost(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
@ -85,6 +86,7 @@ class InteractionViews(TestCase):
view(request, status.id)
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, status)
self.assertEqual(boost.user, self.remote_user)
self.assertEqual(boost.privacy, "public")
@ -95,7 +97,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_status, status)
def test_handle_self_boost(self, _):
def test_self_boost(self, _):
""" boost your own status """
view = views.Boost.as_view()
request = self.factory.post("")
@ -103,7 +105,14 @@ class InteractionViews(TestCase):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
view(request, status.id)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as broadcast_mock:
view(request, status.id)
self.assertEqual(broadcast_mock.call_count, 1)
activity = json.loads(broadcast_mock.call_args[0][1])
self.assertEqual(activity["type"], "Announce")
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, status)
@ -112,7 +121,7 @@ class InteractionViews(TestCase):
self.assertFalse(models.Notification.objects.exists())
def test_handle_boost_unlisted(self, _):
def test_boost_unlisted(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
@ -127,7 +136,7 @@ class InteractionViews(TestCase):
boost = models.Boost.objects.get()
self.assertEqual(boost.privacy, "unlisted")
def test_handle_boost_private(self, _):
def test_boost_private(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
@ -140,7 +149,7 @@ class InteractionViews(TestCase):
view(request, status.id)
self.assertFalse(models.Boost.objects.exists())
def test_handle_boost_twice(self, _):
def test_boost_twice(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
@ -152,7 +161,7 @@ class InteractionViews(TestCase):
view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1)
def test_handle_unboost(self, _):
def test_unboost(self, _):
""" undo a boost """
view = views.Unboost.as_view()
request = self.factory.post("")

View file

@ -1,119 +0,0 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
class TagViews(TestCase):
""" tag views"""
def setUp(self):
""" we need basic test data and mocks """
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
remote_id="https://example.com/users/mouse",
)
self.group = Group.objects.create(name="editor")
self.group.permissions.add(
Permission.objects.create(
name="edit_book",
codename="edit_book",
content_type=ContentType.objects.get_for_model(models.User),
).id
)
self.work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=self.work,
)
models.SiteSettings.objects.create()
def test_tag_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Tag.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
tag = models.Tag.objects.create(name="hi there")
models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book)
request = self.factory.get("")
with patch("bookwyrm.views.tag.is_api_request") as is_api:
is_api.return_value = False
result = view(request, tag.identifier)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
request = self.factory.get("")
with patch("bookwyrm.views.tag.is_api_request") as is_api:
is_api.return_value = True
result = view(request, tag.identifier)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_tag_page_activitypub_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Tag.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
tag = models.Tag.objects.create(name="hi there")
models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book)
request = self.factory.get("", {"page": 1})
with patch("bookwyrm.views.tag.is_api_request") as is_api:
is_api.return_value = True
result = view(request, tag.identifier)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_tag(self):
""" add a tag to a book """
view = views.AddTag.as_view()
request = self.factory.post(
"",
{
"name": "A Tag!?",
"book": self.book.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
view(request)
tag = models.Tag.objects.get()
user_tag = models.UserTag.objects.get()
self.assertEqual(tag.name, "A Tag!?")
self.assertEqual(tag.identifier, "A+Tag%21%3F")
self.assertEqual(user_tag.user, self.local_user)
self.assertEqual(user_tag.book, self.book)
def test_untag(self):
""" remove a tag from a book """
view = views.RemoveTag.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
tag = models.Tag.objects.create(name="A Tag!?")
models.UserTag.objects.create(user=self.local_user, book=self.book, tag=tag)
request = self.factory.post(
"",
{
"user": self.local_user.id,
"book": self.book.id,
"name": tag.name,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
view(request)
self.assertTrue(models.Tag.objects.filter(name="A Tag!?").exists())
self.assertFalse(models.UserTag.objects.exists())

View file

@ -277,11 +277,6 @@ urlpatterns = [
# author
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()),
re_path(r"^author/(?P<author_id>\d+)/edit/?$", views.EditAuthor.as_view()),
# tags
re_path(r"^tag/(?P<tag_id>.+)\.json/?$", 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"^untag/?$", views.RemoveTag.as_view()),
# reading progress
re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"),
re_path(r"^delete-readthrough/?$", views.delete_readthrough),

View file

@ -34,7 +34,6 @@ from .shelf import create_shelf, delete_shelf
from .shelf import shelve, unshelve
from .site import Site
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag
from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following
from .user_admin import UserAdmin, UserAdminList

View file

@ -57,12 +57,7 @@ class Book(View):
)
reviews_page = paginated.get_page(request.GET.get("page"))
user_tags = readthroughs = user_shelves = other_edition_shelves = []
if request.user.is_authenticated:
user_tags = models.UserTag.objects.filter(
book=book, user=request.user
).values_list("tag__identifier", flat=True)
readthroughs = models.ReadThrough.objects.filter(
user=request.user,
book=book,
@ -87,11 +82,9 @@ class Book(View):
"review_count": reviews.count(),
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"tags": models.UserTag.objects.filter(book=book),
"lists": privacy_filter(
request.user, book.list_set.filter(listitem__approved=True)
),
"user_tags": user_tags,
"user_shelves": user_shelves,
"other_edition_shelves": other_edition_shelves,
"readthroughs": readthroughs,

View file

@ -1,73 +0,0 @@
""" tagging views"""
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
from bookwyrm.activitypub import ActivitypubResponse
from .helpers import is_api_request
# pylint: disable= no-self-use
class Tag(View):
""" tag page """
def get(self, request, tag_id):
""" see books related to a tag """
tag_obj = get_object_or_404(models.Tag, identifier=tag_id)
if is_api_request(request):
return ActivitypubResponse(tag_obj.to_activity(**request.GET))
books = models.Edition.objects.filter(
usertag__tag__identifier=tag_id
).distinct()
data = {
"books": books,
"tag": tag_obj,
}
return TemplateResponse(request, "tag.html", data)
@method_decorator(login_required, name="dispatch")
class AddTag(View):
""" add a tag to a book """
def post(self, request):
""" tag a book """
# I'm not using a form here because sometimes "name" is sent as a hidden
# field which doesn't validate
name = request.POST.get("name")
book_id = request.POST.get("book")
book = get_object_or_404(models.Edition, id=book_id)
tag_obj, _ = models.Tag.objects.get_or_create(
name=name,
)
models.UserTag.objects.get_or_create(
user=request.user,
book=book,
tag=tag_obj,
)
return redirect("/book/%s" % book_id)
@method_decorator(login_required, name="dispatch")
class RemoveTag(View):
""" remove a user's tag from a book """
def post(self, request):
""" untag a book """
name = request.POST.get("name")
tag_obj = get_object_or_404(models.Tag, name=name)
book_id = request.POST.get("book")
book = get_object_or_404(models.Edition, id=book_id)
user_tag = get_object_or_404(
models.UserTag, tag=tag_obj, book=book, user=request.user
)
user_tag.delete()
return redirect("/book/%s" % book_id)

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.