Compare commits

..

2 commits

Author SHA1 Message Date
Mouse Reeve
a2a04da493 Adds many to many related items to notifications 2022-04-09 09:44:42 -07:00
Mouse Reeve
8d266fda4d Removes unused related_book field on notification model 2022-04-08 15:21:38 -07:00
144 changed files with 2429 additions and 2980 deletions

View file

@ -21,7 +21,8 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
pip install pylint
- name: Analysing the code with pylint - name: Analysing the code with pylint
run: | run: |
pylint bookwyrm/ pylint bookwyrm/ --ignore=migrations --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801

View file

@ -1,6 +0,0 @@
[MAIN]
ignore=migrations
load-plugins=pylint.extensions.no_self_use
[MESSAGES CONTROL]
disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801,C3001

View file

@ -6,7 +6,6 @@ RUN mkdir /app /app/static /app/images
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install -r requirements.txt --no-cache-dir RUN pip install -r requirements.txt --no-cache-dir
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean

View file

@ -16,7 +16,7 @@ If you'd like to join an instance, you can check out the [instances](https://joi
## Contributing ## Contributing
See [contributing](https://docs.joinbookwyrm.com/contributing.html) for code, translation or monetary contributions. See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions.
## About BookWyrm ## About BookWyrm
### What it is and isn't ### What it is and isn't
@ -76,4 +76,4 @@ Deployment
## Set up BookWyrm ## Set up BookWyrm
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/install-dev.html) or [production](https://docs.joinbookwyrm.com/install-prod.html). The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html).

View file

@ -1,7 +1,6 @@
""" basics for an activitypub serializer """ """ basics for an activitypub serializer """
from dataclasses import dataclass, fields, MISSING from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder from json import JSONEncoder
import logging
from django.apps import apps from django.apps import apps
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
@ -9,8 +8,6 @@ from django.db import IntegrityError, transaction
from bookwyrm.connectors import ConnectorException, get_data from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.tasks import app from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
class ActivitySerializerError(ValueError): class ActivitySerializerError(ValueError):
"""routine problems serializing activitypub json""" """routine problems serializing activitypub json"""
@ -42,12 +39,12 @@ def naive_parse(activity_objects, activity_json, serializer=None):
activity_json["type"] = "PublicKey" activity_json["type"] = "PublicKey"
activity_type = activity_json.get("type") activity_type = activity_json.get("type")
if activity_type in ["Question", "Article"]:
return None
try: try:
serializer = activity_objects[activity_type] serializer = activity_objects[activity_type]
except KeyError as err: except KeyError as err:
# we know this exists and that we can't handle it # we know this exists and that we can't handle it
if activity_type in ["Question"]:
return None
raise ActivitySerializerError(err) raise ActivitySerializerError(err)
return serializer(activity_objects=activity_objects, **activity_json) return serializer(activity_objects=activity_objects, **activity_json)
@ -68,7 +65,7 @@ class ActivityObject:
try: try:
value = kwargs[field.name] value = kwargs[field.name]
if value in (None, MISSING, {}): if value in (None, MISSING, {}):
raise KeyError("Missing required field", field.name) raise KeyError()
try: try:
is_subclass = issubclass(field.type, ActivityObject) is_subclass = issubclass(field.type, ActivityObject)
except TypeError: except TypeError:
@ -271,9 +268,9 @@ def resolve_remote_id(
try: try:
data = get_data(remote_id) data = get_data(remote_id)
except ConnectorException: except ConnectorException:
logger.exception("Could not connect to host for remote_id: %s", remote_id) raise ActivitySerializerError(
return None f"Could not connect to host for remote_id: {remote_id}"
)
# determine the model implicitly, if not provided # determine the model implicitly, if not provided
# or if it's a model with subclasses like Status, check again # or if it's a model with subclasses like Status, check again
if not model or hasattr(model.objects, "select_subclasses"): if not model or hasattr(model.objects, "select_subclasses"):

View file

@ -298,9 +298,8 @@ def add_status_on_create_command(sender, instance, created):
priority = HIGH priority = HIGH
# check if this is an old status, de-prioritize if so # check if this is an old status, de-prioritize if so
# (this will happen if federation is very slow, or, more expectedly, on csv import) # (this will happen if federation is very slow, or, more expectedly, on csv import)
if instance.published_date < timezone.now() - timedelta( one_day = 60 * 60 * 24
days=1 if (instance.created_date - instance.published_date).seconds > one_day:
) or instance.created_date < instance.published_date - timedelta(days=1):
priority = LOW priority = LOW
add_status_task.apply_async( add_status_task.apply_async(

View file

@ -148,8 +148,8 @@ class SearchResult:
def __repr__(self): def __repr__(self):
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
return "<SearchResult key={!r} title={!r} author={!r} confidence={!r}>".format( return "<SearchResult key={!r} title={!r} author={!r}>".format(
self.key, self.title, self.author, self.confidence self.key, self.title, self.author
) )
def json(self): def json(self):

View file

@ -1,8 +1,9 @@
""" functionality outline for a book data connector """ """ functionality outline for a book data connector """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import imghdr import imghdr
import ipaddress
import logging import logging
import re from urllib.parse import urlparse
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
@ -10,7 +11,7 @@ import requests
from requests.exceptions import RequestException from requests.exceptions import RequestException
from bookwyrm import activitypub, models, settings from bookwyrm import activitypub, models, settings
from .connector_manager import load_more_data, ConnectorException, raise_not_valid_url from .connector_manager import load_more_data, ConnectorException
from .format_mappings import format_mappings from .format_mappings import format_mappings
@ -38,34 +39,62 @@ class AbstractMinimalConnector(ABC):
for field in self_fields: for field in self_fields:
setattr(self, field, getattr(info, field)) setattr(self, field, getattr(info, field))
def get_search_url(self, query): def search(self, query, min_confidence=None, timeout=settings.QUERY_TIMEOUT):
"""format the query url""" """free text search"""
# Check if the query resembles an ISBN params = {}
if maybe_isbn(query) and self.isbn_search_url and self.isbn_search_url != "": if min_confidence:
return f"{self.isbn_search_url}{query}" params["min_confidence"] = min_confidence
# NOTE: previously, we tried searching isbn and if that produces no results, data = self.get_search_data(
# searched as free text. This, instead, only searches isbn if it's isbn-y f"{self.search_url}{query}",
return f"{self.search_url}{query}" params=params,
timeout=timeout,
)
results = []
def process_search_response(self, query, data, min_confidence): for doc in self.parse_search_data(data)[:10]:
"""Format the search results based on the formt of the query""" results.append(self.format_search_result(doc))
if maybe_isbn(query): return results
return list(self.parse_isbn_search_data(data))[:10]
return list(self.parse_search_data(data, min_confidence))[:10] def isbn_search(self, query, timeout=settings.QUERY_TIMEOUT):
"""isbn search"""
params = {}
data = self.get_search_data(
f"{self.isbn_search_url}{query}",
params=params,
timeout=timeout,
)
results = []
# this shouldn't be returning mutliple results, but just in case
for doc in self.parse_isbn_search_data(data)[:10]:
results.append(self.format_isbn_search_result(doc))
return results
def get_search_data(self, remote_id, **kwargs): # pylint: disable=no-self-use
"""this allows connectors to override the default behavior"""
return get_data(remote_id, **kwargs)
@abstractmethod @abstractmethod
def get_or_create_book(self, remote_id): def get_or_create_book(self, remote_id):
"""pull up a book record by whatever means possible""" """pull up a book record by whatever means possible"""
@abstractmethod @abstractmethod
def parse_search_data(self, data, min_confidence): def parse_search_data(self, data):
"""turn the result json from a search into a list""" """turn the result json from a search into a list"""
@abstractmethod
def format_search_result(self, search_result):
"""create a SearchResult obj from json"""
@abstractmethod @abstractmethod
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
"""turn the result json from a search into a list""" """turn the result json from a search into a list"""
@abstractmethod
def format_isbn_search_result(self, search_result):
"""create a SearchResult obj from json"""
class AbstractConnector(AbstractMinimalConnector): class AbstractConnector(AbstractMinimalConnector):
"""generic book data connector""" """generic book data connector"""
@ -225,6 +254,9 @@ def get_data(url, params=None, timeout=10):
# check if the url is blocked # check if the url is blocked
raise_not_valid_url(url) raise_not_valid_url(url)
if models.FederatedServer.is_blocked(url):
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
try: try:
resp = requests.get( resp = requests.get(
url, url,
@ -279,6 +311,20 @@ def get_image(url, timeout=10):
return image_content, extension return image_content, extension
def raise_not_valid_url(url):
"""do some basic reality checks on the url"""
parsed = urlparse(url)
if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url)
try:
ipaddress.ip_address(parsed.netloc)
raise ConnectorException("Provided url is an IP address: ", url)
except ValueError:
# it's not an IP address, which is good
pass
class Mapping: class Mapping:
"""associate a local database field with a field in an external dataset""" """associate a local database field with a field in an external dataset"""
@ -320,9 +366,3 @@ def unique_physical_format(format_text):
# try a direct match, so saving this would be redundant # try a direct match, so saving this would be redundant
return None return None
return format_text return format_text
def maybe_isbn(query):
"""check if a query looks like an isbn"""
isbn = re.sub(r"[\W_]", "", query) # removes filler characters
return len(isbn) in [10, 13] # ISBN10 or ISBN13

View file

@ -10,12 +10,15 @@ class Connector(AbstractMinimalConnector):
def get_or_create_book(self, remote_id): def get_or_create_book(self, remote_id):
return activitypub.resolve_remote_id(remote_id, model=models.Edition) return activitypub.resolve_remote_id(remote_id, model=models.Edition)
def parse_search_data(self, data, min_confidence): def parse_search_data(self, data):
for search_result in data: return data
search_result["connector"] = self
yield SearchResult(**search_result) def format_search_result(self, search_result):
search_result["connector"] = self
return SearchResult(**search_result)
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
for search_result in data: return data
search_result["connector"] = self
yield SearchResult(**search_result) def format_isbn_search_result(self, search_result):
return self.format_search_result(search_result)

View file

@ -1,18 +1,17 @@
""" interface with whatever connectors the app has """ """ interface with whatever connectors the app has """
import asyncio from datetime import datetime
import importlib import importlib
import ipaddress
import logging import logging
import re
from urllib.parse import urlparse from urllib.parse import urlparse
import aiohttp
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models import signals from django.db.models import signals
from requests import HTTPError from requests import HTTPError
from bookwyrm import book_search, models from bookwyrm import book_search, models
from bookwyrm.settings import SEARCH_TIMEOUT, USER_AGENT from bookwyrm.settings import SEARCH_TIMEOUT
from bookwyrm.tasks import app from bookwyrm.tasks import app
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,85 +21,53 @@ class ConnectorException(HTTPError):
"""when the connector can't do what was asked""" """when the connector can't do what was asked"""
async def get_results(session, url, min_confidence, query, connector):
"""try this specific connector"""
# pylint: disable=line-too-long
headers = {
"Accept": (
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
),
"User-Agent": USER_AGENT,
}
params = {"min_confidence": min_confidence}
try:
async with session.get(url, headers=headers, params=params) as response:
if not response.ok:
logger.info("Unable to connect to %s: %s", url, response.reason)
return
try:
raw_data = await response.json()
except aiohttp.client_exceptions.ContentTypeError as err:
logger.exception(err)
return
return {
"connector": connector,
"results": connector.process_search_response(
query, raw_data, min_confidence
),
}
except asyncio.TimeoutError:
logger.info("Connection timed out for url: %s", url)
except aiohttp.ClientError as err:
logger.exception(err)
async def async_connector_search(query, items, min_confidence):
"""Try a number of requests simultaneously"""
timeout = aiohttp.ClientTimeout(total=SEARCH_TIMEOUT)
async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = []
for url, connector in items:
tasks.append(
asyncio.ensure_future(
get_results(session, url, min_confidence, query, connector)
)
)
results = await asyncio.gather(*tasks)
return results
def search(query, min_confidence=0.1, return_first=False): def search(query, min_confidence=0.1, return_first=False):
"""find books based on arbitary keywords""" """find books based on arbitary keywords"""
if not query: if not query:
return [] return []
results = [] results = []
items = [] # Have we got a ISBN ?
for connector in get_connectors(): isbn = re.sub(r"[\W_]", "", query)
# get the search url from the connector before sending maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
url = connector.get_search_url(query)
try:
raise_not_valid_url(url)
except ConnectorException:
# if this URL is invalid we should skip it and move on
logger.info("Request denied to blocked domain: %s", url)
continue
items.append((url, connector))
# load as many results as we can start_time = datetime.now()
results = asyncio.run(async_connector_search(query, items, min_confidence)) for connector in get_connectors():
results = [r for r in results if r] result_set = None
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "":
# Search on ISBN
try:
result_set = connector.isbn_search(isbn)
except Exception as err: # pylint: disable=broad-except
logger.info(err)
# if this fails, we can still try regular search
# if no isbn search results, we fallback to generic search
if not result_set:
try:
result_set = connector.search(query, min_confidence=min_confidence)
except Exception as err: # pylint: disable=broad-except
# we don't want *any* error to crash the whole search page
logger.info(err)
continue
if return_first and result_set:
# if we found anything, return it
return result_set[0]
if result_set:
results.append(
{
"connector": connector,
"results": result_set,
}
)
if (datetime.now() - start_time).seconds >= SEARCH_TIMEOUT:
break
if return_first: if return_first:
# find the best result from all the responses and return that return None
all_results = [r for con in results for r in con["results"]]
all_results = sorted(all_results, key=lambda r: r.confidence, reverse=True)
return all_results[0] if all_results else None
# failed requests will return None, so filter those out
return results return results
@ -166,20 +133,3 @@ def create_connector(sender, instance, created, *args, **kwargs):
"""create a connector to an external bookwyrm server""" """create a connector to an external bookwyrm server"""
if instance.application_type == "bookwyrm": if instance.application_type == "bookwyrm":
get_or_create_connector(f"https://{instance.server_name}") get_or_create_connector(f"https://{instance.server_name}")
def raise_not_valid_url(url):
"""do some basic reality checks on the url"""
parsed = urlparse(url)
if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url)
try:
ipaddress.ip_address(parsed.netloc)
raise ConnectorException("Provided url is an IP address: ", url)
except ValueError:
# it's not an IP address, which is good
pass
if models.FederatedServer.is_blocked(url):
raise ConnectorException(f"Attempting to load data from blocked url: {url}")

View file

@ -77,42 +77,53 @@ class Connector(AbstractConnector):
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]}, **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]},
} }
def parse_search_data(self, data, min_confidence): def search(self, query, min_confidence=None): # pylint: disable=arguments-differ
for search_result in data.get("results", []): """overrides default search function with confidence ranking"""
images = search_result.get("image") results = super().search(query)
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None if min_confidence:
# a deeply messy translation of inventaire's scores # filter the search results after the fact
confidence = float(search_result.get("_score", 0.1)) return [r for r in results if r.confidence >= min_confidence]
confidence = 0.1 if confidence < 150 else 0.999 return results
if confidence < min_confidence:
continue def parse_search_data(self, data):
yield SearchResult( return data.get("results")
title=search_result.get("label"),
key=self.get_remote_id(search_result.get("uri")), def format_search_result(self, search_result):
author=search_result.get("description"), images = search_result.get("image")
view_link=f"{self.base_url}/entity/{search_result.get('uri')}", cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
cover=cover, # a deeply messy translation of inventaire's scores
confidence=confidence, confidence = float(search_result.get("_score", 0.1))
connector=self, confidence = 0.1 if confidence < 150 else 0.999
) return SearchResult(
title=search_result.get("label"),
key=self.get_remote_id(search_result.get("uri")),
author=search_result.get("description"),
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
cover=cover,
confidence=confidence,
connector=self,
)
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
"""got some daaaata""" """got some daaaata"""
results = data.get("entities") results = data.get("entities")
if not results: if not results:
return return []
for search_result in list(results.values()): return list(results.values())
title = search_result.get("claims", {}).get("wdt:P1476", [])
if not title: def format_isbn_search_result(self, search_result):
continue """totally different format than a regular search result"""
yield SearchResult( title = search_result.get("claims", {}).get("wdt:P1476", [])
title=title[0], if not title:
key=self.get_remote_id(search_result.get("uri")), return None
author=search_result.get("description"), return SearchResult(
view_link=f"{self.base_url}/entity/{search_result.get('uri')}", title=title[0],
cover=self.get_cover_url(search_result.get("image")), key=self.get_remote_id(search_result.get("uri")),
connector=self, author=search_result.get("description"),
) view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
cover=self.get_cover_url(search_result.get("image")),
connector=self,
)
def is_work_data(self, data): def is_work_data(self, data):
return data.get("type") == "work" return data.get("type") == "work"

View file

@ -152,41 +152,39 @@ class Connector(AbstractConnector):
image_name = f"{cover_id}-{size}.jpg" image_name = f"{cover_id}-{size}.jpg"
return f"{self.covers_url}/b/id/{image_name}" return f"{self.covers_url}/b/id/{image_name}"
def parse_search_data(self, data, min_confidence): def parse_search_data(self, data):
for idx, search_result in enumerate(data.get("docs")): return data.get("docs")
# build the remote id from the openlibrary key
key = self.books_url + search_result["key"]
author = search_result.get("author_name") or ["Unknown"]
cover_blob = search_result.get("cover_i")
cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
# OL doesn't provide confidence, but it does sort by an internal ranking, so def format_search_result(self, search_result):
# this confidence value is relative to the list position # build the remote id from the openlibrary key
confidence = 1 / (idx + 1) key = self.books_url + search_result["key"]
author = search_result.get("author_name") or ["Unknown"]
yield SearchResult( cover_blob = search_result.get("cover_i")
title=search_result.get("title"), cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
key=key, return SearchResult(
author=", ".join(author), title=search_result.get("title"),
connector=self, key=key,
year=search_result.get("first_publish_year"), author=", ".join(author),
cover=cover, connector=self,
confidence=confidence, year=search_result.get("first_publish_year"),
) cover=cover,
)
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
for search_result in list(data.values()): return list(data.values())
# build the remote id from the openlibrary key
key = self.books_url + search_result["key"] def format_isbn_search_result(self, search_result):
authors = search_result.get("authors") or [{"name": "Unknown"}] # build the remote id from the openlibrary key
author_names = [author.get("name") for author in authors] key = self.books_url + search_result["key"]
yield SearchResult( authors = search_result.get("authors") or [{"name": "Unknown"}]
title=search_result.get("title"), author_names = [author.get("name") for author in authors]
key=key, return SearchResult(
author=", ".join(author_names), title=search_result.get("title"),
connector=self, key=key,
year=search_result.get("publish_date"), author=", ".join(author_names),
) connector=self,
year=search_result.get("publish_date"),
)
def load_edition_data(self, olkey): def load_edition_data(self, olkey):
"""query openlibrary for editions of a work""" """query openlibrary for editions of a work"""

View file

@ -53,12 +53,7 @@ class ReadThroughForm(CustomForm):
self.add_error( self.add_error(
"finish_date", _("Reading finish date cannot be before start date.") "finish_date", _("Reading finish date cannot be before start date.")
) )
stopped_date = cleaned_data.get("stopped_date")
if start_date and stopped_date and start_date > stopped_date:
self.add_error(
"stopped_date", _("Reading stopped date cannot be before start date.")
)
class Meta: class Meta:
model = models.ReadThrough model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date", "stopped_date"] fields = ["user", "book", "start_date", "finish_date"]

View file

@ -1,7 +1,6 @@
""" import classes """ """ import classes """
from .importer import Importer from .importer import Importer
from .calibre_import import CalibreImporter
from .goodreads_import import GoodreadsImporter from .goodreads_import import GoodreadsImporter
from .librarything_import import LibrarythingImporter from .librarything_import import LibrarythingImporter
from .openlibrary_import import OpenLibraryImporter from .openlibrary_import import OpenLibraryImporter

View file

@ -1,28 +0,0 @@
""" handle reading a csv from calibre """
from bookwyrm.models import Shelf
from . import Importer
class CalibreImporter(Importer):
"""csv downloads from Calibre"""
service = "Calibre"
def __init__(self, *args, **kwargs):
# Add timestamp to row_mappings_guesses for date_added to avoid
# integrity error
row_mappings_guesses = []
for field, mapping in self.row_mappings_guesses:
if field in ("date_added",):
row_mappings_guesses.append((field, mapping + ["timestamp"]))
else:
row_mappings_guesses.append((field, mapping))
self.row_mappings_guesses = row_mappings_guesses
super().__init__(*args, **kwargs)
def get_shelf(self, normalized_row):
# Calibre export does not indicate which shelf to use. Go with a default one for now
return Shelf.TO_READ

View file

@ -1,8 +1,5 @@
""" handle reading a tsv from librarything """ """ handle reading a tsv from librarything """
import re import re
from bookwyrm.models import Shelf
from . import Importer from . import Importer
@ -24,7 +21,7 @@ class LibrarythingImporter(Importer):
def get_shelf(self, normalized_row): def get_shelf(self, normalized_row):
if normalized_row["date_finished"]: if normalized_row["date_finished"]:
return Shelf.READ_FINISHED return "read"
if normalized_row["date_started"]: if normalized_row["date_started"]:
return Shelf.READING return "reading"
return Shelf.TO_READ return "to-read"

View file

@ -56,17 +56,12 @@ class Command(BaseCommand):
self.stdout.write(" OK 🖼") self.stdout.write(" OK 🖼")
# Books # Books
book_ids = ( books = models.Book.objects.select_subclasses().filter()
models.Book.objects.select_subclasses()
.filter()
.values_list("id", flat=True)
)
self.stdout.write( self.stdout.write(
" → Book preview images ({}): ".format(len(book_ids)), ending="" " → Book preview images ({}): ".format(len(books)), ending=""
) )
for book_id in book_ids: for book in books:
preview_images.generate_edition_preview_image_task.delay(book_id) preview_images.generate_edition_preview_image_task.delay(book.id)
self.stdout.write(".", ending="") self.stdout.write(".", ending="")
self.stdout.write(" OK 🖼") self.stdout.write(" OK 🖼")

View file

@ -89,7 +89,7 @@ def init_connectors():
covers_url="https://inventaire.io", covers_url="https://inventaire.io",
search_url="https://inventaire.io/api/search?types=works&types=works&search=", search_url="https://inventaire.io/api/search?types=works&types=works&search=",
isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A", isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A",
priority=1, priority=3,
) )
models.Connector.objects.create( models.Connector.objects.create(
@ -101,7 +101,7 @@ def init_connectors():
covers_url="https://covers.openlibrary.org", covers_url="https://covers.openlibrary.org",
search_url="https://openlibrary.org/search?q=", search_url="https://openlibrary.org/search?q=",
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:", isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
priority=1, priority=3,
) )

View file

@ -1,80 +0,0 @@
# Generated by Django 3.2.12 on 2022-03-16 23:20
import bookwyrm.models.fields
from django.db import migrations
from bookwyrm.models import Shelf
def add_shelves(apps, schema_editor):
"""add any superusers to the "admin" group"""
db_alias = schema_editor.connection.alias
shelf_model = apps.get_model("bookwyrm", "Shelf")
users = apps.get_model("bookwyrm", "User")
local_users = users.objects.using(db_alias).filter(local=True)
for user in local_users:
remote_id = f"{user.remote_id}/books/stopped"
shelf_model.objects.using(db_alias).create(
name="Stopped reading",
identifier=Shelf.STOPPED_READING,
user=user,
editable=False,
remote_id=remote_id,
)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0145_sitesettings_version"),
]
operations = [
migrations.AlterField(
model_name="comment",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "To-Read"),
("reading", "Reading"),
("read", "Read"),
("stopped-reading", "Stopped-Reading"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="quotation",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "To-Read"),
("reading", "Reading"),
("read", "Read"),
("stopped-reading", "Stopped-Reading"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="review",
name="reading_status",
field=bookwyrm.models.fields.CharField(
blank=True,
choices=[
("to-read", "To-Read"),
("reading", "Reading"),
("read", "Read"),
("stopped-reading", "Stopped-Reading"),
],
max_length=255,
null=True,
),
),
migrations.RunPython(add_shelves, reverse_code=migrations.RunPython.noop),
]

View file

@ -1,13 +0,0 @@
# Generated by Django 3.2.12 on 2022-03-26 20:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0146_auto_20220316_2320"),
("bookwyrm", "0147_alter_user_preferred_language"),
]
operations = []

View file

@ -1,13 +0,0 @@
# Generated by Django 3.2.13 on 2022-05-26 17:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0148_alter_user_preferred_language"),
("bookwyrm", "0148_merge_20220326_2006"),
]
operations = []

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2022-04-08 22:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0148_alter_user_preferred_language"),
]
operations = [
migrations.RemoveField(
model_name="notification",
name="related_book",
),
]

View file

@ -0,0 +1,128 @@
# Generated by Django 3.2.12 on 2022-04-08 22:36
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0149_remove_notification_related_book"),
]
operations = [
migrations.AddField(
model_name="notification",
name="related_groups",
field=models.ManyToManyField(
related_name="notifications", to="bookwyrm.Group"
),
),
migrations.AddField(
model_name="notification",
name="related_list_items",
field=models.ManyToManyField(
related_name="notifications", to="bookwyrm.ListItem"
),
),
migrations.AddField(
model_name="notification",
name="related_reports",
field=models.ManyToManyField(to="bookwyrm.Report"),
),
migrations.AddField(
model_name="notification",
name="related_statuses",
field=models.ManyToManyField(
related_name="notifications", to="bookwyrm.Status"
),
),
migrations.AddField(
model_name="notification",
name="related_users",
field=models.ManyToManyField(
related_name="notifications", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="notification",
name="related_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications_temp",
to="bookwyrm.group",
),
),
migrations.AlterField(
model_name="notification",
name="related_list_item",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications_tmp",
to="bookwyrm.listitem",
),
),
migrations.AlterField(
model_name="notification",
name="related_report",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications_tmp",
to="bookwyrm.report",
),
),
migrations.RunSQL(
sql="""
INSERT INTO bookwyrm_notification_related_statuses (notification_id, status_id)
SELECT id, related_status_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_status_id IS NOT NULL;
INSERT INTO bookwyrm_notification_related_users (notification_id, user_id)
SELECT id, related_user_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_user_id IS NOT NULL;
INSERT INTO bookwyrm_notification_related_groups (notification_id, group_id)
SELECT id, related_group_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_group_id IS NOT NULL;
INSERT INTO bookwyrm_notification_related_list_items (notification_id, listitem_id)
SELECT id, related_list_item_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_list_item_id IS NOT NULL;
INSERT INTO bookwyrm_notification_related_reports (notification_id, report_id)
SELECT id, related_report_id
FROM bookwyrm_notification
WHERE bookwyrm_notification.related_report_id IS NOT NULL;
""",
reverse_sql=migrations.RunSQL.noop,
),
migrations.RemoveField(
model_name="notification",
name="related_group",
),
migrations.RemoveField(
model_name="notification",
name="related_list_item",
),
migrations.RemoveField(
model_name="notification",
name="related_report",
),
migrations.RemoveField(
model_name="notification",
name="related_status",
),
migrations.RemoveField(
model_name="notification",
name="related_user",
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 3.2.13 on 2022-05-26 18:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0149_merge_20220526_1716"),
]
operations = [
migrations.AddField(
model_name="readthrough",
name="stopped_date",
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -8,7 +8,6 @@ from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField from .fields import RemoteIdField
@ -36,11 +35,10 @@ class BookWyrmModel(models.Model):
remote_id = RemoteIdField(null=True, activitypub_field="id") remote_id = RemoteIdField(null=True, activitypub_field="id")
def get_remote_id(self): def get_remote_id(self):
"""generate the url that resolves to the local object, without a slug""" """generate a url that resolves to the local object"""
base_path = f"https://{DOMAIN}" base_path = f"https://{DOMAIN}"
if hasattr(self, "user"): if hasattr(self, "user"):
base_path = f"{base_path}{self.user.local_path}" base_path = f"{base_path}{self.user.local_path}"
model_name = type(self).__name__.lower() model_name = type(self).__name__.lower()
return f"{base_path}/{model_name}/{self.id}" return f"{base_path}/{model_name}/{self.id}"
@ -51,20 +49,8 @@ class BookWyrmModel(models.Model):
@property @property
def local_path(self): def local_path(self):
"""how to link to this object in the local app, with a slug""" """how to link to this object in the local app"""
local = self.get_remote_id().replace(f"https://{DOMAIN}", "") return self.get_remote_id().replace(f"https://{DOMAIN}", "")
name = None
if hasattr(self, "name_field"):
name = getattr(self, self.name_field)
elif hasattr(self, "name"):
name = self.name
if name:
slug = slugify(name)
local = f"{local}/s/{slug}"
return local
def raise_visible_to_user(self, viewer): def raise_visible_to_user(self, viewer):
"""is a user authorized to view an object?""" """is a user authorized to view an object?"""

View file

@ -176,8 +176,8 @@ class Book(BookDataModel):
"""properties of this edition, as a string""" """properties of this edition, as a string"""
items = [ items = [
self.physical_format if hasattr(self, "physical_format") else None, self.physical_format if hasattr(self, "physical_format") else None,
f"{self.languages[0]} language" self.languages[0] + " language"
if self.languages and self.languages[0] and self.languages[0] != "English" if self.languages and self.languages[0] != "English"
else None, else None,
str(self.published_date.year) if self.published_date else None, str(self.published_date.year) if self.published_date else None,
", ".join(self.publishers) if hasattr(self, "publishers") else None, ", ".join(self.publishers) if hasattr(self, "publishers") else None,

View file

@ -175,15 +175,9 @@ class ImportItem(models.Model):
def date_added(self): def date_added(self):
"""when the book was added to this dataset""" """when the book was added to this dataset"""
if self.normalized_data.get("date_added"): if self.normalized_data.get("date_added"):
parsed_date_added = dateutil.parser.parse( return timezone.make_aware(
self.normalized_data.get("date_added") dateutil.parser.parse(self.normalized_data.get("date_added"))
) )
if timezone.is_aware(parsed_date_added):
# Keep timezone if import already had one
return parsed_date_added
return timezone.make_aware(parsed_date_added)
return None return None
@property @property

View file

@ -15,40 +15,25 @@ class Notification(BookWyrmModel):
"""you've been tagged, liked, followed, etc""" """you've been tagged, liked, followed, etc"""
user = models.ForeignKey("User", on_delete=models.CASCADE) user = models.ForeignKey("User", on_delete=models.CASCADE)
related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True)
related_user = models.ForeignKey(
"User", on_delete=models.CASCADE, null=True, related_name="related_user"
)
related_group = models.ForeignKey(
"Group", on_delete=models.CASCADE, null=True, related_name="notifications"
)
related_status = models.ForeignKey("Status", on_delete=models.CASCADE, null=True)
related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
related_list_item = models.ForeignKey(
"ListItem", on_delete=models.CASCADE, null=True
)
related_report = models.ForeignKey("Report", on_delete=models.CASCADE, null=True)
read = models.BooleanField(default=False) read = models.BooleanField(default=False)
notification_type = models.CharField( notification_type = models.CharField(
max_length=255, choices=NotificationType.choices max_length=255, choices=NotificationType.choices
) )
def save(self, *args, **kwargs): related_users = models.ManyToManyField(
"""save, but don't make dupes""" "User", symmetrical=False, related_name="notifications"
# there's probably a better way to do this )
if self.__class__.objects.filter( related_groups = models.ManyToManyField(
user=self.user, "Group", symmetrical=False, related_name="notifications"
related_book=self.related_book, )
related_user=self.related_user, related_statuses = models.ManyToManyField(
related_group=self.related_group, "Status", symmetrical=False, related_name="notifications"
related_status=self.related_status, )
related_import=self.related_import, related_import = models.ForeignKey("ImportJob", on_delete=models.CASCADE, null=True)
related_list_item=self.related_list_item, related_list_items = models.ManyToManyField(
related_report=self.related_report, "ListItem", symmetrical=False, related_name="notifications"
notification_type=self.notification_type, )
).exists(): related_reports = models.ManyToManyField("Report", symmetrical=False)
return
super().save(*args, **kwargs)
class Meta: class Meta:
"""checks if notifcation is in enum list for valid types""" """checks if notifcation is in enum list for valid types"""

View file

@ -27,7 +27,6 @@ class ReadThrough(BookWyrmModel):
) )
start_date = models.DateTimeField(blank=True, null=True) start_date = models.DateTimeField(blank=True, null=True)
finish_date = models.DateTimeField(blank=True, null=True) finish_date = models.DateTimeField(blank=True, null=True)
stopped_date = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -35,7 +34,7 @@ class ReadThrough(BookWyrmModel):
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}") cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
self.user.update_active_date() self.user.update_active_date()
# an active readthrough must have an unset finish date # an active readthrough must have an unset finish date
if self.finish_date or self.stopped_date: if self.finish_date:
self.is_active = False self.is_active = False
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -6,7 +6,6 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from . import fields from . import fields
@ -18,9 +17,8 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
TO_READ = "to-read" TO_READ = "to-read"
READING = "reading" READING = "reading"
READ_FINISHED = "read" READ_FINISHED = "read"
STOPPED_READING = "stopped-reading"
READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED, STOPPED_READING) READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED)
name = fields.CharField(max_length=100) name = fields.CharField(max_length=100)
identifier = models.CharField(max_length=100) identifier = models.CharField(max_length=100)
@ -67,11 +65,6 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
identifier = self.identifier or self.get_identifier() identifier = self.identifier or self.get_identifier()
return f"{base_path}/books/{identifier}" return f"{base_path}/books/{identifier}"
@property
def local_path(self):
"""No slugs"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
def raise_not_deletable(self, viewer): def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf""" """don't let anyone delete a default shelf"""
super().raise_not_deletable(viewer) super().raise_not_deletable(viewer)

View file

@ -116,8 +116,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
"""keep notes if they are replies to existing statuses""" """keep notes if they are replies to existing statuses"""
if activity.type == "Announce": if activity.type == "Announce":
boosted = activitypub.resolve_remote_id(activity.object, get_activity=True) try:
if not boosted: boosted = activitypub.resolve_remote_id(
activity.object, get_activity=True
)
except activitypub.ActivitySerializerError:
# if we can't load the status, definitely ignore it # if we can't load the status, definitely ignore it
return True return True
# keep the boost if we would keep the status # keep the boost if we would keep the status
@ -262,7 +265,7 @@ class GeneratedNote(Status):
ReadingStatusChoices = models.TextChoices( ReadingStatusChoices = models.TextChoices(
"ReadingStatusChoices", ["to-read", "reading", "read", "stopped-reading"] "ReadingStatusChoices", ["to-read", "reading", "read"]
) )
@ -303,17 +306,10 @@ class Comment(BookStatus):
@property @property
def pure_content(self): def pure_content(self):
"""indicate the book in question for mastodon (or w/e) users""" """indicate the book in question for mastodon (or w/e) users"""
if self.progress_mode == "PG" and self.progress and (self.progress > 0): return (
return_value = ( f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">' f'"{self.book.title}"</a>)</p>'
f'"{self.book.title}"</a>, page {self.progress})</p>' )
)
else:
return_value = (
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
f'"{self.book.title}"</a>)</p>'
)
return return_value
activity_serializer = activitypub.Comment activity_serializer = activitypub.Comment
@ -339,17 +335,10 @@ class Quotation(BookStatus):
"""indicate the book in question for mastodon (or w/e) users""" """indicate the book in question for mastodon (or w/e) users"""
quote = re.sub(r"^<p>", '<p>"', self.quote) quote = re.sub(r"^<p>", '<p>"', self.quote)
quote = re.sub(r"</p>$", '"</p>', quote) quote = re.sub(r"</p>$", '"</p>', quote)
if self.position_mode == "PG" and self.position and (self.position > 0): return (
return_value = ( f'{quote} <p>-- <a href="{self.book.remote_id}">'
f'{quote} <p>-- <a href="{self.book.remote_id}">' f'"{self.book.title}"</a></p>{self.content}'
f'"{self.book.title}"</a>, page {self.position}</p>{self.content}' )
)
else:
return_value = (
f'{quote} <p>-- <a href="{self.book.remote_id}">'
f'"{self.book.title}"</a></p>{self.content}'
)
return return_value
activity_serializer = activitypub.Quotation activity_serializer = activitypub.Quotation
@ -388,7 +377,7 @@ class Review(BookStatus):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""clear rating caches""" """clear rating caches"""
if self.book.parent_work: if self.book.parent_work:
cache.delete(f"book-rating-{self.book.parent_work.id}") cache.delete(f"book-rating-{self.book.parent_work.id}-*")
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -374,10 +374,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
"name": "Read", "name": "Read",
"identifier": "read", "identifier": "read",
}, },
{
"name": "Stopped Reading",
"identifier": "stopped-reading",
},
] ]
for shelf in shelves: for shelf in shelves:

View file

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env() env = Env()
env.read_env() env.read_env()
DOMAIN = env("DOMAIN") DOMAIN = env("DOMAIN")
VERSION = "0.4.0" VERSION = "0.3.4"
RELEASE_API = env( RELEASE_API = env(
"RELEASE_API", "RELEASE_API",
@ -21,7 +21,7 @@ RELEASE_API = env(
PAGE_LENGTH = env("PAGE_LENGTH", 15) PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "e678183b" JS_CACHE = "bc93172a"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -212,7 +212,7 @@ STREAMS = [
# Search configuration # Search configuration
# total time in seconds that the instance will spend searching connectors # total time in seconds that the instance will spend searching connectors
SEARCH_TIMEOUT = int(env("SEARCH_TIMEOUT", 8)) SEARCH_TIMEOUT = int(env("SEARCH_TIMEOUT", 15))
# timeout for a query to an individual connector # timeout for a query to an individual connector
QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5)) QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5))

View file

@ -23,8 +23,3 @@
.has-background-tertiary { .has-background-tertiary {
background-color: $background-tertiary !important; background-color: $background-tertiary !important;
} }
/* Workaround for dark theme as .has-text-black doesn't give desired effect. */
.has-text-default {
color: $text !important;
}

View file

@ -53,7 +53,6 @@ $link-hover: $white-bis;
$link-hover-border: #51595d; $link-hover-border: #51595d;
$link-focus: $white-bis; $link-focus: $white-bis;
$link-active: $white-bis; $link-active: $white-bis;
$link-light: #0d1c26;
/* bulma overrides */ /* bulma overrides */
$background: $background-secondary; $background: $background-secondary;
@ -84,13 +83,6 @@ $progress-value-background-color: $border-light;
$family-primary: $family-sans-serif; $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
.has-text-muted {
color: $grey-lighter !important;
}
.has-text-more-muted {
color: $grey-light !important;
}
@import "../bookwyrm.scss"; @import "../bookwyrm.scss";
@import "../vendor/icons.css"; @import "../vendor/icons.css";

View file

@ -57,13 +57,5 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
$family-primary: $family-sans-serif; $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
.has-text-muted {
color: $grey-dark !important;
}
.has-text-more-muted {
color: $grey !important;
}
@import "../bookwyrm.scss"; @import "../bookwyrm.scss";
@import "../vendor/icons.css"; @import "../vendor/icons.css";

View file

@ -203,8 +203,6 @@ let StatusCache = new (class {
.forEach((item) => (item.disabled = false)); .forEach((item) => (item.disabled = false));
next_identifier = next_identifier == "complete" ? "read" : next_identifier; next_identifier = next_identifier == "complete" ? "read" : next_identifier;
next_identifier =
next_identifier == "stopped-reading-complete" ? "stopped-reading" : next_identifier;
// Disable the current state // Disable the current state
button.querySelector( button.querySelector(

View file

@ -50,7 +50,7 @@
</ul> </ul>
</nav> </nav>
<div class="column is-clipped"> <div class="column">
{% block about_content %}{% endblock %} {% block about_content %}{% endblock %}
</div> </div>
</div> </div>

View file

@ -24,7 +24,7 @@
</div> </div>
{% endif %} {% endif %}
<form class="block" name="edit-author" action="{% url 'edit-author' author.id %}" method="post"> <form class="block" name="edit-author" action="{{ author.local_path }}/edit" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}"> <input type="hidden" name="last_edited_by" value="{{ request.user.id }}">

View file

@ -284,7 +284,7 @@
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %} {% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
<nav class="tabs"> <nav class="tabs">
<ul> <ul>
{% url 'book' book.id book.name|slugify as tab_url %} {% url 'book' book.id as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}> <li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a> <a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
</li> </li>

View file

@ -41,18 +41,10 @@
class="block" class="block"
{% if book.id %} {% if book.id %}
name="edit-book" name="edit-book"
{% if confirm_mode %} action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
action="{% url 'edit-book-confirm' book.id %}"
{% else %}
action="{% url 'edit-book' book.id %}"
{% endif %}
{% else %} {% else %}
name="create-book" name="create-book"
{% if confirm_mode %} action="/create-book{% if confirm_mode %}/confirm{% endif %}"
action="{% url 'create-book-confirm' %}"
{% else %}
action="{% url 'create-book' %}"
{% endif %}
{% endif %} {% endif %}
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"

View file

@ -21,7 +21,7 @@
<div class="column my-3-mobile ml-3-tablet mr-auto"> <div class="column my-3-mobile ml-3-tablet mr-auto">
<h2 class="title is-5 mb-1"> <h2 class="title is-5 mb-1">
<a href="{{ book.local_path }}" class="has-text-default"> <a href="{{ book.local_path }}" class="has-text-black">
{{ book|book_title }} {{ book|book_title }}
</a> </a>
</h2> </h2>

View file

@ -15,7 +15,7 @@
<nav class="breadcrumb subtitle" aria-label="breadcrumbs"> <nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul> <ul>
<li><a href="{% url 'book' book.id book.name|slugify %}">{{ book|book_title }}</a></li> <li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
<li class="is-active"> <li class="is-active">
<a href="#" aria-current="page"> <a href="#" aria-current="page">
{% trans "Edit links" %} {% trans "Edit links" %}

View file

@ -10,7 +10,6 @@
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %} {% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %} {% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %} {% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
{% else %}{{ shelf.name }}{% endif %} {% else %}{{ shelf.name }}{% endif %}
</option> </option>
{% endfor %} {% endfor %}

View file

@ -38,7 +38,7 @@
{% for membership in group.memberships.all %} {% for membership in group.memberships.all %}
{% with member=membership.user %} {% with member=membership.user %}
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}"> <div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
<a href="{{ member.local_path }}" class="has-text-default"> <a href="{{ member.local_path }}" class="has-text-black">
{% include 'snippets/avatar.html' with user=member large=True %} {% include 'snippets/avatar.html' with user=member large=True %}
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span> <span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span> <span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>

View file

@ -9,7 +9,7 @@
<div class="column is-flex is-flex-grow-0"> <div class="column is-flex is-flex-grow-0">
{% for user in suggested_users %} {% for user in suggested_users %}
<div class="box has-text-centered is-shadowless has-background-tertiary m-2"> <div class="box has-text-centered is-shadowless has-background-tertiary m-2">
<a href="{{ user.local_path }}" class="has-text-default"> <a href="{{ user.local_path }}" class="has-text-black">
{% include 'snippets/avatar.html' with user=user large=True %} {% include 'snippets/avatar.html' with user=user large=True %}
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span> <span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span> <span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>

View file

@ -32,9 +32,6 @@
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}> <option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
OpenLibrary (CSV) OpenLibrary (CSV)
</option> </option>
<option value="Calibre" {% if current == 'Calibre' %}selected{% endif %}>
Calibre (CSV)
</option>
</select> </select>
</div> </div>

View file

@ -6,7 +6,7 @@
<nav class="breadcrumb subtitle" aria-label="breadcrumbs"> <nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul> <ul>
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li> <li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
<li><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{{ list.name|truncatechars:30 }}</a></li> <li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li>
<li class="is-active"> <li class="is-active">
<a href="#" aria-current="page"> <a href="#" aria-current="page">
{% trans "Curate" %} {% trans "Curate" %}

View file

@ -180,7 +180,7 @@
<h2 class="title is-5"> <h2 class="title is-5">
{% trans "Sort List" %} {% trans "Sort List" %}
</h2> </h2>
<form name="sort" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block"> <form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
<div class="field"> <div class="field">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label> <label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
<div class="select is-fullwidth"> <div class="select is-fullwidth">
@ -207,7 +207,7 @@
{% trans "Suggest Books" %} {% trans "Suggest Books" %}
{% endif %} {% endif %}
</h2> </h2>
<form name="search" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block"> <form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}"> <input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
@ -221,7 +221,7 @@
</div> </div>
</div> </div>
{% if query %} {% if query %}
<p class="help"><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{% trans "Clear search" %}</a></p> <p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p>
{% endif %} {% endif %}
</form> </form>
{% if not suggested_books %} {% if not suggested_books %}

View file

@ -47,12 +47,12 @@
{% block preview %} {% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}"> <div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
<div class="columns"> <div class="columns">
<div class="column is-clipped"> <div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </div>
<div class="column is-narrow has-text-muted"> <div class="column is-narrow has-grey-dark">
{{ related_status.published_date|timesince }} {{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %} {% include 'snippets/privacy-icons.html' with item=related_status %}
</div> </div>

View file

@ -47,12 +47,12 @@
{% block preview %} {% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}"> <div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
<div class="columns"> <div class="columns">
<div class="column is-clipped"> <div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </div>
<div class="column is-narrow has-text-muted"> <div class="column is-narrow has-grey-dark">
{{ related_status.published_date|timesince }} {{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %} {% include 'snippets/privacy-icons.html' with item=related_status %}
</div> </div>

View file

@ -1,7 +1,7 @@
{% load notification_page_tags %} {% load notification_page_tags %}
{% related_status notification as related_status %} {% related_status notification as related_status %}
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}"> <div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-more-muted{% endif %}"> <div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
<div class="column is-narrow is-size-3"> <div class="column is-narrow is-size-3">
<a class="icon" href="{% block primary_link %}{% endblock %}"> <a class="icon" href="{% block primary_link %}{% endblock %}">
{% block icon %}{% endblock %} {% block icon %}{% endblock %}

View file

@ -48,12 +48,12 @@
{% block preview %} {% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}"> <div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
<div class="columns"> <div class="columns">
<div class="column is-clipped"> <div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </div>
<div class="column is-narrow has-text-default"> <div class="column is-narrow has-text-black">
{{ related_status.published_date|timesince }} {{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %} {% include 'snippets/privacy-icons.html' with item=related_status %}
</div> </div>

View file

@ -51,12 +51,12 @@
{% block preview %} {% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}"> <div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
<div class="columns"> <div class="columns">
<div class="column is-clipped"> <div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </div>
<div class="column is-narrow has-text-default"> <div class="column is-narrow has-text-black">
{{ related_status.published_date|timesince }} {{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %} {% include 'snippets/privacy-icons.html' with item=related_status %}
</div> </div>

View file

@ -1,14 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}
{% blocktrans trimmed with book_title=book.title %}
Stop Reading "{{ book_title }}"
{% endblocktrans %}
{% endblock %}
{% block content %}
{% include "snippets/reading_modals/stop_reading_modal.html" with book=book active=True static=True %}
{% endblock %}

View file

@ -19,7 +19,6 @@
</label> </label>
{% include "snippets/progress_field.html" with id=field_id %} {% include "snippets/progress_field.html" with id=field_id %}
{% endif %} {% endif %}
<div class="field"> <div class="field">
<label class="label" for="id_finish_date_{{ readthrough.id }}"> <label class="label" for="id_finish_date_{{ readthrough.id }}">
{% trans "Finished reading" %} {% trans "Finished reading" %}

View file

@ -8,12 +8,10 @@
<div class="column"> <div class="column">
{% trans "Progress Updates:" %} {% trans "Progress Updates:" %}
<ul> <ul>
{% if readthrough.finish_date or readthrough.stopped_date or readthrough.progress %} {% if readthrough.finish_date or readthrough.progress %}
<li> <li>
{% if readthrough.finish_date %} {% if readthrough.finish_date %}
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %} {{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
{% elif readthrough.stopped_date %}
{{ readthrough.stopped_date | localtime | naturalday }}: {% trans "stopped" %}
{% else %} {% else %}
{% if readthrough.progress_mode == 'PG' %} {% if readthrough.progress_mode == 'PG' %}

View file

@ -36,7 +36,7 @@
{% if result_set.results %} {% if result_set.results %}
<section class="mb-5"> <section class="mb-5">
{% if not result_set.connector.local %} {% if not result_set.connector.local %}
<details class="details-panel box" open> <details class="details-panel box" {% if forloop.first %}open{% endif %}>
{% endif %} {% endif %}
{% if not result_set.connector.local %} {% if not result_set.connector.local %}
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2"> <summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">

View file

@ -86,7 +86,6 @@
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %} {% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %} {% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %} {% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
{% else %}{{ shelf.name }}{% endif %} {% else %}{{ shelf.name }}{% endif %}
<span class="subtitle"> <span class="subtitle">
{% include 'snippets/privacy-icons.html' with item=shelf %} {% include 'snippets/privacy-icons.html' with item=shelf %}
@ -151,7 +150,7 @@
{% if is_self %} {% if is_self %}
<th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th> <th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th>
<th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th> <th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th>
<th>{% if shelf.identifier == 'read' %}{% trans "Finished" as text %}{% else %}{% trans "Until" as text %}{% endif %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th> <th>{% trans "Finished" as text %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th>
{% endif %} {% endif %}
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th> <th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
{% endif %} {% endif %}
@ -181,7 +180,7 @@
<td data-title="{% trans "Started" %}"> <td data-title="{% trans "Started" %}">
{{ book.start_date|naturalday|default_if_none:""}} {{ book.start_date|naturalday|default_if_none:""}}
</td> </td>
<td data-title="{% if shelf.identifier == 'read' %}{% trans "Finished" as text %}{% else %}{% trans "Until" as text %}{% endif %}"> <td data-title="{% trans "Finished" %}">
{{ book.finish_date|naturalday|default_if_none:""}} {{ book.finish_date|naturalday|default_if_none:""}}
</td> </td>
{% endif %} {% endif %}

View file

@ -1,42 +0,0 @@
{% extends 'snippets/reading_modals/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block modal-title %}
{% blocktrans trimmed with book_title=book|book_title %}
Stop Reading "<em>{{ book_title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="stop-reading-{{ uuid }}" action="{% url 'reading-status' 'stop' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="reading_status" value="stopped-reading">
<input type="hidden" name="shelf" value="{{ move_from }}">
{% endblock %}
{% block reading-dates %}
<div class="columns">
<div class="column is-half">
<div class="field">
<label class="label" for="stop_id_start_date_{{ uuid }}">
{% trans "Started reading" %}
</label>
<input type="date" name="start_date" class="input" id="stop_id_start_date_{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label" for="id_read_until_date_{{ uuid }}">
{% trans "Stopped reading" %}
</label>
<input type="date" name="stopped_date" class="input" id="id_read_until_date_{{ uuid }}" value="{% now "Y-m-d" %}">
</div>
</div>
</div>
{% endblock %}
{% block form %}
{% include "snippets/reading_modals/form.html" with optional=True type="stop_modal" %}
{% endblock %}

View file

@ -49,13 +49,6 @@
{% join "finish_reading" uuid as modal_id %} {% join "finish_reading" uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %} {% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
{% elif shelf.identifier == 'stopped-reading' %}
{% trans "Stopped reading" as button_text %}
{% url 'reading-status' 'stop' book.id as fallback_url %}
{% join "stop_reading" uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %} {% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %} {% trans "Want to read" as button_text %}
@ -106,8 +99,5 @@
{% join "finish_reading" uuid as modal_id %} {% join "finish_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %} {% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
{% join "stop_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/stop_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View file

@ -29,9 +29,6 @@
{% join "finish_reading" uuid as modal_id %} {% join "finish_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %} {% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
{% join "stop_reading" uuid as modal_id %}
{% include 'snippets/reading_modals/stop_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
{% join "progress_update" uuid as modal_id %} {% join "progress_update" uuid as modal_id %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %} {% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}

View file

@ -8,7 +8,7 @@
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %} {% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
<li role="menuitem" class="dropdown-item p-0"> <li role="menuitem" class="dropdown-item p-0">
<div <div
class="{% if is_current or next_shelf_identifier == shelf.identifier %}is-hidden{% elif shelf.identifier == 'stopped-reading' and active_shelf.shelf.identifier != "reading" %}is-hidden{% endif %}" class="{% if next_shelf_identifier == shelf.identifier %}is-hidden{% endif %}"
data-shelf-dropdown-identifier="{{ shelf.identifier }}" data-shelf-dropdown-identifier="{{ shelf.identifier }}"
data-shelf-next="{{ shelf.identifier|next_shelf }}" data-shelf-next="{{ shelf.identifier|next_shelf }}"
> >
@ -26,13 +26,6 @@
{% join "finish_reading" button_uuid as modal_id %} {% join "finish_reading" button_uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %} {% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
{% elif shelf.identifier == 'stopped-reading' %}
{% trans "Stop reading" as button_text %}
{% url 'reading-status' 'stop' book.id as fallback_url %}
{% join "stop_reading" button_uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %} {% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %} {% trans "Want to read" as button_text %}

View file

@ -13,15 +13,6 @@
</button> </button>
</div> </div>
<div
class="{% if next_shelf_identifier != 'stopped-reading-complete' %}is-hidden{% endif %}"
data-shelf-identifier="stopped-reading-complete"
>
<button type="button" class="button {{ class }}" disabled>
<span>{% trans "Stopped reading" %}</span>
</button>
</div>
{% for shelf in shelves %} {% for shelf in shelves %}
<div <div
class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}" class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
@ -42,14 +33,6 @@
{% join "finish_reading" button_uuid as modal_id %} {% join "finish_reading" button_uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %} {% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
{% elif shelf.identifier == 'stopped-reading' %}
{% trans "Stop reading" as button_text %}
{% url 'reading-status' 'finish' book.id as fallback_url %}
{% join "stop_reading" button_uuid as modal_id %}
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %} {% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %} {% trans "Want to read" as button_text %}

View file

@ -112,9 +112,6 @@
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %} {% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
{% include 'snippets/trimmed_text.html' %} {% include 'snippets/trimmed_text.html' %}
{% endwith %} {% endwith %}
{% if status.progress %}
<div class="is-small is-italic has-text-right mr-3">{% trans "page" %} {{ status.progress }}</div>
{% endif %}
{% endif %} {% endif %}
{% if status.attachments.exists %} {% if status.attachments.exists %}

View file

@ -1,23 +0,0 @@
{% spaceless %}
{% load i18n %}
{% load utilities %}
{% load status_display %}
{% load_book status as book %}
{% if book.authors.exists %}
{% with author=book.authors.first %}
{% blocktrans trimmed with book_path=book.local_path book=book|book_title author_name=author.name author_path=author.local_path %}
stopped reading <a href="{{ book_path }}">{{ book }}</a> by <a href="{{ author_path }}">{{ author_name }}</a>
{% endblocktrans %}
{% endwith %}
{% else %}
{% blocktrans trimmed with book_path=book.local_path book=book|book_title %}
stopped reading <a href="{{ book_path }}">{{ book }}</a>
{% endblocktrans %}
{% endif %}
{% endspaceless %}

View file

@ -5,7 +5,7 @@
{% for user in suggested_users %} {% for user in suggested_users %}
<div class="column is-flex is-flex-grow-0"> <div class="column is-flex is-flex-grow-0">
<div class="box has-text-centered is-shadowless has-background-tertiary m-0"> <div class="box has-text-centered is-shadowless has-background-tertiary m-0">
<a href="{{ user.local_path }}" class="has-text-default"> <a href="{{ user.local_path }}" class="has-text-black">
{% include 'snippets/avatar.html' with user=user large=True %} {% include 'snippets/avatar.html' with user=user large=True %}
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span> <span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span> <span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>

View file

@ -33,9 +33,8 @@
{% if shelf.name == 'To Read' %}{% trans "To Read" %} {% if shelf.name == 'To Read' %}{% trans "To Read" %}
{% elif shelf.name == 'Currently Reading' %}{% trans "Currently Reading" %} {% elif shelf.name == 'Currently Reading' %}{% trans "Currently Reading" %}
{% elif shelf.name == 'Read' %}{% trans "Read" %} {% elif shelf.name == 'Read' %}{% trans "Read" %}
{% elif shelf.name == 'Stopped Reading' %}{% trans "Stopped Reading" %}
{% else %}{{ shelf.name }}{% endif %} {% else %}{{ shelf.name }}{% endif %}
{% if shelf.size > 4 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %} {% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}
</h3> </h3>
<div class="is-mobile field is-grouped"> <div class="is-mobile field is-grouped">
{% for book in shelf.books %} {% for book in shelf.books %}

View file

@ -21,7 +21,7 @@
role="menu" role="menu"
> >
<li role="menuitem"> <li role="menuitem">
<a href="{% url 'user-feed' request.user.localname %}" class="navbar-item"> <a href="{% url 'user-feed' user|username %}" class="navbar-item">
{% trans "Profile" %} {% trans "Profile" %}
</a> </a>
</li> </li>

View file

@ -13,10 +13,10 @@ register = template.Library()
def get_rating(book, user): def get_rating(book, user):
"""get the overall rating of a book""" """get the overall rating of a book"""
return cache.get_or_set( return cache.get_or_set(
f"book-rating-{book.parent_work.id}", f"book-rating-{book.parent_work.id}-{user.id}",
lambda u, b: models.Review.objects.filter( lambda u, b: models.Review.privacy_filter(u)
book__parent_work__editions=b, rating__gt=0 .filter(book__parent_work__editions=b, rating__gt=0)
).aggregate(Avg("rating"))["rating__avg"] .aggregate(Avg("rating"))["rating__avg"]
or 0, or 0,
user, user,
book, book,

View file

@ -30,8 +30,6 @@ def get_next_shelf(current_shelf):
return "read" return "read"
if current_shelf == "read": if current_shelf == "read":
return "complete" return "complete"
if current_shelf == "stopped-reading":
return "stopped-reading-complete"
return "to-read" return "to-read"

View file

@ -1,10 +1,6 @@
""" testing activitystreams """ """ testing activitystreams """
from datetime import datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@ -66,39 +62,6 @@ class ActivitystreamsSignals(TestCase):
self.assertEqual(args["args"][0], status.id) self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "high_priority") self.assertEqual(args["queue"], "high_priority")
def test_add_status_on_create_created_low_priority(self, *_):
"""a new statuses has entered"""
# created later than publication
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="public",
created_date=datetime(2022, 5, 16, tzinfo=timezone.utc),
published_date=datetime(2022, 5, 14, tzinfo=timezone.utc),
)
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
activitystreams.add_status_on_create_command(models.Status, status, False)
self.assertEqual(mock.call_count, 1)
args = mock.call_args[1]
self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "low_priority")
# published later than yesterday
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="public",
published_date=timezone.now() - timedelta(days=1),
)
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
activitystreams.add_status_on_create_command(models.Status, status, False)
self.assertEqual(mock.call_count, 1)
args = mock.call_args[1]
self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "low_priority")
def test_populate_streams_on_account_create_command(self, *_): def test_populate_streams_on_account_create_command(self, *_):
"""create streams for a user""" """create streams for a user"""
with patch("bookwyrm.activitystreams.populate_stream_task.delay") as mock: with patch("bookwyrm.activitystreams.populate_stream_task.delay") as mock:

View file

@ -42,9 +42,15 @@ class AbstractConnector(TestCase):
generated_remote_link_field = "openlibrary_link" generated_remote_link_field = "openlibrary_link"
def parse_search_data(self, data, min_confidence): def format_search_result(self, search_result):
return search_result
def parse_search_data(self, data):
return data return data
def format_isbn_search_result(self, search_result):
return search_result
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
return data return data
@ -95,7 +101,6 @@ class AbstractConnector(TestCase):
result = self.connector.get_or_create_book( result = self.connector.get_or_create_book(
f"https://{DOMAIN}/book/{self.book.id}" f"https://{DOMAIN}/book/{self.book.id}"
) )
self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book) self.assertEqual(result, self.book)

View file

@ -1,5 +1,6 @@
""" testing book data connectors """ """ testing book data connectors """
from django.test import TestCase from django.test import TestCase
import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.connectors import abstract_connector from bookwyrm.connectors import abstract_connector
@ -24,12 +25,18 @@ class AbstractConnector(TestCase):
class TestConnector(abstract_connector.AbstractMinimalConnector): class TestConnector(abstract_connector.AbstractMinimalConnector):
"""nothing added here""" """nothing added here"""
def format_search_result(self, search_result):
return search_result
def get_or_create_book(self, remote_id): def get_or_create_book(self, remote_id):
pass pass
def parse_search_data(self, data, min_confidence): def parse_search_data(self, data):
return data return data
def format_isbn_search_result(self, search_result):
return search_result
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
return data return data
@ -47,6 +54,45 @@ class AbstractConnector(TestCase):
self.assertIsNone(connector.name) self.assertIsNone(connector.name)
self.assertEqual(connector.identifier, "example.com") self.assertEqual(connector.identifier, "example.com")
@responses.activate
def test_search(self):
"""makes an http request to the outside service"""
responses.add(
responses.GET,
"https://example.com/search?q=a%20book%20title",
json=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
status=200,
)
results = self.test_connector.search("a book title")
self.assertEqual(len(results), 10)
self.assertEqual(results[0], "a")
self.assertEqual(results[1], "b")
self.assertEqual(results[2], "c")
@responses.activate
def test_search_min_confidence(self):
"""makes an http request to the outside service"""
responses.add(
responses.GET,
"https://example.com/search?q=a%20book%20title&min_confidence=1",
json=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
status=200,
)
results = self.test_connector.search("a book title", min_confidence=1)
self.assertEqual(len(results), 10)
@responses.activate
def test_isbn_search(self):
"""makes an http request to the outside service"""
responses.add(
responses.GET,
"https://example.com/isbn?q=123456",
json=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
status=200,
)
results = self.test_connector.isbn_search("123456")
self.assertEqual(len(results), 10)
def test_create_mapping(self): def test_create_mapping(self):
"""maps remote fields for book data to bookwyrm activitypub fields""" """maps remote fields for book data to bookwyrm activitypub fields"""
mapping = Mapping("isbn") mapping = Mapping("isbn")

View file

@ -30,11 +30,14 @@ class BookWyrmConnector(TestCase):
result = self.connector.get_or_create_book(book.remote_id) result = self.connector.get_or_create_book(book.remote_id)
self.assertEqual(book, result) self.assertEqual(book, result)
def test_parse_search_data(self): def test_format_search_result(self):
"""create a SearchResult object from search response json""" """create a SearchResult object from search response json"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json")
search_data = json.loads(datafile.read_bytes()) search_data = json.loads(datafile.read_bytes())
result = list(self.connector.parse_search_data(search_data, 0))[0] results = self.connector.parse_search_data(search_data)
self.assertIsInstance(results, list)
result = self.connector.format_search_result(results[0])
self.assertIsInstance(result, SearchResult) self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, "Jonathan Strange and Mr Norrell") self.assertEqual(result.title, "Jonathan Strange and Mr Norrell")
self.assertEqual(result.key, "https://example.com/book/122") self.assertEqual(result.key, "https://example.com/book/122")
@ -42,9 +45,10 @@ class BookWyrmConnector(TestCase):
self.assertEqual(result.year, 2017) self.assertEqual(result.year, 2017)
self.assertEqual(result.connector, self.connector) self.assertEqual(result.connector, self.connector)
def test_parse_isbn_search_data(self): def test_format_isbn_search_result(self):
"""just gotta attach the connector""" """just gotta attach the connector"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json")
search_data = json.loads(datafile.read_bytes()) search_data = json.loads(datafile.read_bytes())
result = list(self.connector.parse_isbn_search_data(search_data))[0] results = self.connector.parse_isbn_search_data(search_data)
result = self.connector.format_isbn_search_result(results[0])
self.assertEqual(result.connector, self.connector) self.assertEqual(result.connector, self.connector)

View file

@ -49,11 +49,39 @@ class ConnectorManager(TestCase):
self.assertEqual(len(connectors), 1) self.assertEqual(len(connectors), 1)
self.assertIsInstance(connectors[0], BookWyrmConnector) self.assertIsInstance(connectors[0], BookWyrmConnector)
@responses.activate
def test_search_plaintext(self):
"""search all connectors"""
responses.add(
responses.GET,
"http://fake.ciom/search/Example?min_confidence=0.1",
json=[{"title": "Hello", "key": "https://www.example.com/search/1"}],
)
results = connector_manager.search("Example")
self.assertEqual(len(results), 1)
self.assertEqual(len(results[0]["results"]), 1)
self.assertEqual(results[0]["connector"].identifier, "test_connector_remote")
self.assertEqual(results[0]["results"][0].title, "Hello")
def test_search_empty_query(self): def test_search_empty_query(self):
"""don't panic on empty queries""" """don't panic on empty queries"""
results = connector_manager.search("") results = connector_manager.search("")
self.assertEqual(results, []) self.assertEqual(results, [])
@responses.activate
def test_search_isbn(self):
"""special handling if a query resembles an isbn"""
responses.add(
responses.GET,
"http://fake.ciom/isbn/0000000000",
json=[{"title": "Hello", "key": "https://www.example.com/search/1"}],
)
results = connector_manager.search("0000000000")
self.assertEqual(len(results), 1)
self.assertEqual(len(results[0]["results"]), 1)
self.assertEqual(results[0]["connector"].identifier, "test_connector_remote")
self.assertEqual(results[0]["results"][0].title, "Hello")
def test_first_search_result(self): def test_first_search_result(self):
"""only get one search result""" """only get one search result"""
result = connector_manager.first_search_result("Example") result = connector_manager.first_search_result("Example")

View file

@ -66,14 +66,38 @@ class Inventaire(TestCase):
with self.assertRaises(ConnectorException): with self.assertRaises(ConnectorException):
self.connector.get_book_data("https://test.url/ok") self.connector.get_book_data("https://test.url/ok")
def test_parse_search_data(self): @responses.activate
def test_search(self):
"""min confidence filtering"""
responses.add(
responses.GET,
"https://inventaire.io/search?q=hi",
json={
"results": [
{
"_score": 200,
"label": "hello",
},
{
"_score": 100,
"label": "hi",
},
],
},
)
results = self.connector.search("hi", min_confidence=0.5)
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, "hello")
def test_format_search_result(self):
"""json to search result objs""" """json to search result objs"""
search_file = pathlib.Path(__file__).parent.joinpath( search_file = pathlib.Path(__file__).parent.joinpath(
"../data/inventaire_search.json" "../data/inventaire_search.json"
) )
search_results = json.loads(search_file.read_bytes()) search_results = json.loads(search_file.read_bytes())
formatted = list(self.connector.parse_search_data(search_results, 0))[0] results = self.connector.parse_search_data(search_results)
formatted = self.connector.format_search_result(results[0])
self.assertEqual(formatted.title, "The Stories of Vladimir Nabokov") self.assertEqual(formatted.title, "The Stories of Vladimir Nabokov")
self.assertEqual( self.assertEqual(
@ -154,14 +178,15 @@ class Inventaire(TestCase):
result = self.connector.resolve_keys(keys) result = self.connector.resolve_keys(keys)
self.assertEqual(result, ["epistolary novel", "crime novel"]) self.assertEqual(result, ["epistolary novel", "crime novel"])
def test_pase_isbn_search_data(self): def test_isbn_search(self):
"""another search type""" """another search type"""
search_file = pathlib.Path(__file__).parent.joinpath( search_file = pathlib.Path(__file__).parent.joinpath(
"../data/inventaire_isbn_search.json" "../data/inventaire_isbn_search.json"
) )
search_results = json.loads(search_file.read_bytes()) search_results = json.loads(search_file.read_bytes())
formatted = list(self.connector.parse_isbn_search_data(search_results))[0] results = self.connector.parse_isbn_search_data(search_results)
formatted = self.connector.format_isbn_search_result(results[0])
self.assertEqual(formatted.title, "L'homme aux cercles bleus") self.assertEqual(formatted.title, "L'homme aux cercles bleus")
self.assertEqual( self.assertEqual(
@ -173,12 +198,25 @@ class Inventaire(TestCase):
"https://covers.inventaire.io/img/entities/12345", "https://covers.inventaire.io/img/entities/12345",
) )
def test_parse_isbn_search_data_empty(self): def test_isbn_search_empty(self):
"""another search type""" """another search type"""
search_results = {} search_results = {}
results = list(self.connector.parse_isbn_search_data(search_results)) results = self.connector.parse_isbn_search_data(search_results)
self.assertEqual(results, []) self.assertEqual(results, [])
def test_isbn_search_no_title(self):
"""another search type"""
search_file = pathlib.Path(__file__).parent.joinpath(
"../data/inventaire_isbn_search.json"
)
search_results = json.loads(search_file.read_bytes())
search_results["entities"]["isbn:9782290349229"]["claims"]["wdt:P1476"] = None
result = self.connector.format_isbn_search_result(
search_results.get("entities")
)
self.assertIsNone(result)
def test_is_work_data(self): def test_is_work_data(self):
"""is it a work""" """is it a work"""
work_file = pathlib.Path(__file__).parent.joinpath( work_file = pathlib.Path(__file__).parent.joinpath(

View file

@ -122,11 +122,21 @@ class Openlibrary(TestCase):
self.assertEqual(result, "https://covers.openlibrary.org/b/id/image-L.jpg") self.assertEqual(result, "https://covers.openlibrary.org/b/id/image-L.jpg")
def test_parse_search_result(self): def test_parse_search_result(self):
"""extract the results from the search json response"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json")
search_data = json.loads(datafile.read_bytes())
result = self.connector.parse_search_data(search_data)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2)
def test_format_search_result(self):
"""translate json from openlibrary into SearchResult""" """translate json from openlibrary into SearchResult"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json")
search_data = json.loads(datafile.read_bytes()) search_data = json.loads(datafile.read_bytes())
result = list(self.connector.parse_search_data(search_data, 0))[0] results = self.connector.parse_search_data(search_data)
self.assertIsInstance(results, list)
result = self.connector.format_search_result(results[0])
self.assertIsInstance(result, SearchResult) self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, "This Is How You Lose the Time War") self.assertEqual(result.title, "This Is How You Lose the Time War")
self.assertEqual(result.key, "https://openlibrary.org/works/OL20639540W") self.assertEqual(result.key, "https://openlibrary.org/works/OL20639540W")
@ -138,10 +148,18 @@ class Openlibrary(TestCase):
"""extract the results from the search json response""" """extract the results from the search json response"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json")
search_data = json.loads(datafile.read_bytes()) search_data = json.loads(datafile.read_bytes())
result = list(self.connector.parse_isbn_search_data(search_data)) result = self.connector.parse_isbn_search_data(search_data)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
result = result[0] def test_format_isbn_search_result(self):
"""translate json from openlibrary into SearchResult"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json")
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_isbn_search_data(search_data)
self.assertIsInstance(results, list)
result = self.connector.format_isbn_search_result(results[0])
self.assertIsInstance(result, SearchResult) self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, "Les ombres errantes") self.assertEqual(result.title, "Les ombres errantes")
self.assertEqual(result.key, "https://openlibrary.org/books/OL16262504M") self.assertEqual(result.key, "https://openlibrary.org/books/OL16262504M")
@ -211,7 +229,7 @@ class Openlibrary(TestCase):
status=200, status=200,
) )
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector.get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
) as mock: ) as mock:
mock.return_value = [] mock.return_value = []
result = self.connector.create_edition_from_data(work, self.edition_data) result = self.connector.create_edition_from_data(work, self.edition_data)

View file

@ -1,2 +0,0 @@
authors,author_sort,rating,library_name,timestamp,formats,size,isbn,identifiers,comments,tags,series,series_index,languages,title,cover,title_sort,publisher,pubdate,id,uuid
"Seanan McGuire","McGuire, Seanan","5","Bücher","2021-01-19T22:41:16+01:00","epub, original_epub","1433809","9780756411800","goodreads:39077187,isbn:9780756411800","REPLACED COMMENTS (BOOK DESCRIPTION) BECAUSE IT IS REALLY LONG.","Cryptids, Fantasy, Romance, Magic","InCryptid","8.0","eng","That Ain't Witchcraft","/home/tastytea/Bücher/Seanan McGuire/That Ain't Witchcraft (864)/cover.jpg","That Ain't Witchcraft","Daw Books","2019-03-05T01:00:00+01:00","864","3051ed45-8943-4900-a22a-d2704e3583df"
1 authors author_sort rating library_name timestamp formats size isbn identifiers comments tags series series_index languages title cover title_sort publisher pubdate id uuid
2 Seanan McGuire McGuire, Seanan 5 Bücher 2021-01-19T22:41:16+01:00 epub, original_epub 1433809 9780756411800 goodreads:39077187,isbn:9780756411800 REPLACED COMMENTS (BOOK DESCRIPTION) BECAUSE IT IS REALLY LONG. Cryptids, Fantasy, Romance, Magic InCryptid 8.0 eng That Ain't Witchcraft /home/tastytea/Bücher/Seanan McGuire/That Ain't Witchcraft (864)/cover.jpg That Ain't Witchcraft Daw Books 2019-03-05T01:00:00+01:00 864 3051ed45-8943-4900-a22a-d2704e3583df

View file

@ -1,71 +0,0 @@
""" testing import """
import pathlib
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models
from bookwyrm.importers import CalibreImporter
from bookwyrm.importers.importer import handle_imported_book
# pylint: disable=consider-using-with
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
class CalibreImport(TestCase):
"""importing from Calibre csv"""
def setUp(self):
"""use a test csv"""
self.importer = CalibreImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/calibre.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
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=work,
)
def test_create_job(self, *_):
"""creates the import job entry and checks csv"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_items = (
models.ImportItem.objects.filter(job=import_job).order_by("index").all()
)
self.assertEqual(len(import_items), 1)
self.assertEqual(import_items[0].index, 0)
self.assertEqual(
import_items[0].normalized_data["title"], "That Ain't Witchcraft"
)
def test_handle_imported_book(self, *_):
"""calibre import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter(
identifier=models.Shelf.TO_READ
).first()
self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)

View file

@ -84,9 +84,7 @@ class GoodreadsImport(TestCase):
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""goodreads import added a book, this adds related connections""" """goodreads import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="read").first()
identifier=models.Shelf.READ_FINISHED
).first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job( import_job = self.importer.create_job(

View file

@ -174,9 +174,7 @@ class GenericImporter(TestCase):
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""import added a book, this adds related connections""" """import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="read").first()
identifier=models.Shelf.READ_FINISHED
).first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job( import_job = self.importer.create_job(
@ -195,9 +193,7 @@ class GenericImporter(TestCase):
def test_handle_imported_book_already_shelved(self, *_): def test_handle_imported_book_already_shelved(self, *_):
"""import added a book, this adds related connections""" """import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
identifier=models.Shelf.TO_READ
).first()
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
shelf=shelf, shelf=shelf,
user=self.local_user, user=self.local_user,
@ -221,16 +217,12 @@ class GenericImporter(TestCase):
shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2) shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2)
) )
self.assertIsNone( self.assertIsNone(
self.local_user.shelf_set.get( self.local_user.shelf_set.get(identifier="read").books.first()
identifier=models.Shelf.READ_FINISHED
).books.first()
) )
def test_handle_import_twice(self, *_): def test_handle_import_twice(self, *_):
"""re-importing books""" """re-importing books"""
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="read").first()
identifier=models.Shelf.READ_FINISHED
).first()
import_job = self.importer.create_job( import_job = self.importer.create_job(
self.local_user, self.csv, False, "public" self.local_user, self.csv, False, "public"
) )

View file

@ -93,9 +93,7 @@ class LibrarythingImport(TestCase):
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""librarything import added a book, this adds related connections""" """librarything import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="read").first()
identifier=models.Shelf.READ_FINISHED
).first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job( import_job = self.importer.create_job(
@ -119,9 +117,7 @@ class LibrarythingImport(TestCase):
def test_handle_imported_book_already_shelved(self, *_): def test_handle_imported_book_already_shelved(self, *_):
"""librarything import added a book, this adds related connections""" """librarything import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
identifier=models.Shelf.TO_READ
).first()
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
shelf=shelf, user=self.local_user, book=self.book shelf=shelf, user=self.local_user, book=self.book
) )
@ -139,9 +135,7 @@ class LibrarythingImport(TestCase):
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
self.assertIsNone( self.assertIsNone(
self.local_user.shelf_set.get( self.local_user.shelf_set.get(identifier="read").books.first()
identifier=models.Shelf.READ_FINISHED
).books.first()
) )
readthrough = models.ReadThrough.objects.get(user=self.local_user) readthrough = models.ReadThrough.objects.get(user=self.local_user)

View file

@ -70,9 +70,7 @@ class OpenLibraryImport(TestCase):
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""openlibrary import added a book, this adds related connections""" """openlibrary import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="reading").first()
identifier=models.Shelf.READING
).first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job( import_job = self.importer.create_job(

View file

@ -62,9 +62,7 @@ class StorygraphImport(TestCase):
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""storygraph import added a book, this adds related connections""" """storygraph import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter( shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
identifier=models.Shelf.TO_READ
).first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job( import_job = self.importer.create_job(

View file

@ -195,7 +195,7 @@ class ImportJob(TestCase):
) as search: ) as search:
search.return_value = result search.return_value = result
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector.get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
): ):
book = item.get_book_from_identifier() book = item.get_book_from_identifier()

View file

@ -462,8 +462,6 @@ class Status(TestCase):
@responses.activate @responses.activate
def test_ignore_activity_boost(self, *_): def test_ignore_activity_boost(self, *_):
"""don't bother with most remote statuses""" """don't bother with most remote statuses"""
responses.add(responses.GET, "http://fish.com/nothing")
activity = activitypub.Announce( activity = activitypub.Announce(
id="http://www.faraway.com/boost/12", id="http://www.faraway.com/boost/12",
actor=self.remote_user.remote_id, actor=self.remote_user.remote_id,

View file

@ -53,17 +53,15 @@ class User(TestCase):
def test_user_shelves(self): def test_user_shelves(self):
shelves = models.Shelf.objects.filter(user=self.user).all() shelves = models.Shelf.objects.filter(user=self.user).all()
self.assertEqual(len(shelves), 4) self.assertEqual(len(shelves), 3)
names = [s.name for s in shelves] names = [s.name for s in shelves]
self.assertTrue("To Read" in names) self.assertTrue("To Read" in names)
self.assertTrue("Currently Reading" in names) self.assertTrue("Currently Reading" in names)
self.assertTrue("Read" in names) self.assertTrue("Read" in names)
self.assertTrue("Stopped Reading" in names)
ids = [s.identifier for s in shelves] ids = [s.identifier for s in shelves]
self.assertTrue("to-read" in ids) self.assertTrue("to-read" in ids)
self.assertTrue("reading" in ids) self.assertTrue("reading" in ids)
self.assertTrue("read" in ids) self.assertTrue("read" in ids)
self.assertTrue("stopped-reading" in ids)
def test_activitypub_serialize(self): def test_activitypub_serialize(self):
activity = self.user.to_activity() activity = self.user.to_activity()

View file

@ -40,8 +40,7 @@ class RatingTags(TestCase):
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_get_rating(self, *_): def test_get_rating(self, *_):
"""privacy filtered rating. Commented versions are how it ought to work with """privacy filtered rating"""
subjective ratings, which are currenly not used for performance reasons."""
# follows-only: not included # follows-only: not included
models.ReviewRating.objects.create( models.ReviewRating.objects.create(
user=self.remote_user, user=self.remote_user,
@ -49,8 +48,7 @@ class RatingTags(TestCase):
book=self.book, book=self.book,
privacy="followers", privacy="followers",
) )
# self.assertEqual(rating_tags.get_rating(self.book, self.local_user), 0) self.assertEqual(rating_tags.get_rating(self.book, self.local_user), 0)
self.assertEqual(rating_tags.get_rating(self.book, self.local_user), 5)
# public: included # public: included
models.ReviewRating.objects.create( models.ReviewRating.objects.create(

View file

@ -102,12 +102,18 @@ class BookSearch(TestCase):
class TestConnector(AbstractMinimalConnector): class TestConnector(AbstractMinimalConnector):
"""nothing added here""" """nothing added here"""
def format_search_result(self, search_result):
return search_result
def get_or_create_book(self, remote_id): def get_or_create_book(self, remote_id):
pass pass
def parse_search_data(self, data, min_confidence): def parse_search_data(self, data):
return data return data
def format_isbn_search_result(self, search_result):
return search_result
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
return data return data

View file

@ -9,7 +9,6 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
from bookwyrm.views.books.edit_book import add_authors
from bookwyrm.tests.validate_html import validate_html from bookwyrm.tests.validate_html import validate_html
from bookwyrm.tests.views.books.test_book import _setup_cover_url from bookwyrm.tests.views.books.test_book import _setup_cover_url
@ -215,22 +214,3 @@ class EditBookViews(TestCase):
self.book.refresh_from_db() self.book.refresh_from_db()
self.assertTrue(self.book.cover) self.assertTrue(self.book.cover)
def test_add_authors_helper(self):
"""converts form input into author matches"""
form = forms.EditionForm(instance=self.book)
form.data["title"] = "New Title"
form.data["last_edited_by"] = self.local_user.id
form.data["add_author"] = ["Sappho", "Some Guy"]
request = self.factory.post("", form.data)
request.user = self.local_user
with patch("bookwyrm.utils.isni.find_authors_by_name") as mock:
mock.return_value = []
result = add_authors(request, form.data)
self.assertTrue(result["confirm_mode"])
self.assertEqual(result["add_author"], ["Sappho", "Some Guy"])
self.assertEqual(len(result["author_matches"]), 2)
self.assertEqual(result["author_matches"][0]["name"], "Sappho")
self.assertEqual(result["author_matches"][1]["name"], "Some Guy")

View file

@ -208,44 +208,16 @@ class InboxCreate(TestCase):
self.assertEqual(book_list.description, "summary text") self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22") self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_create_unsupported_type_question(self, *_): def test_create_unsupported_type(self, *_):
"""ignore activities we know we can't handle""" """ignore activities we know we can't handle"""
activity = self.create_json activity = self.create_json
activity["object"] = { activity["object"] = {
"id": "https://example.com/status/887", "id": "https://example.com/status/887",
"type": "Question", "type": "Question",
} }
# just observe how it doesn't throw an error # just observer how it doesn't throw an error
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
def test_create_unsupported_type_article(self, *_):
"""special case in unsupported type because we do know what it is"""
activity = self.create_json
activity["object"] = {
"id": "https://example.com/status/887",
"type": "Article",
"name": "hello",
"published": "2021-04-29T21:27:30.014235+00:00",
"attributedTo": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"sensitive": False,
"@context": "https://www.w3.org/ns/activitystreams",
}
# just observe how it doesn't throw an error
views.inbox.activity_task(activity)
def test_create_unsupported_type_unknown(self, *_):
"""Something truly unexpected should throw an error"""
activity = self.create_json
activity["object"] = {
"id": "https://example.com/status/887",
"type": "Blaaaah",
}
# error this time
with self.assertRaises(ActivitySerializerError):
views.inbox.activity_task(activity)
def test_create_unknown_type(self, *_): def test_create_unknown_type(self, *_):
"""ignore activities we know we've never heard of""" """ignore activities we know we've never heard of"""
activity = self.create_json activity = self.create_json

View file

@ -1,5 +1,6 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
import pathlib
from unittest.mock import patch from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
@ -7,9 +8,9 @@ from django.http import JsonResponse
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
import responses
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.book_search import SearchResult
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from bookwyrm.tests.validate_html import validate_html from bookwyrm.tests.validate_html import validate_html
@ -64,11 +65,12 @@ class Views(TestCase):
self.assertIsInstance(response, TemplateResponse) self.assertIsInstance(response, TemplateResponse)
validate_html(response.render()) validate_html(response.render())
@responses.activate
def test_search_books(self): def test_search_books(self):
"""searches remote connectors""" """searches remote connectors"""
view = views.Search.as_view() view = views.Search.as_view()
connector = models.Connector.objects.create( models.Connector.objects.create(
identifier="example.com", identifier="example.com",
connector_file="openlibrary", connector_file="openlibrary",
base_url="https://example.com", base_url="https://example.com",
@ -76,24 +78,26 @@ class Views(TestCase):
covers_url="https://example.com/covers", covers_url="https://example.com/covers",
search_url="https://example.com/search?q=", search_url="https://example.com/search?q=",
) )
mock_result = SearchResult(title="Mock Book", connector=connector, key="hello") datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json")
search_data = json.loads(datafile.read_bytes())
responses.add(
responses.GET, "https://example.com/search?q=Test%20Book", json=search_data
)
request = self.factory.get("", {"q": "Test Book", "remote": True}) request = self.factory.get("", {"q": "Test Book", "remote": True})
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.views.search.is_api_request") as is_api: with patch("bookwyrm.views.search.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
with patch("bookwyrm.connectors.connector_manager.search") as remote_search: response = view(request)
remote_search.return_value = [
{"results": [mock_result], "connector": connector}
]
response = view(request)
self.assertIsInstance(response, TemplateResponse) self.assertIsInstance(response, TemplateResponse)
validate_html(response.render()) validate_html(response.render())
connector_results = response.context_data["results"] connector_results = response.context_data["results"]
self.assertEqual(len(connector_results), 2) self.assertEqual(len(connector_results), 2)
self.assertEqual(connector_results[0]["results"][0].title, "Test Book") self.assertEqual(connector_results[0]["results"][0].title, "Test Book")
self.assertEqual(connector_results[1]["results"][0].title, "Mock Book") self.assertEqual(
connector_results[1]["results"][0].title,
"This Is How You Lose the Time War",
)
# don't search remote # don't search remote
request = self.factory.get("", {"q": "Test Book", "remote": True}) request = self.factory.get("", {"q": "Test Book", "remote": True})
@ -102,11 +106,7 @@ class Views(TestCase):
request.user = anonymous_user request.user = anonymous_user
with patch("bookwyrm.views.search.is_api_request") as is_api: with patch("bookwyrm.views.search.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
with patch("bookwyrm.connectors.connector_manager.search") as remote_search: response = view(request)
remote_search.return_value = [
{"results": [mock_result], "connector": connector}
]
response = view(request)
self.assertIsInstance(response, TemplateResponse) self.assertIsInstance(response, TemplateResponse)
validate_html(response.render()) validate_html(response.render())
connector_results = response.context_data["results"] connector_results = response.context_data["results"]

View file

@ -281,7 +281,7 @@ http://www.fish.com/"""
result = views.status.to_markdown(text) result = views.status.to_markdown(text)
self.assertEqual( self.assertEqual(
result, result,
'<p><em>hi</em> and <a href="http://fish.com">fish.com</a> is rad</p>', '<p><em>hi</em> and <a href="http://fish.com">fish.com</a> ' "is rad</p>",
) )
def test_to_markdown_detect_url(self, *_): def test_to_markdown_detect_url(self, *_):
@ -297,7 +297,7 @@ http://www.fish.com/"""
"""this is mostly handled in other places, but nonetheless""" """this is mostly handled in other places, but nonetheless"""
text = "[hi](http://fish.com) is <marquee>rad</marquee>" text = "[hi](http://fish.com) is <marquee>rad</marquee>"
result = views.status.to_markdown(text) result = views.status.to_markdown(text)
self.assertEqual(result, '<p><a href="http://fish.com">hi</a> is rad</p>') self.assertEqual(result, '<p><a href="http://fish.com">hi</a> ' "is rad</p>")
def test_delete_status(self, mock, *_): def test_delete_status(self, mock, *_):
"""marks a status as deleted""" """marks a status as deleted"""

View file

@ -391,9 +391,6 @@ urlpatterns = [
re_path( re_path(
r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group" r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group"
), ),
re_path(
rf"^group/(?P<group_id>\d+){regex.SLUG}/?$", views.Group.as_view(), name="group"
),
re_path( re_path(
r"^group/delete/(?P<group_id>\d+)/?$", views.delete_group, name="delete-group" r"^group/delete/(?P<group_id>\d+)/?$", views.delete_group, name="delete-group"
), ),
@ -420,10 +417,7 @@ urlpatterns = [
re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"), re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"),
re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"), re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"),
re_path(r"^list/(?P<list_id>\d+)(\.json)?/?$", views.List.as_view(), name="list"), re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
re_path(
rf"^list/(?P<list_id>\d+){regex.SLUG}/?$", views.List.as_view(), name="list"
),
re_path( re_path(
r"^list/(?P<list_id>\d+)/item/(?P<list_item>\d+)/?$", r"^list/(?P<list_id>\d+)/item/(?P<list_item>\d+)/?$",
views.ListItem.as_view(), views.ListItem.as_view(),
@ -493,7 +487,6 @@ urlpatterns = [
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock), re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
# statuses # statuses
re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"), re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"),
re_path(rf"{STATUS_PATH}{regex.SLUG}/?$", views.Status.as_view(), name="status"),
re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"), re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"),
re_path( re_path(
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies" rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
@ -530,27 +523,18 @@ urlpatterns = [
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()), re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
# books # books
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"), re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
re_path(rf"{BOOK_PATH}{regex.SLUG}/?$", views.Book.as_view(), name="book"),
re_path( re_path(
rf"{BOOK_PATH}/(?P<user_statuses>review|comment|quote)/?$", rf"{BOOK_PATH}/(?P<user_statuses>review|comment|quote)/?$",
views.Book.as_view(), views.Book.as_view(),
name="book-user-statuses", name="book-user-statuses",
), ),
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"), re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
re_path( re_path(rf"{BOOK_PATH}/confirm/?$", views.ConfirmEditBook.as_view()),
rf"{BOOK_PATH}/confirm/?$",
views.ConfirmEditBook.as_view(),
name="edit-book-confirm",
),
re_path( re_path(
r"^create-book/data/?$", views.create_book_from_data, name="create-book-data" r"^create-book/data/?$", views.create_book_from_data, name="create-book-data"
), ),
re_path(r"^create-book/?$", views.CreateBook.as_view(), name="create-book"), re_path(r"^create-book/?$", views.CreateBook.as_view(), name="create-book"),
re_path( re_path(r"^create-book/confirm/?$", views.ConfirmEditBook.as_view()),
r"^create-book/confirm/?$",
views.ConfirmEditBook.as_view(),
name="create-book-confirm",
),
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()), re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
re_path( re_path(
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover" r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
@ -596,11 +580,6 @@ urlpatterns = [
re_path( re_path(
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author" r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
), ),
re_path(
rf"^author/(?P<author_id>\d+){regex.SLUG}/?$",
views.Author.as_view(),
name="author",
),
re_path( re_path(
r"^author/(?P<author_id>\d+)/edit/?$", r"^author/(?P<author_id>\d+)/edit/?$",
views.EditAuthor.as_view(), views.EditAuthor.as_view(),
@ -622,7 +601,7 @@ urlpatterns = [
name="reading-status-update", name="reading-status-update",
), ),
re_path( re_path(
r"^reading-status/(?P<status>want|start|finish|stop)/(?P<book_id>\d+)/?$", r"^reading-status/(?P<status>want|start|finish)/(?P<book_id>\d+)/?$",
views.ReadingStatus.as_view(), views.ReadingStatus.as_view(),
name="reading-status", name="reading-status",
), ),

View file

@ -6,6 +6,5 @@ STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?" USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"
STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b" STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b"
FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b" FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b"
SLUG = r"/s/(?P<slug>[-_a-z0-9]*)"
# should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2; # should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2;
BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;" BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;"

View file

@ -103,7 +103,7 @@ class Dashboard(View):
status="pending" status="pending"
).count(), ).count(),
"invite_requests": models.InviteRequest.objects.filter( "invite_requests": models.InviteRequest.objects.filter(
ignored=False, invite__isnull=True ignored=False, invite_sent=False
).count(), ).count(),
"user_stats": user_chart.get_chart(start, end, interval), "user_stats": user_chart.get_chart(start, end, interval),
"status_stats": status_chart.get_chart(start, end, interval), "status_stats": status_chart.get_chart(start, end, interval),

View file

@ -11,24 +11,20 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path from bookwyrm.views.helpers import is_api_request
# pylint: disable= no-self-use # pylint: disable= no-self-use
class Author(View): class Author(View):
"""this person wrote a book""" """this person wrote a book"""
# pylint: disable=unused-argument def get(self, request, author_id):
def get(self, request, author_id, slug=None):
"""landing page for an author""" """landing page for an author"""
author = get_object_or_404(models.Author, id=author_id) author = get_object_or_404(models.Author, id=author_id)
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(author.to_activity()) return ActivitypubResponse(author.to_activity())
if redirect_local_path := maybe_redirect_local_path(request, author):
return redirect_local_path
books = ( books = (
models.Work.objects.filter(editions__authors=author) models.Work.objects.filter(editions__authors=author)
.order_by("created_date") .order_by("created_date")

View file

@ -15,14 +15,14 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager, ConnectorException from bookwyrm.connectors import connector_manager, ConnectorException
from bookwyrm.connectors.abstract_connector import get_image from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path from bookwyrm.views.helpers import is_api_request
# pylint: disable=no-self-use # pylint: disable=no-self-use
class Book(View): class Book(View):
"""a book! this is the stuff""" """a book! this is the stuff"""
def get(self, request, book_id, **kwargs): def get(self, request, book_id, user_statuses=False, update_error=False):
"""info about a book""" """info about a book"""
if is_api_request(request): if is_api_request(request):
book = get_object_or_404( book = get_object_or_404(
@ -30,11 +30,7 @@ class Book(View):
) )
return ActivitypubResponse(book.to_activity()) return ActivitypubResponse(book.to_activity())
user_statuses = ( user_statuses = user_statuses if request.user.is_authenticated else False
kwargs.get("user_statuses", False)
if request.user.is_authenticated
else False
)
# it's safe to use this OR because edition and work and subclasses of the same # it's safe to use this OR because edition and work and subclasses of the same
# table, so they never have clashing IDs # table, so they never have clashing IDs
@ -50,11 +46,6 @@ class Book(View):
if not book or not book.parent_work: if not book or not book.parent_work:
raise Http404() raise Http404()
if redirect_local_path := not user_statuses and maybe_redirect_local_path(
request, book
):
return redirect_local_path
# all reviews for all editions of the book # all reviews for all editions of the book
reviews = models.Review.privacy_filter(request.user).filter( reviews = models.Review.privacy_filter(request.user).filter(
book__parent_work__editions=book book__parent_work__editions=book
@ -89,7 +80,7 @@ class Book(View):
else None, else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"], "rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": lists, "lists": lists,
"update_error": kwargs.get("update_error", False), "update_error": update_error,
} }
if request.user.is_authenticated: if request.user.is_authenticated:

View file

@ -115,7 +115,6 @@ class CreateBook(View):
# go to confirm mode # go to confirm mode
if not parent_work_id or data.get("add_author"): if not parent_work_id or data.get("add_author"):
data["confirm_mode"] = True
return TemplateResponse(request, "book/edit/edit_book.html", data) return TemplateResponse(request, "book/edit/edit_book.html", data)
with transaction.atomic(): with transaction.atomic():
@ -190,7 +189,7 @@ def add_authors(request, data):
"existing_isnis": exists, "existing_isnis": exists,
} }
) )
return data return data
@require_POST @require_POST

Some files were not shown because too many files have changed in this diff Show more