mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-19 20:36:20 +00:00
Merge branch 'main' into move
This commit is contained in:
commit
088b9ab555
16 changed files with 186 additions and 67 deletions
|
@ -1,11 +1,20 @@
|
||||||
""" Use the range message from isbn-international to hyphenate ISBNs """
|
""" Use the range message from isbn-international to hyphenate ISBNs """
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
from xml.etree.ElementTree import Element
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from bookwyrm import settings
|
from bookwyrm import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _get_rules(element: Element) -> list[Element]:
|
||||||
|
if (rules_el := element.find("Rules")) is not None:
|
||||||
|
return rules_el.findall("Rule")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class IsbnHyphenator:
|
class IsbnHyphenator:
|
||||||
"""Class to manage the range message xml file and use it to hyphenate ISBNs"""
|
"""Class to manage the range message xml file and use it to hyphenate ISBNs"""
|
||||||
|
|
||||||
|
@ -15,58 +24,94 @@ class IsbnHyphenator:
|
||||||
)
|
)
|
||||||
__element_tree = None
|
__element_tree = None
|
||||||
|
|
||||||
def update_range_message(self):
|
def update_range_message(self) -> None:
|
||||||
"""Download the range message xml file and save it locally"""
|
"""Download the range message xml file and save it locally"""
|
||||||
response = requests.get(self.__range_message_url)
|
response = requests.get(self.__range_message_url)
|
||||||
with open(self.__range_file_path, "w", encoding="utf-8") as file:
|
with open(self.__range_file_path, "w", encoding="utf-8") as file:
|
||||||
file.write(response.text)
|
file.write(response.text)
|
||||||
self.__element_tree = None
|
self.__element_tree = None
|
||||||
|
|
||||||
def hyphenate(self, isbn_13):
|
def hyphenate(self, isbn_13: Optional[str]) -> Optional[str]:
|
||||||
"""hyphenate the given ISBN-13 number using the range message"""
|
"""hyphenate the given ISBN-13 number using the range message"""
|
||||||
if isbn_13 is None:
|
if isbn_13 is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.__element_tree is None:
|
if self.__element_tree is None:
|
||||||
self.__element_tree = ElementTree.parse(self.__range_file_path)
|
self.__element_tree = ElementTree.parse(self.__range_file_path)
|
||||||
|
|
||||||
gs1_prefix = isbn_13[:3]
|
gs1_prefix = isbn_13[:3]
|
||||||
reg_group = self.__find_reg_group(isbn_13, gs1_prefix)
|
reg_group = self.__find_reg_group(isbn_13, gs1_prefix)
|
||||||
if reg_group is None:
|
if reg_group is None:
|
||||||
return isbn_13 # failed to hyphenate
|
return isbn_13 # failed to hyphenate
|
||||||
|
|
||||||
registrant = self.__find_registrant(isbn_13, gs1_prefix, reg_group)
|
registrant = self.__find_registrant(isbn_13, gs1_prefix, reg_group)
|
||||||
if registrant is None:
|
if registrant is None:
|
||||||
return isbn_13 # failed to hyphenate
|
return isbn_13 # failed to hyphenate
|
||||||
|
|
||||||
publication = isbn_13[len(gs1_prefix) + len(reg_group) + len(registrant) : -1]
|
publication = isbn_13[len(gs1_prefix) + len(reg_group) + len(registrant) : -1]
|
||||||
check_digit = isbn_13[-1:]
|
check_digit = isbn_13[-1:]
|
||||||
return "-".join((gs1_prefix, reg_group, registrant, publication, check_digit))
|
return "-".join((gs1_prefix, reg_group, registrant, publication, check_digit))
|
||||||
|
|
||||||
def __find_reg_group(self, isbn_13, gs1_prefix):
|
def __find_reg_group(self, isbn_13: str, gs1_prefix: str) -> Optional[str]:
|
||||||
for ean_ucc_el in self.__element_tree.find("EAN.UCCPrefixes").findall(
|
if self.__element_tree is None:
|
||||||
"EAN.UCC"
|
self.__element_tree = ElementTree.parse(self.__range_file_path)
|
||||||
):
|
|
||||||
if ean_ucc_el.find("Prefix").text == gs1_prefix:
|
ucc_prefixes_el = self.__element_tree.find("EAN.UCCPrefixes")
|
||||||
for rule_el in ean_ucc_el.find("Rules").findall("Rule"):
|
if ucc_prefixes_el is None:
|
||||||
length = int(rule_el.find("Length").text)
|
return None
|
||||||
|
|
||||||
|
for ean_ucc_el in ucc_prefixes_el.findall("EAN.UCC"):
|
||||||
|
if (
|
||||||
|
prefix_el := ean_ucc_el.find("Prefix")
|
||||||
|
) is not None and prefix_el.text == gs1_prefix:
|
||||||
|
for rule_el in _get_rules(ean_ucc_el):
|
||||||
|
length_el = rule_el.find("Length")
|
||||||
|
if length_el is None:
|
||||||
|
continue
|
||||||
|
length = int(text) if (text := length_el.text) else 0
|
||||||
if length == 0:
|
if length == 0:
|
||||||
continue
|
continue
|
||||||
reg_grp_range = [
|
|
||||||
int(x[:length]) for x in rule_el.find("Range").text.split("-")
|
range_el = rule_el.find("Range")
|
||||||
]
|
if range_el is None or range_el.text is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
reg_grp_range = [int(x[:length]) for x in range_el.text.split("-")]
|
||||||
reg_group = isbn_13[len(gs1_prefix) : len(gs1_prefix) + length]
|
reg_group = isbn_13[len(gs1_prefix) : len(gs1_prefix) + length]
|
||||||
if reg_grp_range[0] <= int(reg_group) <= reg_grp_range[1]:
|
if reg_grp_range[0] <= int(reg_group) <= reg_grp_range[1]:
|
||||||
return reg_group
|
return reg_group
|
||||||
return None
|
return None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __find_registrant(self, isbn_13, gs1_prefix, reg_group):
|
def __find_registrant(
|
||||||
|
self, isbn_13: str, gs1_prefix: str, reg_group: str
|
||||||
|
) -> Optional[str]:
|
||||||
from_ind = len(gs1_prefix) + len(reg_group)
|
from_ind = len(gs1_prefix) + len(reg_group)
|
||||||
for group_el in self.__element_tree.find("RegistrationGroups").findall("Group"):
|
|
||||||
if group_el.find("Prefix").text == "-".join((gs1_prefix, reg_group)):
|
if self.__element_tree is None:
|
||||||
for rule_el in group_el.find("Rules").findall("Rule"):
|
self.__element_tree = ElementTree.parse(self.__range_file_path)
|
||||||
length = int(rule_el.find("Length").text)
|
|
||||||
|
reg_groups_el = self.__element_tree.find("RegistrationGroups")
|
||||||
|
if reg_groups_el is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for group_el in reg_groups_el.findall("Group"):
|
||||||
|
if (
|
||||||
|
prefix_el := group_el.find("Prefix")
|
||||||
|
) is not None and prefix_el.text == "-".join((gs1_prefix, reg_group)):
|
||||||
|
for rule_el in _get_rules(group_el):
|
||||||
|
length_el = rule_el.find("Length")
|
||||||
|
if length_el is None:
|
||||||
|
continue
|
||||||
|
length = int(text) if (text := length_el.text) else 0
|
||||||
if length == 0:
|
if length == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
range_el = rule_el.find("Range")
|
||||||
|
if range_el is None or range_el.text is None:
|
||||||
|
continue
|
||||||
registrant_range = [
|
registrant_range = [
|
||||||
int(x[:length]) for x in rule_el.find("Range").text.split("-")
|
int(x[:length]) for x in range_el.text.split("-")
|
||||||
]
|
]
|
||||||
registrant = isbn_13[from_ind : from_ind + length]
|
registrant = isbn_13[from_ind : from_ind + length]
|
||||||
if registrant_range[0] <= int(registrant) <= registrant_range[1]:
|
if registrant_range[0] <= int(registrant) <= registrant_range[1]:
|
||||||
|
|
|
@ -217,6 +217,13 @@ class Book(BookDataModel):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
return f"https://{DOMAIN}/book/{self.id}"
|
return f"https://{DOMAIN}/book/{self.id}"
|
||||||
|
|
||||||
|
def guess_sort_title(self):
|
||||||
|
"""Get a best-guess sort title for the current book"""
|
||||||
|
articles = chain(
|
||||||
|
*(LANGUAGE_ARTICLES.get(language, ()) for language in tuple(self.languages))
|
||||||
|
)
|
||||||
|
return re.sub(f'^{" |^".join(articles)} ', "", str(self.title).lower())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
# pylint: disable=consider-using-f-string
|
# pylint: disable=consider-using-f-string
|
||||||
return "<{} key={!r} title={!r}>".format(
|
return "<{} key={!r} title={!r}>".format(
|
||||||
|
@ -374,16 +381,7 @@ class Edition(Book):
|
||||||
|
|
||||||
# Create sort title by removing articles from title
|
# Create sort title by removing articles from title
|
||||||
if self.sort_title in [None, ""]:
|
if self.sort_title in [None, ""]:
|
||||||
if self.sort_title in [None, ""]:
|
self.sort_title = self.guess_sort_title()
|
||||||
articles = chain(
|
|
||||||
*(
|
|
||||||
LANGUAGE_ARTICLES.get(language, ())
|
|
||||||
for language in tuple(self.languages)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.sort_title = re.sub(
|
|
||||||
f'^{" |^".join(articles)} ', "", str(self.title).lower()
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" models for storing different kinds of Activities """
|
""" models for storing different kinds of Activities """
|
||||||
from dataclasses import MISSING
|
from dataclasses import MISSING
|
||||||
|
from typing import Optional
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -269,7 +270,7 @@ class GeneratedNote(Status):
|
||||||
"""indicate the book in question for mastodon (or w/e) users"""
|
"""indicate the book in question for mastodon (or w/e) users"""
|
||||||
message = self.content
|
message = self.content
|
||||||
books = ", ".join(
|
books = ", ".join(
|
||||||
f'<a href="{book.remote_id}">"{book.title}"</a>'
|
f'<a href="{book.remote_id}"><i>{book.title}</i></a>'
|
||||||
for book in self.mention_books.all()
|
for book in self.mention_books.all()
|
||||||
)
|
)
|
||||||
return f"{self.user.display_name} {message} {books}"
|
return f"{self.user.display_name} {message} {books}"
|
||||||
|
@ -320,17 +321,14 @@ 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):
|
progress = self.progress or 0
|
||||||
return_value = (
|
citation = (
|
||||||
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
f'comment on <a href="{self.book.remote_id}">'
|
||||||
f'"{self.book.title}"</a>, page {self.progress})</p>'
|
f"<i>{self.book.title}</i></a>"
|
||||||
)
|
)
|
||||||
else:
|
if self.progress_mode == "PG" and progress > 0:
|
||||||
return_value = (
|
citation += f", p. {progress}"
|
||||||
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
return f"{self.content}<p>({citation})</p>"
|
||||||
f'"{self.book.title}"</a>)</p>'
|
|
||||||
)
|
|
||||||
return return_value
|
|
||||||
|
|
||||||
activity_serializer = activitypub.Comment
|
activity_serializer = activitypub.Comment
|
||||||
|
|
||||||
|
@ -354,22 +352,24 @@ class Quotation(BookStatus):
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _format_position(self) -> Optional[str]:
|
||||||
|
"""serialize page position"""
|
||||||
|
beg = self.position
|
||||||
|
end = self.endposition or 0
|
||||||
|
if self.position_mode != "PG" or not beg:
|
||||||
|
return None
|
||||||
|
return f"pp. {beg}-{end}" if end > beg else f"p. {beg}"
|
||||||
|
|
||||||
@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"""
|
||||||
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):
|
title, href = self.book.title, self.book.remote_id
|
||||||
return_value = (
|
citation = f'— <a href="{href}"><i>{title}</i></a>'
|
||||||
f'{quote} <p>-- <a href="{self.book.remote_id}">'
|
if position := self._format_position():
|
||||||
f'"{self.book.title}"</a>, page {self.position}</p>{self.content}'
|
citation += f", {position}"
|
||||||
)
|
return f"{quote} <p>{citation}</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
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
""" bookwyrm settings and configuration """
|
""" bookwyrm settings and configuration """
|
||||||
import os
|
import os
|
||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
from environs import Env
|
from environs import Env
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -12,7 +14,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.6.4"
|
VERSION = "0.6.5"
|
||||||
|
|
||||||
RELEASE_API = env(
|
RELEASE_API = env(
|
||||||
"RELEASE_API",
|
"RELEASE_API",
|
||||||
|
@ -37,7 +39,7 @@ EMAIL_SENDER_DOMAIN = env("EMAIL_SENDER_DOMAIN", DOMAIN)
|
||||||
EMAIL_SENDER = f"{EMAIL_SENDER_NAME}@{EMAIL_SENDER_DOMAIN}"
|
EMAIL_SENDER = f"{EMAIL_SENDER_NAME}@{EMAIL_SENDER_DOMAIN}"
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR: AnyStr = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
os.path.join(BASE_DIR, "locale"),
|
os.path.join(BASE_DIR, "locale"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -106,7 +106,7 @@ const tries = {
|
||||||
e: {
|
e: {
|
||||||
p: {
|
p: {
|
||||||
u: {
|
u: {
|
||||||
b: "ePub",
|
b: "EPUB",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{% 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 }}">
|
||||||
{% if form.parent_work %}
|
{% if book.parent_work.id or form.parent_work %}
|
||||||
<input type="hidden" name="parent_work" value="{% firstof book.parent_work.id form.parent_work %}">
|
<input type="hidden" name="parent_work" value="{% firstof book.parent_work.id form.parent_work %}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
|
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}"><i>{{ work_title }}</i></a>{% endblocktrans %}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'book/editions/edition_filters.html' %}
|
{% include 'book/editions/edition_filters.html' %}
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
required=""
|
required=""
|
||||||
id="id_filetype"
|
id="id_filetype"
|
||||||
value="{% firstof file_link_form.filetype.value '' %}"
|
value="{% firstof file_link_form.filetype.value '' %}"
|
||||||
placeholder="ePub"
|
placeholder="EPUB"
|
||||||
list="mimetypes-list"
|
list="mimetypes-list"
|
||||||
data-autocomplete="mimetype"
|
data-autocomplete="mimetype"
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,14 +3,13 @@
|
||||||
xmlns="http://a9.com/-/spec/opensearch/1.1/"
|
xmlns="http://a9.com/-/spec/opensearch/1.1/"
|
||||||
xmlns:moz="http://www.mozilla.org/2006/browser/search/"
|
xmlns:moz="http://www.mozilla.org/2006/browser/search/"
|
||||||
>
|
>
|
||||||
<ShortName>{{ site_name }}</ShortName>
|
<ShortName>{{ site.name }}</ShortName>
|
||||||
<Description>{% blocktrans trimmed with site_name=site.name %}
|
<Description>{% blocktrans trimmed with site_name=site.name %}
|
||||||
{{ site_name }} search
|
{{ site_name }} search
|
||||||
{% endblocktrans %}</Description>
|
{% endblocktrans %}</Description>
|
||||||
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
|
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
|
||||||
<Url
|
<Url
|
||||||
type="text/html"
|
type="text/html"
|
||||||
method="get"
|
|
||||||
template="https://{{ DOMAIN }}{% url 'search' %}?q={searchTerms}"
|
template="https://{{ DOMAIN }}{% url 'search' %}?q={searchTerms}"
|
||||||
/>
|
/>
|
||||||
</OpenSearchDescription>
|
</OpenSearchDescription>
|
||||||
|
|
|
@ -212,7 +212,7 @@ class Status(TestCase):
|
||||||
def test_generated_note_to_pure_activity(self, *_):
|
def test_generated_note_to_pure_activity(self, *_):
|
||||||
"""subclass of the base model version with a "pure" serializer"""
|
"""subclass of the base model version with a "pure" serializer"""
|
||||||
status = models.GeneratedNote.objects.create(
|
status = models.GeneratedNote.objects.create(
|
||||||
content="test content", user=self.local_user
|
content="reads", user=self.local_user
|
||||||
)
|
)
|
||||||
status.mention_books.set([self.book])
|
status.mention_books.set([self.book])
|
||||||
status.mention_users.set([self.local_user])
|
status.mention_users.set([self.local_user])
|
||||||
|
@ -220,7 +220,7 @@ class Status(TestCase):
|
||||||
self.assertEqual(activity["id"], status.remote_id)
|
self.assertEqual(activity["id"], status.remote_id)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
activity["content"],
|
activity["content"],
|
||||||
f'mouse test content <a href="{self.book.remote_id}">"Test Edition"</a>',
|
f'mouse reads <a href="{self.book.remote_id}"><i>Test Edition</i></a>',
|
||||||
)
|
)
|
||||||
self.assertEqual(len(activity["tag"]), 2)
|
self.assertEqual(len(activity["tag"]), 2)
|
||||||
self.assertEqual(activity["type"], "Note")
|
self.assertEqual(activity["type"], "Note")
|
||||||
|
@ -249,14 +249,18 @@ class Status(TestCase):
|
||||||
def test_comment_to_pure_activity(self, *_):
|
def test_comment_to_pure_activity(self, *_):
|
||||||
"""subclass of the base model version with a "pure" serializer"""
|
"""subclass of the base model version with a "pure" serializer"""
|
||||||
status = models.Comment.objects.create(
|
status = models.Comment.objects.create(
|
||||||
content="test content", user=self.local_user, book=self.book
|
content="test content", user=self.local_user, book=self.book, progress=27
|
||||||
)
|
)
|
||||||
activity = status.to_activity(pure=True)
|
activity = status.to_activity(pure=True)
|
||||||
self.assertEqual(activity["id"], status.remote_id)
|
self.assertEqual(activity["id"], status.remote_id)
|
||||||
self.assertEqual(activity["type"], "Note")
|
self.assertEqual(activity["type"], "Note")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
activity["content"],
|
activity["content"],
|
||||||
f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>',
|
(
|
||||||
|
"test content"
|
||||||
|
f'<p>(comment on <a href="{self.book.remote_id}">'
|
||||||
|
"<i>Test Edition</i></a>, p. 27)</p>"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
# self.assertTrue(
|
# self.assertTrue(
|
||||||
|
@ -295,7 +299,11 @@ class Status(TestCase):
|
||||||
self.assertEqual(activity["type"], "Note")
|
self.assertEqual(activity["type"], "Note")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
activity["content"],
|
activity["content"],
|
||||||
f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content',
|
(
|
||||||
|
"a sickening sense "
|
||||||
|
f'<p>— <a href="{self.book.remote_id}">'
|
||||||
|
"<i>Test Edition</i></a></p>test content"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
|
@ -306,6 +314,29 @@ class Status(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
|
def test_quotation_page_serialization(self, *_):
|
||||||
|
"""serialization of quotation page position"""
|
||||||
|
tests = [
|
||||||
|
("single pos", 7, None, "p. 7"),
|
||||||
|
("page range", 7, 10, "pp. 7-10"),
|
||||||
|
]
|
||||||
|
for desc, beg, end, pages in tests:
|
||||||
|
with self.subTest(desc):
|
||||||
|
status = models.Quotation.objects.create(
|
||||||
|
quote="<p>my quote</p>",
|
||||||
|
content="",
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
position=beg,
|
||||||
|
endposition=end,
|
||||||
|
position_mode="PG",
|
||||||
|
)
|
||||||
|
activity = status.to_activity(pure=True)
|
||||||
|
self.assertRegex(
|
||||||
|
activity["content"],
|
||||||
|
f'^<p>"my quote"</p> <p>— <a .+</a>, {pages}</p>$',
|
||||||
|
)
|
||||||
|
|
||||||
def test_review_to_activity(self, *_):
|
def test_review_to_activity(self, *_):
|
||||||
"""subclass of the base model version with a "pure" serializer"""
|
"""subclass of the base model version with a "pure" serializer"""
|
||||||
status = models.Review.objects.create(
|
status = models.Review.objects.create(
|
||||||
|
|
31
bookwyrm/tests/test_isbn.py
Normal file
31
bookwyrm/tests/test_isbn.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
""" test ISBN hyphenator for books """
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookwyrm.isbn.isbn import hyphenator_singleton as hyphenator
|
||||||
|
|
||||||
|
|
||||||
|
class TestISBN(TestCase):
|
||||||
|
"""isbn hyphenator"""
|
||||||
|
|
||||||
|
def test_isbn_hyphenation(self):
|
||||||
|
"""different isbn hyphenations"""
|
||||||
|
# nothing
|
||||||
|
self.assertEqual(hyphenator.hyphenate(None), None)
|
||||||
|
# 978-0 (English language) 3700000-6389999
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9780439554930"), "978-0-439-55493-0")
|
||||||
|
# 978-2 (French language) 0000000-1999999
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9782070100927"), "978-2-07-010092-7")
|
||||||
|
# 978-3 (German language) 2000000-6999999
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9783518188125"), "978-3-518-18812-5")
|
||||||
|
# 978-4 (Japan) 0000000-1999999
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9784101050454"), "978-4-10-105045-4")
|
||||||
|
# 978-626 (Taiwan) 9500000-9999999
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9786269533251"), "978-626-95332-5-1")
|
||||||
|
# 979-8 (United States) 4000000-8499999
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9798627974040"), "979-8-6279-7404-0")
|
||||||
|
# 978-626 (Taiwan) 8000000-9499999 (unassigned)
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9786268533251"), "9786268533251")
|
||||||
|
# 978 range 6600000-6999999 (unassigned)
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9786769533251"), "9786769533251")
|
||||||
|
# 979-8 (United States) 2300000-3499999 (unassigned)
|
||||||
|
self.assertEqual(hyphenator.hyphenate("9798311111111"), "9798311111111")
|
|
@ -1,6 +1,7 @@
|
||||||
""" url routing for the app and api """
|
""" url routing for the app and api """
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
|
@ -780,5 +781,8 @@ urlpatterns = [
|
||||||
path("guided-tour/<tour>", views.toggle_guided_tour),
|
path("guided-tour/<tour>", views.toggle_guided_tour),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
# Serves /static when DEBUG is true.
|
||||||
|
urlpatterns.extend(staticfiles_urlpatterns())
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
handler500 = "bookwyrm.views.server_error"
|
handler500 = "bookwyrm.views.server_error"
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
@ -108,6 +109,7 @@ class EditAnnouncement(View):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||||
|
@require_POST
|
||||||
def delete_announcement(_, announcement_id):
|
def delete_announcement(_, announcement_id):
|
||||||
"""delete announcement"""
|
"""delete announcement"""
|
||||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||||
|
|
|
@ -32,6 +32,9 @@ class EditBook(View):
|
||||||
def get(self, request, book_id):
|
def get(self, request, book_id):
|
||||||
"""info about a book"""
|
"""info about a book"""
|
||||||
book = get_edition(book_id)
|
book = get_edition(book_id)
|
||||||
|
# This doesn't update the sort title, just pre-populates it in the form
|
||||||
|
if book.sort_title in ["", None]:
|
||||||
|
book.sort_title = book.guess_sort_title()
|
||||||
if not book.description:
|
if not book.description:
|
||||||
book.description = book.parent_work.description
|
book.description = book.parent_work.description
|
||||||
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
||||||
|
@ -40,6 +43,7 @@ class EditBook(View):
|
||||||
def post(self, request, book_id):
|
def post(self, request, book_id):
|
||||||
"""edit a book cool"""
|
"""edit a book cool"""
|
||||||
book = get_object_or_404(models.Edition, id=book_id)
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
|
|
||||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||||
|
|
||||||
data = {"book": book, "form": form}
|
data = {"book": book, "form": form}
|
||||||
|
|
|
@ -2,7 +2,7 @@ version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:1.25.2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "1333:80"
|
- "1333:80"
|
||||||
|
@ -38,7 +38,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "8000"
|
- "8000"
|
||||||
redis_activity:
|
redis_activity:
|
||||||
image: redis
|
image: redis:7.2.1
|
||||||
command: redis-server --requirepass ${REDIS_ACTIVITY_PASSWORD} --appendonly yes --port ${REDIS_ACTIVITY_PORT}
|
command: redis-server --requirepass ${REDIS_ACTIVITY_PASSWORD} --appendonly yes --port ${REDIS_ACTIVITY_PORT}
|
||||||
volumes:
|
volumes:
|
||||||
- ./redis.conf:/etc/redis/redis.conf
|
- ./redis.conf:/etc/redis/redis.conf
|
||||||
|
@ -48,7 +48,7 @@ services:
|
||||||
- main
|
- main
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
redis_broker:
|
redis_broker:
|
||||||
image: redis
|
image: redis:7.2.1
|
||||||
command: redis-server --requirepass ${REDIS_BROKER_PASSWORD} --appendonly yes --port ${REDIS_BROKER_PORT}
|
command: redis-server --requirepass ${REDIS_BROKER_PASSWORD} --appendonly yes --port ${REDIS_BROKER_PORT}
|
||||||
volumes:
|
volumes:
|
||||||
- ./redis.conf:/etc/redis/redis.conf
|
- ./redis.conf:/etc/redis/redis.conf
|
||||||
|
|
3
mypy.ini
3
mypy.ini
|
@ -16,6 +16,9 @@ ignore_errors = False
|
||||||
[mypy-bookwyrm.importers.*]
|
[mypy-bookwyrm.importers.*]
|
||||||
ignore_errors = False
|
ignore_errors = False
|
||||||
|
|
||||||
|
[mypy-bookwyrm.isbn.*]
|
||||||
|
ignore_errors = False
|
||||||
|
|
||||||
[mypy-celerywyrm.*]
|
[mypy-celerywyrm.*]
|
||||||
ignore_errors = False
|
ignore_errors = False
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue