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"] 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 CoverForm(CustomForm):
class Meta: class Meta:
model = models.Book 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 .notification import Notification
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
from .tag import Tag, UserTag
from .user import User, KeyPair, AnnualGoal from .user import User, KeyPair, AnnualGoal
from .relationship import UserFollows, UserFollowRequest, UserBlocks from .relationship import UserFollows, UserFollowRequest, UserBlocks
from .report import Report, ReportComment from .report import Report, ReportComment

View file

@ -204,7 +204,9 @@ class ObjectMixin(ActivitypubMixin):
created = created or not bool(self.id) created = created or not bool(self.id)
# first off, we want to save normally no matter what # first off, we want to save normally no matter what
super().save(*args, **kwargs) super().save(*args, **kwargs)
if not broadcast: if not broadcast or (
hasattr(self, "status_type") and self.status_type == "Announce"
):
return return
# this will work for objects owned by a user (lists, shelves) # 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): def save(self, *args, **kwargs):
""" save and notify """ """ 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) super().save(*args, **kwargs)
if not self.boosted_status.user.local or self.boosted_status.user == self.user: if not self.boosted_status.user.local or self.boosted_status.user == self.user:
return 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")), ("de-de", _("German")),
("es", _("Spanish")), ("es", _("Spanish")),
("fr-fr", _("French")), ("fr-fr", _("French")),
("zh-cn", _("Simplified Chinese")), ("zh-hans", _("Simplified Chinese")),
] ]

View file

@ -1,6 +1,5 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
scroll-padding-top: 20%;
} }
body { body {
@ -30,6 +29,40 @@ body {
min-width: 75% !important; 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 /** Shelving
******************************************************************************/ ******************************************************************************/
@ -86,6 +119,13 @@ body {
} }
} }
/** Stars
******************************************************************************/
.stars {
white-space: nowrap;
}
/** Stars in a review form /** Stars in a review form
* *
* Specificity makes hovering taking over checked inputs. * Specificity makes hovering taking over checked inputs.
@ -256,3 +296,53 @@ body {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; 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"> <div class="columns is-multiline">
{% for user in users %} {% for user in users %}
<div class="column is-one-third"> <div class="column is-one-third">
<div class="card block"> <div class="card is-stretchable">
<div class="card-content"> <div class="card-content">
<div class="media"> <div class="media">
<a href="{{ user.local_path }}" class="media-left"> <a href="{{ user.local_path }}" class="media-left">
@ -56,13 +56,13 @@
</div> </div>
</div> </div>
<div class="content"> <div>
{% if user.summary %} {% if user.summary %}
{{ user.summary | to_markdown | safe | truncatechars_html:40 }} {{ user.summary | to_markdown | safe | truncatechars_html:40 }}
{% else %}&nbsp;{% endif %} {% else %}&nbsp;{% endif %}
</div> </div>
</div> </div>
<footer class="card-footer content"> <footer class="card-footer">
{% if user != request.user %} {% if user != request.user %}
{% if user.mutuals %} {% if user.mutuals %}
<div class="card-footer-item"> <div class="card-footer-item">

View file

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

View file

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

View file

@ -1,7 +1,8 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% if book.authors %} {% 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 %} {% else %}
<a href="{{ book.local_path }}">{{ book.title }}</a> <a href="{{ book.local_path }}">{{ book|title }}</a>
{% endif %} {% endif %}

View file

@ -4,18 +4,16 @@
{% with status.id|uuid as uuid %} {% 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 }}"> <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 %} {% csrf_token %}
<button class="button is-small" type="submit" {% if not status.boostable %}disabled{% endif %}> <button class="button is-small is-light is-transparent" type="submit" {% if not status.boostable %}disabled{% endif %}>
<span class="icon icon-boost" title="{% trans 'Boost status' %}"> <span class="icon icon-boost m-0-mobile" title="{% trans 'Boost' %}"></span>
<span class="is-sr-only">{% trans "Boost status" %}</span> <span class="is-sr-only-mobile">{% trans "Boost" %}</span>
</span>
</button> </button>
</form> </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 }}"> <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 %} {% csrf_token %}
<button class="button is-small is-primary" type="submit"> <button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-boost" title="{% trans 'Un-boost status' %}"> <span class="icon icon-boost has-text-primary m-0-mobile" title="{% trans 'Un-boost' %}"></span>
<span class="is-sr-only">{% trans "Un-boost status" %}</span> <span class="is-sr-only-mobile">{% trans "Un-boost" %}</span>
</span>
</button> </button>
</form> </form>
{% endwith %} {% endwith %}

View file

@ -3,18 +3,17 @@
{% with status.id|uuid as uuid %} {% 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 }}"> <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 %} {% csrf_token %}
<button class="button is-small" type="submit"> <button class="button is-small is-light is-transparent" type="submit">
<span class="icon icon-heart" title="{% trans 'Like status' %}"> <span class="icon icon-heart m-0-mobile" title="{% trans 'Like' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span> </span>
<span class="is-sr-only-mobile">{% trans "Like" %}</span>
</button> </button>
</form> </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 }}"> <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 %} {% csrf_token %}
<button class="button is-primary is-small" type="submit"> <button class="button is-light is-transparent is-small" type="submit">
<span class="icon icon-heart" title="{% trans 'Un-like status' %}"> <span class="icon icon-heart has-text-primary m-0-mobile" title="{% trans 'Un-like' %}"></span>
<span class="is-sr-only">{% trans "Un-like status" %}</span> <span class="is-sr-only-mobile">{% trans "Un-like" %}</span>
</span>
</button> </button>
</form> </form>
{% endwith %} {% endwith %}

View file

@ -1,10 +1,10 @@
{% load i18n %} {% load i18n %}
{% if rating %} {% 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 %} {% 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 %} {% endif %}

View file

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

View file

@ -4,16 +4,16 @@
{% load humanize %} {% load humanize %}
{% block card-header %} {% block card-header %}
<h3 class="card-header-title has-background-white-ter is-block"> <div class="card-header-title has-background-white-ter is-block">
{% include 'snippets/status/status_header.html' with status=status %} {% include 'snippets/status/status_header.html' with status=status %}
</h3> </div>
{% endblock %} {% endblock %}
{% block card-content %}{% endblock %} {% block card-content %}{% endblock %}
{% block card-footer %} {% block card-footer %}
{% if moderation_mode and perms.bookwyrm.moderate_post %}
<div class="card-footer-item"> <div class="card-footer-item">
{% if moderation_mode and perms.bookwyrm.moderate_post %}
{# moderation options #} {# moderation options #}
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post"> <form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
@ -22,54 +22,45 @@
{% trans "Delete status" %} {% trans "Delete status" %}
</button> </button>
</form> </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> </div>
{% elif no_interact %}
{# nothing here #}
{% elif request.user.is_authenticated %}
<div class="card-footer-item"> <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>
<div class="card-footer-item"> <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> </div>
{% if not moderation_mode %} {% if not moderation_mode %}
<div class="card-footer-item"> <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> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block card-bonus %} {% block card-bonus %}
{% if request.user.is_authenticated and not moderation_mode %} {% if request.user.is_authenticated and not moderation_mode %}
{% with status.id|uuid as uuid %} {% 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 bookwyrm_tags %}
{% load i18n %} {% load i18n %}
<span {% load humanize %}
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" %}
<span itemprop="name">{{ status.user.display_name }}</span> <div class="media">
</a> <figure class="media-left" aria-hidden="true">
</span> <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' %} <div class="media-content">
{{ status.content | safe }} <h3 class="has-text-weight-bold">
{% elif status.status_type == 'Rating' %} <span
{% trans "rated" %} itemprop="author"
{% elif status.status_type == 'Review' %} itemscope
{% trans "reviewed" %} itemtype="https://schema.org/Person"
{% elif status.status_type == 'Comment' %} >
{% trans "commented on" %} {% if status.user.avatar %}
{% elif status.status_type == 'Quotation' %} <meta itemprop="image" content="/images/{{ status.user.avatar }}">
{% trans "quoted" %} {% endif %}
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
{% if parent_status.status_type == 'Review' %} <a
{% 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 %} href="{{ status.user.local_path }}"
{% elif parent_status.status_type == 'Comment' %} itemprop="url"
{% 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' %} <span itemprop="name">{{ status.user.display_name }}</span>
{% 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 %} </a>
{% else %} </span>
{% 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 %}
{% endwith %} {% if status.status_type == 'GeneratedNote' %}
{% endif %} {{ 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 %} {% 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 %}
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %} {% endwith %}
<a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>{% if status.status_type == 'Rating' %}: {% endif %}
<span
itemprop="reviewRating"
itemscope
itemtype="https://schema.org/Rating"
>
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{# @todo Is it possible to not hard-code the value? #} {% if status.book %}
<meta itemprop="bestRating" content="5"> {% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
</span> <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 %} {% if status.book %}
{% endif %} {% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
{% else %} <a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
{% include 'snippets/book_titleby.html' with book=status.book %} <span
{% endif %} itemprop="reviewRating"
{% elif status.mention_books %} itemscope
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first.title }}</a> itemtype="https://schema.org/Rating"
{% endif %} >
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{% if status.progress %} {# @todo Is it possible to not hard-code the value? #}
<p class="help"> <meta itemprop="bestRating" content="5">
({% if status.progress_mode == 'PG' %}{% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}{% else %}{{ status.progress }}%{% endif %}) </span>
</p>
{% 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 %}
{% 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 %} {% load bookwyrm_tags %}
{% block dropdown-trigger %} {% block dropdown-trigger %}
<span class="icon icon-dots-three"> <span class="icon icon-dots-three m-0-mobile"></span>
<span class="is-sr-only">{% trans "More options" %}</span> <span class="is-sr-only-mobile">{% trans "More options" %}</span>
</span>
{% endblock %} {% endblock %}
{% block dropdown-list %} {% 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 %} {% 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 class="is-sr-only">{{ text }}</span>
</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 %} {% else %}
<span>{{ text }}</span> <span>{{ text }}</span>
{% endif %} {% 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 class="block">
<div> <div>
{% if books|length > 0 %} {% if books|length > 0 %}
<div class="scroll-x"> <table class="table is-striped is-fullwidth is-mobile">
<table class="table is-striped is-fullwidth"> <thead>
<tr>
<tr class="book-preview"> <th>{% trans "Cover" %}</th>
<th>{% trans "Cover" %}</th> <th>{% trans "Title" %}</th>
<th>{% trans "Title" %}</th> <th>{% trans "Author" %}</th>
<th>{% trans "Author" %}</th> <th>{% trans "Shelved" %}</th>
<th>{% trans "Shelved" %}</th> <th>{% trans "Started" %}</th>
<th>{% trans "Started" %}</th> <th>{% trans "Finished" %}</th>
<th>{% trans "Finished" %}</th> {% if ratings %}<th>{% trans "Rating" %}</th>{% endif %}
{% if ratings %}<th>{% trans "Rating" %}</th>{% endif %} {% if shelf.user == request.user %}
{% if shelf.user == request.user %} <th aria-hidden="true"></th>
<th aria-hidden="true"></th> {% endif %}
{% endif %} </tr>
</tr> </thead>
{% for book in books %} <tbody>
<tr class="book-preview"> {% for book in books %}
<td> {% spaceless %}
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a> <tr class="book-preview">
</td> <td class="book-preview-top-row">
<td> <a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
<a href="{{ book.local_path }}">{{ book.title }}</a> </td>
</td> <td data-title="{% trans "Title" %}">
<td> <a href="{{ book.local_path }}">{{ book.title }}</a>
{% include 'snippets/authors.html' %} </td>
</td> <td data-title="{% trans "Author" %}">
<td> {% include 'snippets/authors.html' %}
{{ book.created_date | naturalday }} </td>
</td> <td data-title="{% trans "Shelved" %}">
{% latest_read_through book user as read_through %} {{ book.created_date | naturalday }}
<td> </td>
{{ read_through.start_date | naturalday |default_if_none:""}} {% latest_read_through book user as read_through %}
</td> <td data-title="{% trans "Started" %}">
<td> {{ read_through.start_date | naturalday |default_if_none:""}}
{{ read_through.finish_date | naturalday |default_if_none:""}} </td>
</td> <td data-title="{% trans "Finished" %}">
{% if ratings %} {{ read_through.finish_date | naturalday |default_if_none:""}}
<td> </td>
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} {% if ratings %}
</td> <td data-title="{% trans "Rating" %}">
{% endif %} {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
{% if shelf.user == request.user %} </td>
<td> {% endif %}
{% with right=True %} {% if shelf.user == request.user %}
{% if not shelf.id %} <td class="book-preview-top-row has-text-right">
{% active_shelf book as current %} {% with right=True %}
{% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %} {% if not shelf.id %}
{% else %} {% active_shelf book as current %}
{% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %} {% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %}
{% endif %} {% else %}
{% endwith %} {% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %}
</td> {% endif %}
{% endif %} {% endwith %}
</tr> </td>
{% endfor %} {% endif %}
</tr>
{% endspaceless %}
{% endfor %}
</tbody>
</table> </table>
</div>
{% else %} {% else %}
<p>{% trans "This shelf is empty." %}</p> <p>{% trans "This shelf is empty." %}</p>
{% if shelf.id and shelf.editable %} {% if shelf.id and shelf.editable %}

View file

@ -1,7 +1,7 @@
""" template filters """ """ template filters """
from uuid import uuid4 from uuid import uuid4
from django import template from django import template, utils
from django.db.models import Avg from django.db.models import Avg
from bookwyrm import models, views from bookwyrm import models, views
@ -168,6 +168,17 @@ def get_next_shelf(current_shelf):
return "to-read" 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) @register.simple_tag(takes_context=False)
def related_status(notification): def related_status(notification):
""" for notifications """ """ for notifications """
@ -217,3 +228,10 @@ def active_read_through(book, user):
def comparison_bool(str1, str2): def comparison_bool(str1, str2):
""" idk why I need to write a tag for this, it reutrns a bool """ """ idk why I need to write a tag for this, it reutrns a bool """
return str1 == str2 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, *_): def test_review_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """ """ subclass of the base model version with a "pure" serializer """
status = models.Review.objects.create( status = models.Review.objects.create(
name="Review name", name="Review's name",
content="test content", content="test content",
rating=3.0, rating=3.0,
user=self.local_user, user=self.local_user,
@ -280,7 +280,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
activity["name"], 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["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")

View file

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

View file

@ -1,4 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
import json
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -39,7 +40,7 @@ class InteractionViews(TestCase):
parent_work=work, parent_work=work,
) )
def test_handle_favorite(self, _): def test_favorite(self, _):
""" create and broadcast faving a status """ """ create and broadcast faving a status """
view = views.Favorite.as_view() view = views.Favorite.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -57,7 +58,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_user, self.remote_user)
def test_handle_unfavorite(self, _): def test_unfavorite(self, _):
""" unfav a status """ """ unfav a status """
view = views.Unfavorite.as_view() view = views.Unfavorite.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -74,7 +75,7 @@ class InteractionViews(TestCase):
self.assertEqual(models.Favorite.objects.count(), 0) self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0)
def test_handle_boost(self, _): def test_boost(self, _):
""" boost a status """ """ boost a status """
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -85,6 +86,7 @@ class InteractionViews(TestCase):
view(request, status.id) view(request, status.id)
boost = models.Boost.objects.get() boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, status) self.assertEqual(boost.boosted_status, status)
self.assertEqual(boost.user, self.remote_user) self.assertEqual(boost.user, self.remote_user)
self.assertEqual(boost.privacy, "public") self.assertEqual(boost.privacy, "public")
@ -95,7 +97,7 @@ class InteractionViews(TestCase):
self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_status, status) self.assertEqual(notification.related_status, status)
def test_handle_self_boost(self, _): def test_self_boost(self, _):
""" boost your own status """ """ boost your own status """
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -103,7 +105,14 @@ class InteractionViews(TestCase):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"): with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi") 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() boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, status) self.assertEqual(boost.boosted_status, status)
@ -112,7 +121,7 @@ class InteractionViews(TestCase):
self.assertFalse(models.Notification.objects.exists()) self.assertFalse(models.Notification.objects.exists())
def test_handle_boost_unlisted(self, _): def test_boost_unlisted(self, _):
""" boost a status """ """ boost a status """
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -127,7 +136,7 @@ class InteractionViews(TestCase):
boost = models.Boost.objects.get() boost = models.Boost.objects.get()
self.assertEqual(boost.privacy, "unlisted") self.assertEqual(boost.privacy, "unlisted")
def test_handle_boost_private(self, _): def test_boost_private(self, _):
""" boost a status """ """ boost a status """
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -140,7 +149,7 @@ class InteractionViews(TestCase):
view(request, status.id) view(request, status.id)
self.assertFalse(models.Boost.objects.exists()) self.assertFalse(models.Boost.objects.exists())
def test_handle_boost_twice(self, _): def test_boost_twice(self, _):
""" boost a status """ """ boost a status """
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post("") request = self.factory.post("")
@ -152,7 +161,7 @@ class InteractionViews(TestCase):
view(request, status.id) view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Boost.objects.count(), 1)
def test_handle_unboost(self, _): def test_unboost(self, _):
""" undo a boost """ """ undo a boost """
view = views.Unboost.as_view() view = views.Unboost.as_view()
request = self.factory.post("") 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 # author
re_path(r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view()), 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()), 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 # 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

@ -34,7 +34,6 @@ 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, DeleteAndRedraft from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag
from .updates import get_notification_count, get_unread_status_count from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following from .user import User, EditUser, Followers, Following
from .user_admin import UserAdmin, UserAdminList from .user_admin import UserAdmin, UserAdminList

View file

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