Merge branch 'main' into prettier

This commit is contained in:
Mouse Reeve
2021-12-28 16:29:50 -08:00
39 changed files with 2139 additions and 736 deletions

View File

@ -14,7 +14,8 @@ class LibrarythingImporter(Importer):
"""use the dataclass to create the formatted row of data"""
remove_brackets = lambda v: re.sub(r"\[|\]", "", v) if v else None
normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()}
isbn_13 = normalized["isbn_13"].split(", ")
isbn_13 = normalized.get("isbn_13")
isbn_13 = isbn_13.split(", ") if isbn_13 else []
normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None
return normalized

View File

@ -84,6 +84,7 @@ class BookWyrmModel(models.Model):
# you can see groups of which you are a member
if (
hasattr(self, "memberships")
and viewer.is_authenticated
and self.memberships.filter(user=viewer).exists()
):
return

View File

@ -14,7 +14,7 @@ VERSION = "0.1.0"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "3891b373"
JS_CACHE = "2d3181e1"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")

View File

@ -8,6 +8,41 @@ body {
flex-direction: column;
}
button {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
/* inherit font, color & alignment from ancestor */
color: inherit;
font: inherit;
text-align: inherit;
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
line-height: normal;
/* Corrects font smoothing for webkit */
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
/* Corrects inability to style clickable `input` types in iOS */
-webkit-appearance: none;
}
button::-moz-focus-inner {
/* Remove excess padding and border in Firefox 4+ */
border: 0;
padding: 0;
}
/* Better accessibility for keyboard users */
*:focus-visible {
outline-style: auto !important;
}
.image {
overflow: hidden;
}
@ -29,10 +64,30 @@ body {
overflow-x: auto;
}
/* stylelint-disable no-descending-specificity */
.modal-card:focus {
outline-style: auto;
}
.modal-card:focus:not(:focus-visible) {
outline-style: initial;
}
.modal-card:focus-visible {
outline-style: auto;
}
/* stylelint-enable no-descending-specificity */
.modal-card.is-fullwidth {
min-width: 75% !important;
}
@media only screen and (min-width: 769px) {
.modal-card.is-thin {
width: 350px !important;
}
}
.modal-card-body {
max-height: 70vh;
}
@ -612,17 +667,16 @@ ol.ordered-list li::before {
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10em, 1fr));
gap: 1.5rem;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
align-items: end;
justify-items: center;
justify-items: stretch;
}
.books-grid > .is-big {
grid-column: span 2;
grid-row: span 2;
justify-self: stretch;
padding: 1.5rem 1.5rem 0;
}
.books-grid .book-cover {
@ -638,6 +692,17 @@ ol.ordered-list li::before {
min-height: calc(2 * var(--height-basis));
}
@media only screen and (min-width: 769px) {
.books-grid {
gap: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(10em, 1fr));
}
.books-grid > .is-big {
padding: 1.5rem 1.5rem 0;
}
}
/* Copy
******************************************************************************/

View File

@ -34,6 +34,10 @@ let BookWyrm = new (class {
.querySelectorAll('input[type="file"]')
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
document
.querySelectorAll("button[data-modal-open]")
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
document
.querySelectorAll("[data-duplicate]")
.forEach((node) => node.addEventListener("click", this.duplicateInput.bind(this)));
@ -400,6 +404,93 @@ let BookWyrm = new (class {
}
}
/**
* Handle the modal component.
*
* @param {Event} event - Event fired by an element
* with the `data-modal-open` attribute
* pointing to a modal by its id.
* @return {undefined}
*
* See https://github.com/bookwyrm-social/bookwyrm/pull/1633
* for information about using the modal.
*/
handleModalButton(event) {
const modalButton = event.currentTarget;
const targetModalId = modalButton.dataset.modalOpen;
const htmlElement = document.querySelector("html");
const modal = document.getElementById(targetModalId);
if (!modal) {
return;
}
// Helper functions
function handleModalOpen(modalElement) {
htmlElement.classList.add("is-clipped");
modalElement.classList.add("is-active");
modalElement.getElementsByClassName("modal-card")[0].focus();
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
closeButtons.forEach((button) => {
button.addEventListener("click", function () {
handleModalClose(modalElement);
});
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
handleModalClose(modalElement);
}
});
modalElement.addEventListener("keydown", handleFocusTrap);
}
function handleModalClose(modalElement) {
modalElement.removeEventListener("keydown", handleFocusTrap);
htmlElement.classList.remove("is-clipped");
modalElement.classList.remove("is-active");
modalButton.focus();
}
function handleFocusTrap(event) {
if (event.key !== "Tab") {
return;
}
const focusableEls = event.currentTarget.querySelectorAll(
[
"a[href]:not([disabled])",
"button:not([disabled])",
"textarea:not([disabled])",
'input:not([type="hidden"]):not([disabled])',
"select:not([disabled])",
"details:not([disabled])",
'[tabindex]:not([tabindex="-1"]):not([disabled])',
].join(",")
);
const firstFocusableEl = focusableEls[0];
const lastFocusableEl = focusableEls[focusableEls.length - 1];
if (event.shiftKey) {
/* Shift + tab */ if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
event.preventDefault();
}
} /* Tab */ else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
event.preventDefault();
}
}
}
// Open modal
handleModalOpen(modal);
}
/**
* Display pop up window.
*

View File

@ -1,6 +1,7 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load static %}
{% load humanize %}
{% block title %}{% blocktrans %}{{ year }} in the books{% endblocktrans %}{% endblock %}
@ -57,7 +58,15 @@
{% if year_key %}
<div class="horizontal-copy mb-5">
<textarea rows="1" readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy address' %}" data-copytext-success="{% trans 'Copied!' %}">{{ request.scheme|add:"://"|add:request.get_host|add:request.path }}?key={{ year_key }}</textarea>
<textarea
rows="1"
readonly
class="textarea is-small"
aria-labelledby="embed-label"
data-copytext
data-copytext-label="{% trans 'Copy address' %}"
data-copytext-success="{% trans 'Copied!' %}"
>{{ request.scheme|add:"://"|add:request.get_host|add:request.path }}?key={{ year_key }}</textarea>
</div>
{% endif %}
@ -106,12 +115,16 @@
<div class="columns is-mobile">
<div class="column is-8 is-offset-2 has-text-centered">
<h2 class="title is-3 is-serif">
{% blocktrans %}In {{ year }}, {{ display_name }} read {{ books_total }} books<br />for a total of {{ pages_total }} pages!{% endblocktrans %}
{% blocktrans trimmed count counter=books_total with pages_total=pages_total|intcomma %}
In {{ year }}, {{ display_name }} read {{ books_total }} book<br />for a total of {{ pages_total }} pages!
{% plural %}
In {{ year }}, {{ display_name }} read {{ books_total }} books<br />for a total of {{ pages_total }} pages!
{% endblocktrans %}
</h2>
<p class="subtitle is-5">{% trans "Thats great!" %}</p>
<p class="title is-4 is-serif">
{% blocktrans %}That makes an average of {{ pages_average }} pages per book.{% endblocktrans %}
{% blocktrans with pages=pages_average|intcomma %}That makes an average of {{ pages }} pages per book.{% endblocktrans %}
</p>
{% if no_page_number %}
@ -127,9 +140,9 @@
</div>
{% if book_pages_lowest and book_pages_highest %}
<div class="columns is-mobile is-align-items-center mt-5">
<div class="columns is-align-items-center mt-5">
<div class="column is-2 is-offset-1">
<a href="{{ book_pages_lowest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_lowest cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
<a href="{{ book_pages_lowest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_lowest cover_class='is-w-auto-tablet is-h-l-mobile' size='xxlarge' %}</a>
</div>
<div class="column is-3">
{% trans "Their shortest read this year…" %}
@ -150,7 +163,7 @@
</p>
</div>
<div class="column is-2">
<a href="{{ book_pages_highest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_highest cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
<a href="{{ book_pages_highest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_highest cover_class='is-w-auto-tablet is-h-l-mobile' size='xxlarge' %}</a>
</div>
<div class="column is-3">
{% trans "…and the longest" %}
@ -183,13 +196,17 @@
<div class="columns">
<div class="column has-text-centered">
<h2 class="title is-3 is-serif">
{% blocktrans %}{{ display_name }} left {{ ratings_total }} ratings, <br />their average rating is {{ rating_average }}{% endblocktrans %}
{% blocktrans trimmed count counter=ratings_total %}
{{ display_name }} left {{ ratings_total }} rating, <br />their average rating is {{ rating_average }}
{% plural %}
{{ display_name }} left {{ ratings_total }} ratings, <br />their average rating is {{ rating_average }}
{% endblocktrans %}
</h2>
</div>
</div>
<div class="columns is-align-items-center">
<div class="column is-3 is-offset-3">
<a href="{{ book_rating_highest.book.local_path }}">{% include 'snippets/book_cover.html' with book=book_rating_highest.book cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
<div class="column is-2 is-offset-4">
<a href="{{ book_rating_highest.book.local_path }}">{% include 'snippets/book_cover.html' with book=book_rating_highest.book cover_class='is-w-xl-tablet is-h-l-mobile' size='xxlarge' %}</a>
</div>
{% if book_rating_highest %}
<div class="column is-4">
@ -223,7 +240,7 @@
<div class="columns">
<div class="column has-text-centered">
<h2 class="title is-3 is-serif">
{% blocktrans %}All the books {{ display_name }} read in 2021{% endblocktrans %}
{% blocktrans %}All the books {{ display_name }} read in {{ year }}{% endblocktrans %}
</h2>
</div>
</div>
@ -234,14 +251,14 @@
{% for book in books %}
{% if book.id in best_ratings_books_ids %}
<a href="{{ book.local_path }}" class="has-text-centered is-big has-text-success-dark">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' size='xxlarge' %}
<span class="book-title is-serif is-size-5">
{{ book.title }}
</span>
</a>
{% else %}
<a href="{{ book.local_path }}" class="has-text-centered has-text-success-dark">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' size='xlarge' %}
<span class="book-title is-serif is-size-6">
{{ book.title }}
</span>

View File

@ -73,7 +73,7 @@
{% endif %}
{% if annual_summary_year and tab.key == 'home' %}
<section class="block">
<section class="block" data-hide="hide_annual_summary_{{ annual_summary_year }}">
{% include 'feed/summary_card.html' with year=annual_summary_year %}
<hr>
</section>

View File

@ -7,6 +7,13 @@
<span class="icon is-size-3 mr-2" aria-hidden="true"></span>
{% blocktrans %}{{ year }} in the books{% endblocktrans %}
</h3>
<div class="card-header-icon has-background-success-dark has-text-white">
{% trans "Dismiss message" as button_text %}
<button class="delete set-display" type="button" data-id="hide_annual_summary_{{ year }}" data-value="true">
<span>{% trans "Dismiss message" %}</span>
</button>
</div>
{% endblock %}
{% block card-content %}

View File

@ -38,7 +38,7 @@
<a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
</a>
<form class="navbar-item column" action="{% url 'search' %}">
<form class="navbar-item column is-align-items-start pt-5" action="{% url 'search' %}">
<div class="field has-addons">
<div class="control">
{% if user.is_authenticated %}
@ -58,25 +58,22 @@
</div>
</form>
<div role="button" tabindex="0" class="navbar-burger pulldown-menu" data-controls="main_nav" aria-expanded="false">
<div class="navbar-item mt-3">
<div class="icon icon-dots-three-vertical" title="{% trans 'Main navigation menu' %}">
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>
</div>
</div>
</div>
<button type="button" tabindex="0" class="navbar-burger pulldown-menu my-4" data-controls="main_nav" aria-expanded="false">
<i class="icon icon-dots-three-vertical" aria-hidden="true"></i>
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>
</button>
</div>
<div class="navbar-menu" id="main_nav">
<div class="navbar-start">
{% if request.user.is_authenticated %}
<a href="/#feed" class="navbar-item">
<a href="/#feed" class="navbar-item mt-3 py-0">
{% trans "Feed" %}
</a>
<a href="{% url 'lists' %}" class="navbar-item">
<a href="{% url 'lists' %}" class="navbar-item mt-3 py-0">
{% trans "Lists" %}
</a>
<a href="{% url 'discover' %}" class="navbar-item">
<a href="{% url 'discover' %}" class="navbar-item mt-3 py-0">
{% trans "Discover" %}
</a>
{% endif %}
@ -84,7 +81,7 @@
<div class="navbar-end">
{% if request.user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
<a
href="{{ request.user.local_path }}"
class="navbar-link pulldown-menu"
@ -143,7 +140,7 @@
</li>
</ul>
</div>
<div class="navbar-item">
<div class="navbar-item mt-3 py-0">
<a href="{% url 'notifications' %}" class="tags has-addons">
<span class="tag is-medium">
<span class="icon icon-bell" title="{% trans 'Notifications' %}">
@ -161,7 +158,7 @@
</a>
</div>
{% else %}
<div class="navbar-item">
<div class="navbar-item pt-5 pb-0">
{% if request.path != '/login' and request.path != '/login/' %}
<div class="columns">
<div class="column">

View File

@ -1,4 +1,4 @@
Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End
5498194 Marelle 1 Cortazar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cort<72>azar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814
5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910
5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151
5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151

1 Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End
2 5498194 Marelle 1 Cortazar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cortâazar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814
3 5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910
4 5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151

View File

@ -48,7 +48,9 @@ class OpenLibraryImport(TestCase):
self.local_user, self.csv, False, "public"
)
import_items = models.ImportItem.objects.filter(job=import_job).all()
import_items = (
models.ImportItem.objects.filter(job=import_job).order_by("index").all()
)
self.assertEqual(len(import_items), 4)
self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].data["Work Id"], "OL102749W")

View File

@ -60,6 +60,18 @@ class ShelfViews(TestCase):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_shelf_page_all_books_json(self, *_):
"""there is no json view here"""
view = views.Shelf.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api:
is_api.return_value = True
result = view(request, self.local_user.username)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_shelf_page_all_books_anonymous(self, *_):
"""there are so many views, this just makes sure it LOADS"""
view = views.Shelf.as_view()

View File

@ -36,6 +36,13 @@ class FeedViews(TestCase):
local=True,
localname="mouse",
)
self.another_user = models.User.objects.create_user(
"nutria@local.com",
"nutria@nutria.nutria",
"password",
local=True,
localname="nutria",
)
self.book = models.Edition.objects.create(
parent_work=models.Work.objects.create(title="hi"),
title="Example Edition",
@ -171,6 +178,17 @@ class FeedViews(TestCase):
result.render()
self.assertEqual(result.status_code, 200)
def test_direct_messages_page_user(self, *_):
"""there are so many views, this just makes sure it LOADS"""
view = views.DirectMessage.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request, "nutria")
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertEqual(result.context_data["partner"], self.another_user)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
def test_get_suggested_book(self, *_):

View File

@ -1,6 +1,8 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.http import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -43,6 +45,8 @@ class GroupViews(TestCase):
self.membership = models.GroupMember.objects.create(
group=self.testgroup, user=self.local_user
)
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
models.SiteSettings.objects.create()
@ -56,6 +60,17 @@ class GroupViews(TestCase):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_group_get_anonymous(self, _):
"""there are so many views, this just makes sure it LOADS"""
self.testgroup.privacy = "followers"
self.testgroup.save()
view = views.Group.as_view()
request = self.factory.get("")
request.user = self.anonymous_user
with self.assertRaises(Http404):
view(request, group_id=self.testgroup.id)
def test_usergroups_get(self, _):
"""there are so many views, this just makes sure it LOADS"""
view = views.UserGroups.as_view()

View File

@ -80,6 +80,29 @@ class UserViews(TestCase):
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_user_page_domain(self):
"""when the user domain has dashes in it"""
with patch("bookwyrm.models.user.set_remote_server"):
self.remote_user = models.User.objects.create_user(
"nutria",
"",
"nutriaword",
local=False,
remote_id="https://ex--ample.co----m/users/nutria",
inbox="https://ex--ample.co----m/users/nutria/inbox",
outbox="https://ex--ample.co----m/users/nutria/outbox",
)
view = views.User.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.user.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "nutria@ex--ample.co----m")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_user_page_blocked(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.User.as_view()
@ -186,3 +209,11 @@ class UserViews(TestCase):
self.local_user.refresh_from_db()
self.assertFalse(self.local_user.show_suggested_users)
def test_user_redirect(self):
"""test the basic redirect"""
request = self.factory.get("@mouse")
request.user = self.anonymous_user
result = views.user_redirect(request, "mouse")
self.assertEqual(result.status_code, 302)

View File

@ -50,7 +50,7 @@ urlpatterns = [
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
# authentication
re_path(r"^login/?$", views.Login.as_view(), name="login"),
re_path(r"^login/(?P<confirmed>confirmed)?$", views.Login.as_view(), name="login"),
re_path(r"^login/(?P<confirmed>confirmed)/?$", views.Login.as_view(), name="login"),
re_path(r"^register/?$", views.Register.as_view()),
re_path(r"confirm-email/?$", views.ConfirmEmail.as_view(), name="confirm-email"),
re_path(
@ -112,12 +112,12 @@ urlpatterns = [
name="settings-federated-server",
),
re_path(
r"^settings/federation/(?P<server>\d+)/block?$",
r"^settings/federation/(?P<server>\d+)/block/?$",
views.block_server,
name="settings-federated-server-block",
),
re_path(
r"^settings/federation/(?P<server>\d+)/unblock?$",
r"^settings/federation/(?P<server>\d+)/unblock/?$",
views.unblock_server,
name="settings-federated-server-unblock",
),
@ -140,7 +140,7 @@ urlpatterns = [
name="settings-invite-requests",
),
re_path(
r"^settings/requests/ignore?$",
r"^settings/requests/ignore/?$",
views.ignore_invite_request,
name="settings-invite-requests-ignore",
),
@ -229,7 +229,7 @@ urlpatterns = [
r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages"
),
re_path(
rf"^direct-messages/(?P<username>{regex.USERNAME})?$",
rf"^direct-messages/(?P<username>{regex.USERNAME})/?$",
views.DirectMessage.as_view(),
name="direct-messages-user",
),
@ -276,6 +276,7 @@ urlpatterns = [
# users
re_path(rf"{USER_PATH}\.json$", views.User.as_view()),
re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"),
re_path(rf"^@(?P<username>{regex.USERNAME})$", views.user_redirect),
re_path(rf"{USER_PATH}/rss/?$", views.rss_feed.RssFeed(), name="user-rss"),
re_path(
rf"{USER_PATH}/followers(.json)?/?$",
@ -338,7 +339,7 @@ urlpatterns = [
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
re_path(
r"^list/(?P<list_id>\d+)/embed/(?P<list_key>[0-9a-f]+)?$",
r"^list/(?P<list_id>\d+)/embed/(?P<list_key>[0-9a-f]+)/?$",
views.unsafe_embed_list,
name="embed-list",
),
@ -355,7 +356,7 @@ urlpatterns = [
name="shelf",
),
re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"),
re_path(r"^delete-shelf/(?P<shelf_id>\d+)?$", views.delete_shelf),
re_path(r"^delete-shelf/(?P<shelf_id>\d+)/?$", views.delete_shelf),
re_path(r"^shelve/?$", views.shelve),
re_path(r"^unshelve/?$", views.unshelve),
# goals
@ -422,7 +423,7 @@ urlpatterns = [
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
re_path(rf"{BOOK_PATH}/confirm/?$", views.ConfirmEditBook.as_view()),
re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"),
re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()),
re_path(r"^create-book/confirm/?$", views.ConfirmEditBook.as_view()),
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
re_path(
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"

View File

@ -1,6 +1,6 @@
""" defining regexes for regularly used concepts """
DOMAIN = r"[\w_\-\.]+\.[a-z]{2,}"
DOMAIN = r"[\w_\-\.]+\.[a-z\-]{2,}"
LOCALNAME = r"@?[a-zA-Z_\-\.0-9]+"
STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"

View File

@ -94,7 +94,7 @@ from .search import Search
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress
from .status import edit_readthrough
from .updates import get_notification_count, get_unread_status_count
from .user import User, Followers, Following, hide_suggestions
from .user import User, Followers, Following, hide_suggestions, user_redirect
from .wellknown import *
from .annual_summary import (
AnnualSummary,

View File

@ -5,7 +5,7 @@ from uuid import uuid4
from django.contrib.auth.decorators import login_required
from django.db.models import Case, When, Avg, Sum
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.views import View
from django.views.decorators.http import require_POST
@ -41,7 +41,14 @@ class AnnualSummary(View):
)
# get data
read_book_ids_in_year = get_read_book_ids_in_year(user, year)
read_book_ids_in_year = (
user.readthrough_set.filter(
finish_date__year__gte=year,
finish_date__year__lt=int(year) + 1,
)
.order_by("finish_date")
.values_list("book__id", flat=True)
)
if len(read_book_ids_in_year) == 0:
data = {
@ -222,45 +229,6 @@ def get_earliest_year(user, year):
return year
def get_read_book_ids_in_year(user, year):
"""return an ordered QuerySet of the read book ids"""
read_shelf = get_object_or_404(user.shelf_set, identifier="read")
shelved_book_ids = (
models.ShelfBook.objects.filter(shelf=read_shelf)
.filter(user=user)
.values_list("book", "shelved_date")
)
book_dates = []
for book in shelved_book_ids:
finished_in_year = (
models.ReadThrough.objects.filter(user__id=user.id)
.filter(book_id=book[0])
.filter(finish_date__year=year)
.values("finish_date")
.first()
)
if finished_in_year:
# Finished a readthrough in the year
book_dates.append((book[0], finished_in_year["finish_date"]))
else:
has_other_year_readthrough = (
models.ReadThrough.objects.filter(user__id=user.id)
.filter(book_id=book[0])
.exists()
)
if not has_other_year_readthrough and book[1].year == int(year):
# No readthrough but shelved this year
book_dates.append(book)
book_dates = sorted(book_dates, key=lambda tup: tup[1])
return [book[0] for book in book_dates]
def get_books_from_shelfbooks(books_ids):
"""return an ordered QuerySet of books from a list"""

View File

@ -39,7 +39,8 @@ class Login(View):
return redirect("/")
login_form = forms.LoginForm(request.POST)
localname = login_form.data["localname"]
localname = login_form.data.get("localname")
if "@" in localname: # looks like an email address to me
try:
username = models.User.objects.get(email=localname).username
@ -47,7 +48,7 @@ class Login(View):
username = localname
else:
username = f"{localname}@{DOMAIN}"
password = login_form.data["password"]
password = login_form.data.get("password")
# perform authentication
user = authenticate(request, username=username, password=password)

View File

@ -52,7 +52,7 @@ class Shelf(View):
)
shelf = FakeShelf("all", _("All books"), user, books, "public")
if is_api_request(request):
if is_api_request(request) and shelf_identifier:
return ActivitypubResponse(shelf.to_activity(**request.GET))
reviews = models.Review.objects

View File

@ -151,3 +151,9 @@ def hide_suggestions(request):
request.user.show_suggested_users = False
request.user.save(broadcast=False, update_fields=["show_suggested_users"])
return redirect(request.headers.get("Referer", "/"))
# pylint: disable=unused-argument
def user_redirect(request, username):
"""redirect to a user's feed"""
return redirect("user-feed", username=username)