mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-04 05:12:22 +00:00
Merge branch 'main' into opensearch
This commit is contained in:
commit
15fc31bf77
143 changed files with 3967 additions and 1784 deletions
|
@ -16,7 +16,7 @@ DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
POSTGRES_PORT=5432
|
PGPORT=5432
|
||||||
POSTGRES_PASSWORD=securedbypassword123
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
POSTGRES_USER=fedireads
|
POSTGRES_USER=fedireads
|
||||||
POSTGRES_DB=fedireads
|
POSTGRES_DB=fedireads
|
||||||
|
|
|
@ -16,7 +16,7 @@ DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
POSTGRES_PORT=5432
|
PGPORT=5432
|
||||||
POSTGRES_PASSWORD=securedbypassword123
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
POSTGRES_USER=fedireads
|
POSTGRES_USER=fedireads
|
||||||
POSTGRES_DB=fedireads
|
POSTGRES_DB=fedireads
|
||||||
|
|
|
@ -101,7 +101,7 @@ class ActivityObject:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if field.default == MISSING and field.default_factory == MISSING:
|
if field.default == MISSING and field.default_factory == MISSING:
|
||||||
raise ActivitySerializerError(
|
raise ActivitySerializerError(
|
||||||
"Missing required field: %s" % field.name
|
f"Missing required field: {field.name}"
|
||||||
)
|
)
|
||||||
value = field.default
|
value = field.default
|
||||||
setattr(self, field.name, value)
|
setattr(self, field.name, value)
|
||||||
|
@ -219,8 +219,8 @@ def set_related_field(
|
||||||
model_name, origin_model_name, related_field_name, related_remote_id, data
|
model_name, origin_model_name, related_field_name, related_remote_id, data
|
||||||
):
|
):
|
||||||
"""load reverse related fields (editions, attachments) without blocking"""
|
"""load reverse related fields (editions, attachments) without blocking"""
|
||||||
model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True)
|
model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True)
|
||||||
origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True)
|
origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
|
@ -234,7 +234,7 @@ def set_related_field(
|
||||||
# this must exist because it's the object that triggered this function
|
# this must exist because it's the object that triggered this function
|
||||||
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
instance = origin_model.find_existing_by_remote_id(related_remote_id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise ValueError("Invalid related remote id: %s" % related_remote_id)
|
raise ValueError(f"Invalid related remote id: {related_remote_id}")
|
||||||
|
|
||||||
# set the origin's remote id on the activity so it will be there when
|
# set the origin's remote id on the activity so it will be there when
|
||||||
# the model instance is created
|
# the model instance is created
|
||||||
|
@ -265,7 +265,7 @@ def get_model_from_type(activity_type):
|
||||||
]
|
]
|
||||||
if not model:
|
if not model:
|
||||||
raise ActivitySerializerError(
|
raise ActivitySerializerError(
|
||||||
'No model found for activity type "%s"' % activity_type
|
f'No model found for activity type "{activity_type}"'
|
||||||
)
|
)
|
||||||
return model[0]
|
return model[0]
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ def resolve_remote_id(
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
raise ActivitySerializerError(
|
raise ActivitySerializerError(
|
||||||
"Could not connect to host for remote_id in: %s" % (remote_id)
|
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
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
""" access the activity streams stored in redis """
|
""" access the activity streams stored in redis """
|
||||||
|
from datetime import timedelta
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import signals, Q
|
from django.db.models import signals, Q
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.redis_store import RedisStore, r
|
from bookwyrm.redis_store import RedisStore, r
|
||||||
|
@ -14,11 +16,12 @@ class ActivityStream(RedisStore):
|
||||||
|
|
||||||
def stream_id(self, user):
|
def stream_id(self, user):
|
||||||
"""the redis key for this user's instance of this stream"""
|
"""the redis key for this user's instance of this stream"""
|
||||||
return "{}-{}".format(user.id, self.key)
|
return f"{user.id}-{self.key}"
|
||||||
|
|
||||||
def unread_id(self, user):
|
def unread_id(self, user):
|
||||||
"""the redis key for this user's unread count for this stream"""
|
"""the redis key for this user's unread count for this stream"""
|
||||||
return "{}-unread".format(self.stream_id(user))
|
stream_id = self.stream_id(user)
|
||||||
|
return f"{stream_id}-unread"
|
||||||
|
|
||||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||||
"""statuses are sorted by date published"""
|
"""statuses are sorted by date published"""
|
||||||
|
@ -436,6 +439,10 @@ def remove_status_task(status_ids):
|
||||||
def add_status_task(status_id, increment_unread=False):
|
def add_status_task(status_id, increment_unread=False):
|
||||||
"""add a status to any stream it should be in"""
|
"""add a status to any stream it should be in"""
|
||||||
status = models.Status.objects.get(id=status_id)
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
# we don't want to tick the unread count for csv import statuses, idk how better
|
||||||
|
# to check than just to see if the states is more than a few days old
|
||||||
|
if status.created_date < timezone.now() - timedelta(days=2):
|
||||||
|
increment_unread = False
|
||||||
for stream in streams.values():
|
for stream in streams.values():
|
||||||
stream.add_status(status, increment_unread=increment_unread)
|
stream.add_status(status, increment_unread=increment_unread)
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ class AbstractMinimalConnector(ABC):
|
||||||
params["min_confidence"] = min_confidence
|
params["min_confidence"] = min_confidence
|
||||||
|
|
||||||
data = self.get_search_data(
|
data = self.get_search_data(
|
||||||
"%s%s" % (self.search_url, query),
|
f"{self.search_url}{query}",
|
||||||
params=params,
|
params=params,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
@ -57,7 +57,7 @@ class AbstractMinimalConnector(ABC):
|
||||||
"""isbn search"""
|
"""isbn search"""
|
||||||
params = {}
|
params = {}
|
||||||
data = self.get_search_data(
|
data = self.get_search_data(
|
||||||
"%s%s" % (self.isbn_search_url, query),
|
f"{self.isbn_search_url}{query}",
|
||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
results = []
|
results = []
|
||||||
|
@ -131,7 +131,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
work_data = data
|
work_data = data
|
||||||
|
|
||||||
if not work_data or not edition_data:
|
if not work_data or not edition_data:
|
||||||
raise ConnectorException("Unable to load book data: %s" % remote_id)
|
raise ConnectorException(f"Unable to load book data: {remote_id}")
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# create activitypub object
|
# create activitypub object
|
||||||
|
@ -222,9 +222,7 @@ def get_data(url, params=None, timeout=10):
|
||||||
"""wrapper for request.get"""
|
"""wrapper for request.get"""
|
||||||
# check if the url is blocked
|
# check if the url is blocked
|
||||||
if models.FederatedServer.is_blocked(url):
|
if models.FederatedServer.is_blocked(url):
|
||||||
raise ConnectorException(
|
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||||
"Attempting to load data from blocked url: {:s}".format(url)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
|
@ -283,6 +281,7 @@ class SearchResult:
|
||||||
confidence: int = 1
|
confidence: int = 1
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||||
self.key, self.title, self.author
|
self.key, self.title, self.author
|
||||||
)
|
)
|
||||||
|
|
|
@ -109,10 +109,10 @@ def get_or_create_connector(remote_id):
|
||||||
connector_info = models.Connector.objects.create(
|
connector_info = models.Connector.objects.create(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
connector_file="bookwyrm_connector",
|
connector_file="bookwyrm_connector",
|
||||||
base_url="https://%s" % identifier,
|
base_url=f"https://{identifier}",
|
||||||
books_url="https://%s/book" % identifier,
|
books_url=f"https://{identifier}/book",
|
||||||
covers_url="https://%s/images/covers" % identifier,
|
covers_url=f"https://{identifier}/images/covers",
|
||||||
search_url="https://%s/search?q=" % identifier,
|
search_url=f"https://{identifier}/search?q=",
|
||||||
priority=2,
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ def load_more_data(connector_id, book_id):
|
||||||
def load_connector(connector_info):
|
def load_connector(connector_info):
|
||||||
"""instantiate the connector class"""
|
"""instantiate the connector class"""
|
||||||
connector = importlib.import_module(
|
connector = importlib.import_module(
|
||||||
"bookwyrm.connectors.%s" % connector_info.connector_file
|
f"bookwyrm.connectors.{connector_info.connector_file}"
|
||||||
)
|
)
|
||||||
return connector.Connector(connector_info.identifier)
|
return connector.Connector(connector_info.identifier)
|
||||||
|
|
||||||
|
@ -141,4 +141,4 @@ def load_connector(connector_info):
|
||||||
def create_connector(sender, instance, created, *args, **kwargs):
|
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("https://{:s}".format(instance.server_name))
|
get_or_create_connector(f"https://{instance.server_name}")
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
def get_remote_id(self, value):
|
def get_remote_id(self, value):
|
||||||
"""convert an id/uri into a url"""
|
"""convert an id/uri into a url"""
|
||||||
return "{:s}?action=by-uris&uris={:s}".format(self.books_url, value)
|
return f"{self.books_url}?action=by-uris&uris={value}"
|
||||||
|
|
||||||
def get_book_data(self, remote_id):
|
def get_book_data(self, remote_id):
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
|
@ -87,11 +87,7 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
images = search_result.get("image")
|
images = search_result.get("image")
|
||||||
cover = (
|
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
||||||
"{:s}/img/entities/{:s}".format(self.covers_url, images[0])
|
|
||||||
if images
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
# a deeply messy translation of inventaire's scores
|
# a deeply messy translation of inventaire's scores
|
||||||
confidence = float(search_result.get("_score", 0.1))
|
confidence = float(search_result.get("_score", 0.1))
|
||||||
confidence = 0.1 if confidence < 150 else 0.999
|
confidence = 0.1 if confidence < 150 else 0.999
|
||||||
|
@ -99,9 +95,7 @@ class Connector(AbstractConnector):
|
||||||
title=search_result.get("label"),
|
title=search_result.get("label"),
|
||||||
key=self.get_remote_id(search_result.get("uri")),
|
key=self.get_remote_id(search_result.get("uri")),
|
||||||
author=search_result.get("description"),
|
author=search_result.get("description"),
|
||||||
view_link="{:s}/entity/{:s}".format(
|
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
|
||||||
self.base_url, search_result.get("uri")
|
|
||||||
),
|
|
||||||
cover=cover,
|
cover=cover,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
connector=self,
|
connector=self,
|
||||||
|
@ -123,9 +117,7 @@ class Connector(AbstractConnector):
|
||||||
title=title[0],
|
title=title[0],
|
||||||
key=self.get_remote_id(search_result.get("uri")),
|
key=self.get_remote_id(search_result.get("uri")),
|
||||||
author=search_result.get("description"),
|
author=search_result.get("description"),
|
||||||
view_link="{:s}/entity/{:s}".format(
|
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
|
||||||
self.base_url, search_result.get("uri")
|
|
||||||
),
|
|
||||||
cover=self.get_cover_url(search_result.get("image")),
|
cover=self.get_cover_url(search_result.get("image")),
|
||||||
connector=self,
|
connector=self,
|
||||||
)
|
)
|
||||||
|
@ -135,11 +127,7 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
def load_edition_data(self, work_uri):
|
def load_edition_data(self, work_uri):
|
||||||
"""get a list of editions for a work"""
|
"""get a list of editions for a work"""
|
||||||
url = (
|
url = f"{self.books_url}?action=reverse-claims&property=wdt:P629&value={work_uri}&sort=true"
|
||||||
"{:s}?action=reverse-claims&property=wdt:P629&value={:s}&sort=true".format(
|
|
||||||
self.books_url, work_uri
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return get_data(url)
|
return get_data(url)
|
||||||
|
|
||||||
def get_edition_from_work_data(self, data):
|
def get_edition_from_work_data(self, data):
|
||||||
|
@ -195,7 +183,7 @@ class Connector(AbstractConnector):
|
||||||
# cover may or may not be an absolute url already
|
# cover may or may not be an absolute url already
|
||||||
if re.match(r"^http", cover_id):
|
if re.match(r"^http", cover_id):
|
||||||
return cover_id
|
return cover_id
|
||||||
return "%s%s" % (self.covers_url, cover_id)
|
return f"{self.covers_url}{cover_id}"
|
||||||
|
|
||||||
def resolve_keys(self, keys):
|
def resolve_keys(self, keys):
|
||||||
"""cool, it's "wd:Q3156592" now what the heck does that mean"""
|
"""cool, it's "wd:Q3156592" now what the heck does that mean"""
|
||||||
|
@ -213,9 +201,7 @@ class Connector(AbstractConnector):
|
||||||
link = links.get("enwiki")
|
link = links.get("enwiki")
|
||||||
if not link:
|
if not link:
|
||||||
return ""
|
return ""
|
||||||
url = "{:s}/api/data?action=wp-extract&lang=en&title={:s}".format(
|
url = f"{self.base_url}/api/data?action=wp-extract&lang=en&title={link}"
|
||||||
self.base_url, link
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
data = get_data(url)
|
data = get_data(url)
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
|
|
|
@ -71,7 +71,7 @@ class Connector(AbstractConnector):
|
||||||
key = data["key"]
|
key = data["key"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConnectorException("Invalid book data")
|
raise ConnectorException("Invalid book data")
|
||||||
return "%s%s" % (self.books_url, key)
|
return f"{self.books_url}{key}"
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
return bool(re.match(r"^[\/\w]+OL\d+W$", data["key"]))
|
return bool(re.match(r"^[\/\w]+OL\d+W$", data["key"]))
|
||||||
|
@ -81,7 +81,7 @@ class Connector(AbstractConnector):
|
||||||
key = data["key"]
|
key = data["key"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConnectorException("Invalid book data")
|
raise ConnectorException("Invalid book data")
|
||||||
url = "%s%s/editions" % (self.books_url, key)
|
url = f"{self.books_url}{key}/editions"
|
||||||
data = self.get_book_data(url)
|
data = self.get_book_data(url)
|
||||||
edition = pick_default_edition(data["entries"])
|
edition = pick_default_edition(data["entries"])
|
||||||
if not edition:
|
if not edition:
|
||||||
|
@ -93,7 +93,7 @@ class Connector(AbstractConnector):
|
||||||
key = data["works"][0]["key"]
|
key = data["works"][0]["key"]
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
raise ConnectorException("No work found for edition")
|
raise ConnectorException("No work found for edition")
|
||||||
url = "%s%s" % (self.books_url, key)
|
url = f"{self.books_url}{key}"
|
||||||
return self.get_book_data(url)
|
return self.get_book_data(url)
|
||||||
|
|
||||||
def get_authors_from_data(self, data):
|
def get_authors_from_data(self, data):
|
||||||
|
@ -102,7 +102,7 @@ class Connector(AbstractConnector):
|
||||||
author_blob = author_blob.get("author", author_blob)
|
author_blob = author_blob.get("author", author_blob)
|
||||||
# this id is "/authors/OL1234567A"
|
# this id is "/authors/OL1234567A"
|
||||||
author_id = author_blob["key"]
|
author_id = author_blob["key"]
|
||||||
url = "%s%s" % (self.base_url, author_id)
|
url = f"{self.base_url}{author_id}"
|
||||||
author = self.get_or_create_author(url)
|
author = self.get_or_create_author(url)
|
||||||
if not author:
|
if not author:
|
||||||
continue
|
continue
|
||||||
|
@ -113,8 +113,8 @@ class Connector(AbstractConnector):
|
||||||
if not cover_blob:
|
if not cover_blob:
|
||||||
return None
|
return None
|
||||||
cover_id = cover_blob[0]
|
cover_id = cover_blob[0]
|
||||||
image_name = "%s-%s.jpg" % (cover_id, size)
|
image_name = f"{cover_id}-{size}.jpg"
|
||||||
return "%s/b/id/%s" % (self.covers_url, image_name)
|
return f"{self.covers_url}/b/id/{image_name}"
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data):
|
||||||
return data.get("docs")
|
return data.get("docs")
|
||||||
|
@ -152,7 +152,7 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
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"""
|
||||||
url = "%s/works/%s/editions" % (self.books_url, olkey)
|
url = f"{self.books_url}/works/{olkey}/editions"
|
||||||
return self.get_book_data(url)
|
return self.get_book_data(url)
|
||||||
|
|
||||||
def expand_book_data(self, book):
|
def expand_book_data(self, book):
|
||||||
|
|
|
@ -71,7 +71,7 @@ class Connector(AbstractConnector):
|
||||||
def format_search_result(self, search_result):
|
def format_search_result(self, search_result):
|
||||||
cover = None
|
cover = None
|
||||||
if search_result.cover:
|
if search_result.cover:
|
||||||
cover = "%s%s" % (self.covers_url, search_result.cover)
|
cover = f"{self.covers_url}{search_result.cover}"
|
||||||
|
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
title=search_result.title,
|
title=search_result.title,
|
||||||
|
|
|
@ -11,7 +11,7 @@ def email_data():
|
||||||
"""fields every email needs"""
|
"""fields every email needs"""
|
||||||
site = models.SiteSettings.objects.get()
|
site = models.SiteSettings.objects.get()
|
||||||
if site.logo_small:
|
if site.logo_small:
|
||||||
logo_path = "/images/{}".format(site.logo_small.url)
|
logo_path = f"/images/{site.logo_small.url}"
|
||||||
else:
|
else:
|
||||||
logo_path = "/static/images/logo-small.png"
|
logo_path = "/static/images/logo-small.png"
|
||||||
|
|
||||||
|
@ -48,18 +48,12 @@ def password_reset_email(reset_code):
|
||||||
|
|
||||||
def format_email(email_name, data):
|
def format_email(email_name, data):
|
||||||
"""render the email templates"""
|
"""render the email templates"""
|
||||||
subject = (
|
subject = get_template(f"email/{email_name}/subject.html").render(data).strip()
|
||||||
get_template("email/{}/subject.html".format(email_name)).render(data).strip()
|
|
||||||
)
|
|
||||||
html_content = (
|
html_content = (
|
||||||
get_template("email/{}/html_content.html".format(email_name))
|
get_template(f"email/{email_name}/html_content.html").render(data).strip()
|
||||||
.render(data)
|
|
||||||
.strip()
|
|
||||||
)
|
)
|
||||||
text_content = (
|
text_content = (
|
||||||
get_template("email/{}/text_content.html".format(email_name))
|
get_template(f"email/{email_name}/text_content.html").render(data).strip()
|
||||||
.render(data)
|
|
||||||
.strip()
|
|
||||||
)
|
)
|
||||||
return (subject, html_content, text_content)
|
return (subject, html_content, text_content)
|
||||||
|
|
||||||
|
|
|
@ -125,6 +125,12 @@ class StatusForm(CustomForm):
|
||||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class DirectForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Status
|
||||||
|
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
class EditUserForm(CustomForm):
|
class EditUserForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
|
@ -254,10 +260,7 @@ class CreateInviteForm(CustomForm):
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"use_limit": widgets.Select(
|
"use_limit": widgets.Select(
|
||||||
choices=[
|
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
|
||||||
(i, _("%(count)d uses" % {"count": i}))
|
|
||||||
for i in [1, 5, 10, 25, 50, 100]
|
|
||||||
]
|
|
||||||
+ [(None, _("Unlimited"))]
|
+ [(None, _("Unlimited"))]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -305,6 +308,12 @@ class EmailBlocklistForm(CustomForm):
|
||||||
fields = ["domain"]
|
fields = ["domain"]
|
||||||
|
|
||||||
|
|
||||||
|
class IPBlocklistForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.IPBlocklist
|
||||||
|
fields = ["address"]
|
||||||
|
|
||||||
|
|
||||||
class ServerForm(CustomForm):
|
class ServerForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FederatedServer
|
model = models.FederatedServer
|
||||||
|
|
|
@ -3,6 +3,7 @@ import csv
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.models import ImportJob, ImportItem
|
from bookwyrm.models import ImportJob, ImportItem
|
||||||
|
@ -71,19 +72,20 @@ def import_data(source, job_id):
|
||||||
item.resolve()
|
item.resolve()
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
logger.exception(err)
|
logger.exception(err)
|
||||||
item.fail_reason = "Error loading book"
|
item.fail_reason = _("Error loading book")
|
||||||
item.save()
|
item.save()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item.book:
|
if item.book or item.book_guess:
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
if item.book:
|
||||||
# shelves book and handles reviews
|
# shelves book and handles reviews
|
||||||
handle_imported_book(
|
handle_imported_book(
|
||||||
source, job.user, item, job.include_reviews, job.privacy
|
source, job.user, item, job.include_reviews, job.privacy
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
item.fail_reason = "Could not find a match for book"
|
item.fail_reason = _("Could not find a match for book")
|
||||||
item.save()
|
item.save()
|
||||||
finally:
|
finally:
|
||||||
job.complete = True
|
job.complete = True
|
||||||
|
@ -125,6 +127,7 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
# but "now" is a bad guess
|
# but "now" is a bad guess
|
||||||
published_date_guess = item.date_read or item.date_added
|
published_date_guess = item.date_read or item.date_added
|
||||||
if item.review:
|
if item.review:
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
review_title = (
|
review_title = (
|
||||||
"Review of {!r} on {!r}".format(
|
"Review of {!r} on {!r}".format(
|
||||||
item.book.title,
|
item.book.title,
|
||||||
|
|
3
bookwyrm/middleware/__init__.py
Normal file
3
bookwyrm/middleware/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
""" look at all this nice middleware! """
|
||||||
|
from .timezone_middleware import TimezoneMiddleware
|
||||||
|
from .ip_middleware import IPBlocklistMiddleware
|
16
bookwyrm/middleware/ip_middleware.py
Normal file
16
bookwyrm/middleware/ip_middleware.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
""" Block IP addresses """
|
||||||
|
from django.http import Http404
|
||||||
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
|
class IPBlocklistMiddleware:
|
||||||
|
"""check incoming traffic against an IP block-list"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
address = request.META.get("REMOTE_ADDR")
|
||||||
|
if models.IPBlocklist.objects.filter(address=address).exists():
|
||||||
|
raise Http404()
|
||||||
|
return self.get_response(request)
|
39
bookwyrm/migrations/0094_auto_20210911_1550.py
Normal file
39
bookwyrm/migrations/0094_auto_20210911_1550.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-11 15:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import F, Value, CharField
|
||||||
|
|
||||||
|
|
||||||
|
def set_deactivate_date(apps, schema_editor):
|
||||||
|
"""best-guess for deactivation date"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
apps.get_model("bookwyrm", "User").objects.using(db_alias).filter(
|
||||||
|
is_active=False
|
||||||
|
).update(deactivation_date=models.F("last_active_date"))
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_func(apps, schema_editor):
|
||||||
|
"""noop"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0093_alter_sitesettings_instance_short_description"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="deactivation_date",
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="saved_lists",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name="saved_lists", to="bookwyrm.List"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_deactivate_date, reverse_func),
|
||||||
|
]
|
25
bookwyrm/migrations/0094_importitem_book_guess.py
Normal file
25
bookwyrm/migrations/0094_importitem_book_guess.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-11 14:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0093_alter_sitesettings_instance_short_description"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="importitem",
|
||||||
|
name="book_guess",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="book_guess",
|
||||||
|
to="bookwyrm.book",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
45
bookwyrm/migrations/0095_auto_20210911_2053.py
Normal file
45
bookwyrm/migrations/0095_auto_20210911_2053.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-11 20:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0094_auto_20210911_1550"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="connector",
|
||||||
|
name="deactivation_reason",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("pending", "Pending"),
|
||||||
|
("self_deletion", "Self deletion"),
|
||||||
|
("moderator_suspension", "Moderator suspension"),
|
||||||
|
("moderator_deletion", "Moderator deletion"),
|
||||||
|
("domain_block", "Domain block"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="deactivation_reason",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("pending", "Pending"),
|
||||||
|
("self_deletion", "Self deletion"),
|
||||||
|
("moderator_suspension", "Moderator suspension"),
|
||||||
|
("moderator_deletion", "Moderator deletion"),
|
||||||
|
("domain_block", "Domain block"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0095_merge_20210911_2143.py
Normal file
13
bookwyrm/migrations/0095_merge_20210911_2143.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-11 21:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0094_auto_20210911_1550"),
|
||||||
|
("bookwyrm", "0094_importitem_book_guess"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
13
bookwyrm/migrations/0096_merge_20210912_0044.py
Normal file
13
bookwyrm/migrations/0096_merge_20210912_0044.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-12 00:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0095_auto_20210911_2053"),
|
||||||
|
("bookwyrm", "0095_merge_20210911_2143"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
38
bookwyrm/migrations/0097_auto_20210917_1858.py
Normal file
38
bookwyrm/migrations/0097_auto_20210917_1858.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-17 18:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0096_merge_20210912_0044"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="IPBlocklist",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("address", models.CharField(max_length=255, unique=True)),
|
||||||
|
("is_active", models.BooleanField(default=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("-created_date",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="emailblocklist",
|
||||||
|
name="is_active",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
27
bookwyrm/migrations/0098_auto_20210918_2238.py
Normal file
27
bookwyrm/migrations/0098_auto_20210918_2238.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-18 22:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0097_auto_20210917_1858"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sitesettings",
|
||||||
|
name="invite_request_text",
|
||||||
|
field=models.TextField(
|
||||||
|
default="If your request is approved, you will receive an email with a registration link."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sitesettings",
|
||||||
|
name="registration_closed_text",
|
||||||
|
field=models.TextField(
|
||||||
|
default='We aren\'t taking new users at this time. You can find an open instance at <a href="https://joinbookwyrm.com/instances">joinbookwyrm.com/instances</a>.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
37
bookwyrm/migrations/0099_readthrough_is_active.py
Normal file
37
bookwyrm/migrations/0099_readthrough_is_active.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-22 16:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_readthrough(apps, schema_editor):
|
||||||
|
"""best-guess for deactivation date"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
apps.get_model("bookwyrm", "ReadThrough").objects.using(db_alias).filter(
|
||||||
|
start_date__isnull=False,
|
||||||
|
finish_date__isnull=True,
|
||||||
|
).update(is_active=True)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_func(apps, schema_editor):
|
||||||
|
"""noop"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0098_auto_20210918_2238"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="readthrough",
|
||||||
|
name="is_active",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_active_readthrough, reverse_func),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="readthrough",
|
||||||
|
name="is_active",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,7 +14,6 @@ from .status import Review, ReviewRating
|
||||||
from .status import Boost
|
from .status import Boost
|
||||||
from .attachment import Image
|
from .attachment import Image
|
||||||
from .favorite import Favorite
|
from .favorite import Favorite
|
||||||
from .notification import Notification
|
|
||||||
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
|
from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
|
||||||
|
|
||||||
from .user import User, KeyPair, AnnualGoal
|
from .user import User, KeyPair, AnnualGoal
|
||||||
|
@ -26,8 +25,10 @@ from .import_job import ImportJob, ImportItem
|
||||||
|
|
||||||
from .site import SiteSettings, SiteInvite
|
from .site import SiteSettings, SiteInvite
|
||||||
from .site import PasswordReset, InviteRequest
|
from .site import PasswordReset, InviteRequest
|
||||||
from .site import EmailBlocklist
|
|
||||||
from .announcement import Announcement
|
from .announcement import Announcement
|
||||||
|
from .antispam import EmailBlocklist, IPBlocklist
|
||||||
|
|
||||||
|
from .notification import Notification
|
||||||
|
|
||||||
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||||
activity_models = {
|
activity_models = {
|
||||||
|
|
|
@ -266,7 +266,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
signed_message = signer.sign(SHA256.new(content.encode("utf8")))
|
signed_message = signer.sign(SHA256.new(content.encode("utf8")))
|
||||||
|
|
||||||
signature = activitypub.Signature(
|
signature = activitypub.Signature(
|
||||||
creator="%s#main-key" % user.remote_id,
|
creator=f"{user.remote_id}#main-key",
|
||||||
created=activity_object.published,
|
created=activity_object.published,
|
||||||
signatureValue=b64encode(signed_message).decode("utf8"),
|
signatureValue=b64encode(signed_message).decode("utf8"),
|
||||||
)
|
)
|
||||||
|
@ -285,16 +285,16 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
return activitypub.Delete(
|
return activitypub.Delete(
|
||||||
id=self.remote_id + "/activity",
|
id=self.remote_id + "/activity",
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
to=["%s/followers" % user.remote_id],
|
to=[f"{user.remote_id}/followers"],
|
||||||
cc=["https://www.w3.org/ns/activitystreams#Public"],
|
cc=["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
object=self,
|
object=self,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
def to_update_activity(self, user):
|
def to_update_activity(self, user):
|
||||||
"""wrapper for Updates to an activity"""
|
"""wrapper for Updates to an activity"""
|
||||||
activity_id = "%s#update/%s" % (self.remote_id, uuid4())
|
uuid = uuid4()
|
||||||
return activitypub.Update(
|
return activitypub.Update(
|
||||||
id=activity_id,
|
id=f"{self.remote_id}#update/{uuid}",
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
to=["https://www.w3.org/ns/activitystreams#Public"],
|
to=["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
object=self,
|
object=self,
|
||||||
|
@ -337,8 +337,8 @@ class OrderedCollectionPageMixin(ObjectMixin):
|
||||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
# add computed fields specific to orderd collections
|
# add computed fields specific to orderd collections
|
||||||
activity["totalItems"] = paginated.count
|
activity["totalItems"] = paginated.count
|
||||||
activity["first"] = "%s?page=1" % remote_id
|
activity["first"] = f"{remote_id}?page=1"
|
||||||
activity["last"] = "%s?page=%d" % (remote_id, paginated.num_pages)
|
activity["last"] = f"{remote_id}?page={paginated.num_pages}"
|
||||||
|
|
||||||
return serializer(**activity)
|
return serializer(**activity)
|
||||||
|
|
||||||
|
@ -420,7 +420,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||||
"""AP for shelving a book"""
|
"""AP for shelving a book"""
|
||||||
collection_field = getattr(self, self.collection_field)
|
collection_field = getattr(self, self.collection_field)
|
||||||
return activitypub.Add(
|
return activitypub.Add(
|
||||||
id="{:s}#add".format(collection_field.remote_id),
|
id=f"{collection_field.remote_id}#add",
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
object=self.to_activity_dataclass(),
|
object=self.to_activity_dataclass(),
|
||||||
target=collection_field.remote_id,
|
target=collection_field.remote_id,
|
||||||
|
@ -430,7 +430,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||||
"""AP for un-shelving a book"""
|
"""AP for un-shelving a book"""
|
||||||
collection_field = getattr(self, self.collection_field)
|
collection_field = getattr(self, self.collection_field)
|
||||||
return activitypub.Remove(
|
return activitypub.Remove(
|
||||||
id="{:s}#remove".format(collection_field.remote_id),
|
id=f"{collection_field.remote_id}#remove",
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
object=self.to_activity_dataclass(),
|
object=self.to_activity_dataclass(),
|
||||||
target=collection_field.remote_id,
|
target=collection_field.remote_id,
|
||||||
|
@ -458,7 +458,7 @@ class ActivityMixin(ActivitypubMixin):
|
||||||
"""undo an action"""
|
"""undo an action"""
|
||||||
user = self.user if hasattr(self, "user") else self.user_subject
|
user = self.user if hasattr(self, "user") else self.user_subject
|
||||||
return activitypub.Undo(
|
return activitypub.Undo(
|
||||||
id="%s#undo" % self.remote_id,
|
id=f"{self.remote_id}#undo",
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
object=self,
|
object=self,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
@ -555,11 +555,11 @@ def to_ordered_collection_page(
|
||||||
|
|
||||||
prev_page = next_page = None
|
prev_page = next_page = None
|
||||||
if activity_page.has_next():
|
if activity_page.has_next():
|
||||||
next_page = "%s?page=%d" % (remote_id, activity_page.next_page_number())
|
next_page = f"{remote_id}?page={activity_page.next_page_number()}"
|
||||||
if activity_page.has_previous():
|
if activity_page.has_previous():
|
||||||
prev_page = "%s?page=%d" % (remote_id, activity_page.previous_page_number())
|
prev_page = f"{remote_id}?page=%d{activity_page.previous_page_number()}"
|
||||||
return activitypub.OrderedCollectionPage(
|
return activitypub.OrderedCollectionPage(
|
||||||
id="%s?page=%s" % (remote_id, page),
|
id=f"{remote_id}?page={page}",
|
||||||
partOf=remote_id,
|
partOf=remote_id,
|
||||||
orderedItems=items,
|
orderedItems=items,
|
||||||
next=next_page,
|
next=next_page,
|
||||||
|
|
35
bookwyrm/models/antispam.py
Normal file
35
bookwyrm/models/antispam.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
""" Lets try NOT to sell viagra """
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class EmailBlocklist(models.Model):
|
||||||
|
"""blocked email addresses"""
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
domain = models.CharField(max_length=255, unique=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""default sorting"""
|
||||||
|
|
||||||
|
ordering = ("-created_date",)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self):
|
||||||
|
"""find the users associated with this address"""
|
||||||
|
return User.objects.filter(email__endswith=f"@{self.domain}")
|
||||||
|
|
||||||
|
|
||||||
|
class IPBlocklist(models.Model):
|
||||||
|
"""blocked ip addresses"""
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
address = models.CharField(max_length=255, unique=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""default sorting"""
|
||||||
|
|
||||||
|
ordering = ("-created_date",)
|
|
@ -35,7 +35,7 @@ class Author(BookDataModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
return "https://%s/author/%s" % (DOMAIN, self.id)
|
return f"https://{DOMAIN}/author/{self.id}"
|
||||||
|
|
||||||
activity_serializer = activitypub.Author
|
activity_serializer = activitypub.Author
|
||||||
|
|
||||||
|
|
|
@ -3,21 +3,19 @@ import base64
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .fields import RemoteIdField
|
from .fields import RemoteIdField
|
||||||
|
|
||||||
|
|
||||||
DeactivationReason = models.TextChoices(
|
DeactivationReason = [
|
||||||
"DeactivationReason",
|
("pending", _("Pending")),
|
||||||
[
|
("self_deletion", _("Self deletion")),
|
||||||
"pending",
|
("moderator_suspension", _("Moderator suspension")),
|
||||||
"self_deletion",
|
("moderator_deletion", _("Moderator deletion")),
|
||||||
"moderator_suspension",
|
("domain_block", _("Domain block")),
|
||||||
"moderator_deletion",
|
]
|
||||||
"domain_block",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def new_access_code():
|
def new_access_code():
|
||||||
|
@ -34,11 +32,11 @@ class BookWyrmModel(models.Model):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""generate a url that resolves to the local object"""
|
"""generate a url that resolves to the local object"""
|
||||||
base_path = "https://%s" % DOMAIN
|
base_path = f"https://{DOMAIN}"
|
||||||
if hasattr(self, "user"):
|
if hasattr(self, "user"):
|
||||||
base_path = "%s%s" % (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 "%s/%s/%d" % (base_path, model_name, self.id)
|
return f"{base_path}/{model_name}/{self.id}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""this is just here to provide default fields for other models"""
|
"""this is just here to provide default fields for other models"""
|
||||||
|
@ -48,7 +46,7 @@ class BookWyrmModel(models.Model):
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
"""how to link to this object in the local app"""
|
"""how to link to this object in the local app"""
|
||||||
return self.get_remote_id().replace("https://%s" % DOMAIN, "")
|
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||||
|
|
||||||
def visible_to_user(self, viewer):
|
def visible_to_user(self, viewer):
|
||||||
"""is a user authorized to view an object?"""
|
"""is a user authorized to view an object?"""
|
||||||
|
|
|
@ -3,8 +3,8 @@ import re
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
from django.db import transaction
|
from django.db.models import Prefetch
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from model_utils import FieldTracker
|
from model_utils import FieldTracker
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
|
@ -164,9 +164,9 @@ class Book(BookDataModel):
|
||||||
@property
|
@property
|
||||||
def alt_text(self):
|
def alt_text(self):
|
||||||
"""image alt test"""
|
"""image alt test"""
|
||||||
text = "%s" % self.title
|
text = self.title
|
||||||
if self.edition_info:
|
if self.edition_info:
|
||||||
text += " (%s)" % self.edition_info
|
text += f" ({self.edition_info})"
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -177,9 +177,10 @@ class Book(BookDataModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
return "https://%s/book/%d" % (DOMAIN, self.id)
|
return f"https://{DOMAIN}/book/{self.id}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
return "<{} key={!r} title={!r}>".format(
|
return "<{} key={!r} title={!r}>".format(
|
||||||
self.__class__,
|
self.__class__,
|
||||||
self.openlibrary_key,
|
self.openlibrary_key,
|
||||||
|
@ -216,7 +217,7 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||||
"""an ordered collection of editions"""
|
"""an ordered collection of editions"""
|
||||||
return self.to_ordered_collection(
|
return self.to_ordered_collection(
|
||||||
self.editions.order_by("-edition_rank").all(),
|
self.editions.order_by("-edition_rank").all(),
|
||||||
remote_id="%s/editions" % self.remote_id,
|
remote_id=f"{self.remote_id}/editions",
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -306,6 +307,27 @@ class Edition(Book):
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def viewer_aware_objects(cls, viewer):
|
||||||
|
"""annotate a book query with metadata related to the user"""
|
||||||
|
queryset = cls.objects
|
||||||
|
if not viewer or not viewer.is_authenticated:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
queryset = queryset.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"shelfbook_set",
|
||||||
|
queryset=viewer.shelfbook_set.all(),
|
||||||
|
to_attr="current_shelves",
|
||||||
|
),
|
||||||
|
Prefetch(
|
||||||
|
"readthrough_set",
|
||||||
|
queryset=viewer.readthrough_set.filter(is_active=True).all(),
|
||||||
|
to_attr="active_readthroughs",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def isbn_10_to_13(isbn_10):
|
def isbn_10_to_13(isbn_10):
|
||||||
"""convert an isbn 10 into an isbn 13"""
|
"""convert an isbn 10 into an isbn 13"""
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Connector(BookWyrmModel):
|
||||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
deactivation_reason = models.CharField(
|
deactivation_reason = models.CharField(
|
||||||
max_length=255, choices=DeactivationReason.choices, null=True, blank=True
|
max_length=255, choices=DeactivationReason, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
base_url = models.CharField(max_length=255)
|
base_url = models.CharField(max_length=255)
|
||||||
|
@ -29,7 +29,4 @@ class Connector(BookWyrmModel):
|
||||||
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
|
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} ({})".format(
|
return f"{self.identifier} ({self.id})"
|
||||||
self.identifier,
|
|
||||||
self.id,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
""" like/fav/star a status """
|
""" like/fav/star a status """
|
||||||
from django.apps import apps
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .activitypub_mixin import ActivityMixin
|
from .activitypub_mixin import ActivityMixin
|
||||||
|
@ -29,38 +27,9 @@ class Favorite(ActivityMixin, BookWyrmModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""update user active time"""
|
"""update user active time"""
|
||||||
self.user.last_active_date = timezone.now()
|
self.user.update_active_date()
|
||||||
self.user.save(broadcast=False, update_fields=["last_active_date"])
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.status.user.local and self.status.user != self.user:
|
|
||||||
notification_model = apps.get_model(
|
|
||||||
"bookwyrm.Notification", require_ready=True
|
|
||||||
)
|
|
||||||
notification_model.objects.create(
|
|
||||||
user=self.status.user,
|
|
||||||
notification_type="FAVORITE",
|
|
||||||
related_user=self.user,
|
|
||||||
related_status=self.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
"""delete and delete notifications"""
|
|
||||||
# check for notification
|
|
||||||
if self.status.user.local:
|
|
||||||
notification_model = apps.get_model(
|
|
||||||
"bookwyrm.Notification", require_ready=True
|
|
||||||
)
|
|
||||||
notification = notification_model.objects.filter(
|
|
||||||
user=self.status.user,
|
|
||||||
related_user=self.user,
|
|
||||||
related_status=self.status,
|
|
||||||
notification_type="FAVORITE",
|
|
||||||
).first()
|
|
||||||
if notification:
|
|
||||||
notification.delete()
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""can't fav things twice"""
|
"""can't fav things twice"""
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
""" connections to external ActivityPub servers """
|
""" connections to external ActivityPub servers """
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
FederationStatus = models.TextChoices(
|
FederationStatus = [
|
||||||
"Status",
|
("federated", _("Federated")),
|
||||||
[
|
("blocked", _("Blocked")),
|
||||||
"federated",
|
]
|
||||||
"blocked",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FederatedServer(BookWyrmModel):
|
class FederatedServer(BookWyrmModel):
|
||||||
|
@ -18,7 +18,7 @@ class FederatedServer(BookWyrmModel):
|
||||||
|
|
||||||
server_name = models.CharField(max_length=255, unique=True)
|
server_name = models.CharField(max_length=255, unique=True)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=255, default="federated", choices=FederationStatus.choices
|
max_length=255, default="federated", choices=FederationStatus
|
||||||
)
|
)
|
||||||
# is it mastodon, bookwyrm, etc
|
# is it mastodon, bookwyrm, etc
|
||||||
application_type = models.CharField(max_length=255, null=True, blank=True)
|
application_type = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
|
@ -56,7 +56,7 @@ class ActivitypubFieldMixin:
|
||||||
activitypub_field=None,
|
activitypub_field=None,
|
||||||
activitypub_wrapper=None,
|
activitypub_wrapper=None,
|
||||||
deduplication_field=False,
|
deduplication_field=False,
|
||||||
**kwargs
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.deduplication_field = deduplication_field
|
self.deduplication_field = deduplication_field
|
||||||
if activitypub_wrapper:
|
if activitypub_wrapper:
|
||||||
|
@ -308,7 +308,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
if self.link_only:
|
if self.link_only:
|
||||||
return "%s/%s" % (value.instance.remote_id, self.name)
|
return f"{value.instance.remote_id}/{self.name}"
|
||||||
return [i.remote_id for i in value.all()]
|
return [i.remote_id for i in value.all()]
|
||||||
|
|
||||||
def field_from_activity(self, value):
|
def field_from_activity(self, value):
|
||||||
|
@ -388,7 +388,7 @@ def image_serializer(value, alt):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
if not url[:4] == "http":
|
if not url[:4] == "http":
|
||||||
url = "https://{:s}{:s}".format(DOMAIN, url)
|
url = f"https://{DOMAIN}{url}"
|
||||||
return activitypub.Document(url=url, name=alt)
|
return activitypub.Document(url=url, name=alt)
|
||||||
|
|
||||||
|
|
||||||
|
@ -448,7 +448,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
|
|
||||||
image_content = ContentFile(response.content)
|
image_content = ContentFile(response.content)
|
||||||
extension = imghdr.what(None, image_content.read()) or ""
|
extension = imghdr.what(None, image_content.read()) or ""
|
||||||
image_name = "{:s}.{:s}".format(str(uuid4()), extension)
|
image_name = f"{uuid4()}.{extension}"
|
||||||
return [image_name, image_content]
|
return [image_name, image_content]
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import re
|
import re
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -50,19 +49,6 @@ class ImportJob(models.Model):
|
||||||
)
|
)
|
||||||
retry = models.BooleanField(default=False)
|
retry = models.BooleanField(default=False)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""save and notify"""
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
if self.complete:
|
|
||||||
notification_model = apps.get_model(
|
|
||||||
"bookwyrm.Notification", require_ready=True
|
|
||||||
)
|
|
||||||
notification_model.objects.create(
|
|
||||||
user=self.user,
|
|
||||||
notification_type="IMPORT",
|
|
||||||
related_import=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ImportItem(models.Model):
|
class ImportItem(models.Model):
|
||||||
"""a single line of a csv being imported"""
|
"""a single line of a csv being imported"""
|
||||||
|
@ -71,6 +57,13 @@ class ImportItem(models.Model):
|
||||||
index = models.IntegerField()
|
index = models.IntegerField()
|
||||||
data = models.JSONField()
|
data = models.JSONField()
|
||||||
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True)
|
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
book_guess = models.ForeignKey(
|
||||||
|
Book,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="book_guess",
|
||||||
|
)
|
||||||
fail_reason = models.TextField(null=True)
|
fail_reason = models.TextField(null=True)
|
||||||
|
|
||||||
def resolve(self):
|
def resolve(self):
|
||||||
|
@ -78,9 +71,13 @@ class ImportItem(models.Model):
|
||||||
if self.isbn:
|
if self.isbn:
|
||||||
self.book = self.get_book_from_isbn()
|
self.book = self.get_book_from_isbn()
|
||||||
else:
|
else:
|
||||||
# don't fall back on title/author search is isbn is present.
|
# don't fall back on title/author search if isbn is present.
|
||||||
# you're too likely to mismatch
|
# you're too likely to mismatch
|
||||||
self.book = self.get_book_from_title_author()
|
book, confidence = self.get_book_from_title_author()
|
||||||
|
if confidence > 0.999:
|
||||||
|
self.book = book
|
||||||
|
else:
|
||||||
|
self.book_guess = book
|
||||||
|
|
||||||
def get_book_from_isbn(self):
|
def get_book_from_isbn(self):
|
||||||
"""search by isbn"""
|
"""search by isbn"""
|
||||||
|
@ -96,12 +93,15 @@ class ImportItem(models.Model):
|
||||||
"""search by title and author"""
|
"""search by title and author"""
|
||||||
search_term = construct_search_term(self.title, self.author)
|
search_term = construct_search_term(self.title, self.author)
|
||||||
search_result = connector_manager.first_search_result(
|
search_result = connector_manager.first_search_result(
|
||||||
search_term, min_confidence=0.999
|
search_term, min_confidence=0.1
|
||||||
)
|
)
|
||||||
if search_result:
|
if search_result:
|
||||||
# raises ConnectorException
|
# raises ConnectorException
|
||||||
return search_result.connector.get_or_create_book(search_result.key)
|
return (
|
||||||
return None
|
search_result.connector.get_or_create_book(search_result.key),
|
||||||
|
search_result.confidence,
|
||||||
|
)
|
||||||
|
return None, 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
|
@ -184,7 +184,9 @@ class ImportItem(models.Model):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
return "<{!r}Item {!r}>".format(self.data["import_source"], self.data["Title"])
|
return "<{!r}Item {!r}>".format(self.data["import_source"], self.data["Title"])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
return "{} by {}".format(self.data["Title"], self.data["Author"])
|
return "{} by {}".format(self.data["Title"], self.data["Author"])
|
||||||
|
|
|
@ -42,7 +42,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""don't want the user to be in there in this case"""
|
"""don't want the user to be in there in this case"""
|
||||||
return "https://%s/list/%d" % (DOMAIN, self.id)
|
return f"https://{DOMAIN}/list/{self.id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
""" alert a user to activity """
|
""" alert a user to activity """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.dispatch import receiver
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
from . import Boost, Favorite, ImportJob, Report, Status, User
|
||||||
|
|
||||||
|
|
||||||
NotificationType = models.TextChoices(
|
NotificationType = models.TextChoices(
|
||||||
|
@ -53,3 +55,127 @@ class Notification(BookWyrmModel):
|
||||||
name="notification_type_valid",
|
name="notification_type_valid",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=Favorite)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_on_fav(sender, instance, *args, **kwargs):
|
||||||
|
"""someone liked your content, you ARE loved"""
|
||||||
|
if not instance.status.user.local or instance.status.user == instance.user:
|
||||||
|
return
|
||||||
|
Notification.objects.create(
|
||||||
|
user=instance.status.user,
|
||||||
|
notification_type="FAVORITE",
|
||||||
|
related_user=instance.user,
|
||||||
|
related_status=instance.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_delete, sender=Favorite)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_on_unfav(sender, instance, *args, **kwargs):
|
||||||
|
"""oops, didn't like that after all"""
|
||||||
|
if not instance.status.user.local:
|
||||||
|
return
|
||||||
|
Notification.objects.filter(
|
||||||
|
user=instance.status.user,
|
||||||
|
related_user=instance.user,
|
||||||
|
related_status=instance.status,
|
||||||
|
notification_type="FAVORITE",
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_user_on_mention(sender, instance, *args, **kwargs):
|
||||||
|
"""creating and deleting statuses with @ mentions and replies"""
|
||||||
|
if not issubclass(sender, Status):
|
||||||
|
return
|
||||||
|
|
||||||
|
if instance.deleted:
|
||||||
|
Notification.objects.filter(related_status=instance).delete()
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
instance.reply_parent
|
||||||
|
and instance.reply_parent.user != instance.user
|
||||||
|
and instance.reply_parent.user.local
|
||||||
|
):
|
||||||
|
Notification.objects.create(
|
||||||
|
user=instance.reply_parent.user,
|
||||||
|
notification_type="REPLY",
|
||||||
|
related_user=instance.user,
|
||||||
|
related_status=instance,
|
||||||
|
)
|
||||||
|
for mention_user in instance.mention_users.all():
|
||||||
|
# avoid double-notifying about this status
|
||||||
|
if not mention_user.local or (
|
||||||
|
instance.reply_parent and mention_user == instance.reply_parent.user
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
Notification.objects.create(
|
||||||
|
user=mention_user,
|
||||||
|
notification_type="MENTION",
|
||||||
|
related_user=instance.user,
|
||||||
|
related_status=instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=Boost)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_user_on_boost(sender, instance, *args, **kwargs):
|
||||||
|
"""boosting a status"""
|
||||||
|
if (
|
||||||
|
not instance.boosted_status.user.local
|
||||||
|
or instance.boosted_status.user == instance.user
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
Notification.objects.create(
|
||||||
|
user=instance.boosted_status.user,
|
||||||
|
related_status=instance.boosted_status,
|
||||||
|
related_user=instance.user,
|
||||||
|
notification_type="BOOST",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_delete, sender=Boost)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_user_on_unboost(sender, instance, *args, **kwargs):
|
||||||
|
"""unboosting a status"""
|
||||||
|
Notification.objects.filter(
|
||||||
|
user=instance.boosted_status.user,
|
||||||
|
related_status=instance.boosted_status,
|
||||||
|
related_user=instance.user,
|
||||||
|
notification_type="BOOST",
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=ImportJob)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_user_on_import_complete(sender, instance, *args, **kwargs):
|
||||||
|
"""we imported your books! aren't you proud of us"""
|
||||||
|
if not instance.complete:
|
||||||
|
return
|
||||||
|
Notification.objects.create(
|
||||||
|
user=instance.user,
|
||||||
|
notification_type="IMPORT",
|
||||||
|
related_import=instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=Report)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def notify_admins_on_report(sender, instance, *args, **kwargs):
|
||||||
|
"""something is up, make sure the admins know"""
|
||||||
|
# moderators and superusers should be notified
|
||||||
|
admins = User.objects.filter(
|
||||||
|
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
|
||||||
|
| models.Q(is_superuser=True)
|
||||||
|
).all()
|
||||||
|
for admin in admins:
|
||||||
|
Notification.objects.create(
|
||||||
|
user=admin,
|
||||||
|
related_report=instance,
|
||||||
|
notification_type="REPORT",
|
||||||
|
)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
@ -27,11 +26,14 @@ 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)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""update user active time"""
|
"""update user active time"""
|
||||||
self.user.last_active_date = timezone.now()
|
self.user.update_active_date()
|
||||||
self.user.save(broadcast=False, update_fields=["last_active_date"])
|
# an active readthrough must have an unset finish date
|
||||||
|
if self.finish_date:
|
||||||
|
self.is_active = False
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def create_update(self):
|
def create_update(self):
|
||||||
|
@ -65,6 +67,5 @@ class ProgressUpdate(BookWyrmModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""update user active time"""
|
"""update user active time"""
|
||||||
self.user.last_active_date = timezone.now()
|
self.user.update_active_date()
|
||||||
self.user.save(broadcast=False, update_fields=["last_active_date"])
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
|
@ -53,7 +53,7 @@ class UserRelationship(BookWyrmModel):
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""use shelf identifier in remote_id"""
|
"""use shelf identifier in remote_id"""
|
||||||
base_path = self.user_subject.remote_id
|
base_path = self.user_subject.remote_id
|
||||||
return "%s#follows/%d" % (base_path, self.id)
|
return f"{base_path}#follows/{self.id}"
|
||||||
|
|
||||||
|
|
||||||
class UserFollows(ActivityMixin, UserRelationship):
|
class UserFollows(ActivityMixin, UserRelationship):
|
||||||
|
@ -144,7 +144,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
"""get id for sending an accept or reject of a local user"""
|
"""get id for sending an accept or reject of a local user"""
|
||||||
|
|
||||||
base_path = self.user_object.remote_id
|
base_path = self.user_object.remote_id
|
||||||
return "%s#%s/%d" % (base_path, status, self.id or 0)
|
status_id = self.id or 0
|
||||||
|
return f"{base_path}#{status}/{status_id}"
|
||||||
|
|
||||||
def accept(self, broadcast_only=False):
|
def accept(self, broadcast_only=False):
|
||||||
"""turn this request into the real deal"""
|
"""turn this request into the real deal"""
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
""" flagged for moderation """
|
""" flagged for moderation """
|
||||||
from django.apps import apps
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -16,23 +15,6 @@ class Report(BookWyrmModel):
|
||||||
statuses = models.ManyToManyField("Status", blank=True)
|
statuses = models.ManyToManyField("Status", blank=True)
|
||||||
resolved = models.BooleanField(default=False)
|
resolved = models.BooleanField(default=False)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""notify admins when a report is created"""
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
|
||||||
# moderators and superusers should be notified
|
|
||||||
admins = user_model.objects.filter(
|
|
||||||
Q(user_permissions__name__in=["moderate_user", "moderate_post"])
|
|
||||||
| Q(is_superuser=True)
|
|
||||||
).all()
|
|
||||||
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
|
||||||
for admin in admins:
|
|
||||||
notification_model.objects.create(
|
|
||||||
user=admin,
|
|
||||||
related_report=self,
|
|
||||||
notification_type="REPORT",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""don't let users report themselves"""
|
"""don't let users report themselves"""
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
def get_identifier(self):
|
def get_identifier(self):
|
||||||
"""custom-shelf-123 for the url"""
|
"""custom-shelf-123 for the url"""
|
||||||
slug = re.sub(r"[^\w]", "", self.name).lower()
|
slug = re.sub(r"[^\w]", "", self.name).lower()
|
||||||
return "{:s}-{:d}".format(slug, self.id)
|
return f"{slug}-{self.id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
|
@ -55,7 +55,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
"""shelf identifier instead of id"""
|
"""shelf identifier instead of id"""
|
||||||
base_path = self.user.remote_id
|
base_path = self.user.remote_id
|
||||||
identifier = self.identifier or self.get_identifier()
|
identifier = self.identifier or self.get_identifier()
|
||||||
return "%s/books/%s" % (base_path, identifier)
|
return f"{base_path}/books/{identifier}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""user/shelf unqiueness"""
|
"""user/shelf unqiueness"""
|
||||||
|
|
|
@ -24,7 +24,13 @@ class SiteSettings(models.Model):
|
||||||
|
|
||||||
# about page
|
# about page
|
||||||
registration_closed_text = models.TextField(
|
registration_closed_text = models.TextField(
|
||||||
default="Contact an administrator to get an invite"
|
default="We aren't taking new users at this time. You can find an open "
|
||||||
|
'instance at <a href="https://joinbookwyrm.com/instances">'
|
||||||
|
"joinbookwyrm.com/instances</a>."
|
||||||
|
)
|
||||||
|
invite_request_text = models.TextField(
|
||||||
|
default="If your request is approved, you will receive an email with a "
|
||||||
|
"registration link."
|
||||||
)
|
)
|
||||||
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
||||||
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
||||||
|
@ -81,7 +87,7 @@ class SiteInvite(models.Model):
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
"""formats the invite link"""
|
"""formats the invite link"""
|
||||||
return "https://{}/invite/{}".format(DOMAIN, self.code)
|
return f"https://{DOMAIN}/invite/{self.code}"
|
||||||
|
|
||||||
|
|
||||||
class InviteRequest(BookWyrmModel):
|
class InviteRequest(BookWyrmModel):
|
||||||
|
@ -121,24 +127,7 @@ class PasswordReset(models.Model):
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
"""formats the invite link"""
|
"""formats the invite link"""
|
||||||
return "https://{}/password-reset/{}".format(DOMAIN, self.code)
|
return f"https://{DOMAIN}/password-reset/{self.code}"
|
||||||
|
|
||||||
|
|
||||||
class EmailBlocklist(models.Model):
|
|
||||||
"""blocked email addresses"""
|
|
||||||
|
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
domain = models.CharField(max_length=255, unique=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""default sorting"""
|
|
||||||
|
|
||||||
ordering = ("-created_date",)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def users(self):
|
|
||||||
"""find the users associated with this address"""
|
|
||||||
return User.objects.filter(email__endswith=f"@{self.domain}")
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
|
@ -67,40 +67,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
|
|
||||||
ordering = ("-published_date",)
|
ordering = ("-published_date",)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""save and notify"""
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
|
||||||
|
|
||||||
if self.deleted:
|
|
||||||
notification_model.objects.filter(related_status=self).delete()
|
|
||||||
return
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.reply_parent
|
|
||||||
and self.reply_parent.user != self.user
|
|
||||||
and self.reply_parent.user.local
|
|
||||||
):
|
|
||||||
notification_model.objects.create(
|
|
||||||
user=self.reply_parent.user,
|
|
||||||
notification_type="REPLY",
|
|
||||||
related_user=self.user,
|
|
||||||
related_status=self,
|
|
||||||
)
|
|
||||||
for mention_user in self.mention_users.all():
|
|
||||||
# avoid double-notifying about this status
|
|
||||||
if not mention_user.local or (
|
|
||||||
self.reply_parent and mention_user == self.reply_parent.user
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
notification_model.objects.create(
|
|
||||||
user=mention_user,
|
|
||||||
notification_type="MENTION",
|
|
||||||
related_user=self.user,
|
|
||||||
related_status=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||||
""" "delete" a status"""
|
""" "delete" a status"""
|
||||||
if hasattr(self, "boosted_status"):
|
if hasattr(self, "boosted_status"):
|
||||||
|
@ -108,6 +74,10 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
return
|
return
|
||||||
self.deleted = True
|
self.deleted = True
|
||||||
|
# clear user content
|
||||||
|
self.content = None
|
||||||
|
if hasattr(self, "quotation"):
|
||||||
|
self.quotation = None # pylint: disable=attribute-defined-outside-init
|
||||||
self.deleted_date = timezone.now()
|
self.deleted_date = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@ -179,9 +149,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
"""helper function for loading AP serialized replies to a status"""
|
"""helper function for loading AP serialized replies to a status"""
|
||||||
return self.to_ordered_collection(
|
return self.to_ordered_collection(
|
||||||
self.replies(self),
|
self.replies(self),
|
||||||
remote_id="%s/replies" % self.remote_id,
|
remote_id=f"{self.remote_id}/replies",
|
||||||
collection_only=True,
|
collection_only=True,
|
||||||
**kwargs
|
**kwargs,
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ
|
def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ
|
||||||
|
@ -226,10 +196,10 @@ 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(
|
||||||
'<a href="%s">"%s"</a>' % (book.remote_id, book.title)
|
f'<a href="{book.remote_id}">"{book.title}"</a>'
|
||||||
for book in self.mention_books.all()
|
for book in self.mention_books.all()
|
||||||
)
|
)
|
||||||
return "%s %s %s" % (self.user.display_name, message, books)
|
return f"{self.user.display_name} {message} {books}"
|
||||||
|
|
||||||
activity_serializer = activitypub.GeneratedNote
|
activity_serializer = activitypub.GeneratedNote
|
||||||
pure_type = "Note"
|
pure_type = "Note"
|
||||||
|
@ -277,10 +247,9 @@ 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"""
|
||||||
return '%s<p>(comment on <a href="%s">"%s"</a>)</p>' % (
|
return (
|
||||||
self.content,
|
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
||||||
self.book.remote_id,
|
f'"{self.book.title}"</a>)</p>'
|
||||||
self.book.title,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Comment
|
activity_serializer = activitypub.Comment
|
||||||
|
@ -306,11 +275,9 @@ 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)
|
||||||
return '%s <p>-- <a href="%s">"%s"</a></p>%s' % (
|
return (
|
||||||
quote,
|
f'{quote} <p>-- <a href="{self.book.remote_id}">'
|
||||||
self.book.remote_id,
|
f'"{self.book.title}"</a></p>{self.content}'
|
||||||
self.book.title,
|
|
||||||
self.content,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_serializer = activitypub.Quotation
|
activity_serializer = activitypub.Quotation
|
||||||
|
@ -389,27 +356,6 @@ class Boost(ActivityMixin, Status):
|
||||||
return
|
return
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if not self.boosted_status.user.local or self.boosted_status.user == self.user:
|
|
||||||
return
|
|
||||||
|
|
||||||
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
|
||||||
notification_model.objects.create(
|
|
||||||
user=self.boosted_status.user,
|
|
||||||
related_status=self.boosted_status,
|
|
||||||
related_user=self.user,
|
|
||||||
notification_type="BOOST",
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
"""delete and un-notify"""
|
|
||||||
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
|
||||||
notification_model.objects.filter(
|
|
||||||
user=self.boosted_status.user,
|
|
||||||
related_status=self.boosted_status,
|
|
||||||
related_user=self.user,
|
|
||||||
notification_type="BOOST",
|
|
||||||
).delete()
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""the user field is "actor" here instead of "attributedTo" """
|
"""the user field is "actor" here instead of "attributedTo" """
|
||||||
|
@ -422,10 +368,6 @@ class Boost(ActivityMixin, Status):
|
||||||
self.image_fields = []
|
self.image_fields = []
|
||||||
self.deserialize_reverse_fields = []
|
self.deserialize_reverse_fields = []
|
||||||
|
|
||||||
# This constraint can't work as it would cross tables.
|
|
||||||
# class Meta:
|
|
||||||
# unique_together = ('user', 'boosted_status')
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@receiver(models.signals.post_save)
|
@receiver(models.signals.post_save)
|
||||||
|
|
|
@ -105,7 +105,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
related_name="blocked_by",
|
related_name="blocked_by",
|
||||||
)
|
)
|
||||||
saved_lists = models.ManyToManyField(
|
saved_lists = models.ManyToManyField(
|
||||||
"List", symmetrical=False, related_name="saved_lists"
|
"List", symmetrical=False, related_name="saved_lists", blank=True
|
||||||
)
|
)
|
||||||
favorites = models.ManyToManyField(
|
favorites = models.ManyToManyField(
|
||||||
"Status",
|
"Status",
|
||||||
|
@ -134,8 +134,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
max_length=255,
|
max_length=255,
|
||||||
)
|
)
|
||||||
deactivation_reason = models.CharField(
|
deactivation_reason = models.CharField(
|
||||||
max_length=255, choices=DeactivationReason.choices, null=True, blank=True
|
max_length=255, choices=DeactivationReason, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
deactivation_date = models.DateTimeField(null=True, blank=True)
|
||||||
confirmation_code = models.CharField(max_length=32, default=new_access_code)
|
confirmation_code = models.CharField(max_length=32, default=new_access_code)
|
||||||
|
|
||||||
name_field = "username"
|
name_field = "username"
|
||||||
|
@ -151,12 +152,13 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
@property
|
@property
|
||||||
def following_link(self):
|
def following_link(self):
|
||||||
"""just how to find out the following info"""
|
"""just how to find out the following info"""
|
||||||
return "{:s}/following".format(self.remote_id)
|
return f"{self.remote_id}/following"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alt_text(self):
|
def alt_text(self):
|
||||||
"""alt text with username"""
|
"""alt text with username"""
|
||||||
return "avatar for %s" % (self.localname or self.username)
|
# pylint: disable=consider-using-f-string
|
||||||
|
return "avatar for {:s}".format(self.localname or self.username)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
|
@ -193,12 +195,15 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
queryset = queryset.exclude(blocks=viewer)
|
queryset = queryset.exclude(blocks=viewer)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def update_active_date(self):
|
||||||
|
"""this user is here! they are doing things!"""
|
||||||
|
self.last_active_date = timezone.now()
|
||||||
|
self.save(broadcast=False, update_fields=["last_active_date"])
|
||||||
|
|
||||||
def to_outbox(self, filter_type=None, **kwargs):
|
def to_outbox(self, filter_type=None, **kwargs):
|
||||||
"""an ordered collection of statuses"""
|
"""an ordered collection of statuses"""
|
||||||
if filter_type:
|
if filter_type:
|
||||||
filter_class = apps.get_model(
|
filter_class = apps.get_model(f"bookwyrm.{filter_type}", require_ready=True)
|
||||||
"bookwyrm.%s" % filter_type, require_ready=True
|
|
||||||
)
|
|
||||||
if not issubclass(filter_class, Status):
|
if not issubclass(filter_class, Status):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"filter_status_class must be a subclass of models.Status"
|
"filter_status_class must be a subclass of models.Status"
|
||||||
|
@ -222,7 +227,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
|
|
||||||
def to_following_activity(self, **kwargs):
|
def to_following_activity(self, **kwargs):
|
||||||
"""activitypub following list"""
|
"""activitypub following list"""
|
||||||
remote_id = "%s/following" % self.remote_id
|
remote_id = f"{self.remote_id}/following"
|
||||||
return self.to_ordered_collection(
|
return self.to_ordered_collection(
|
||||||
self.following.order_by("-updated_date").all(),
|
self.following.order_by("-updated_date").all(),
|
||||||
remote_id=remote_id,
|
remote_id=remote_id,
|
||||||
|
@ -265,10 +270,15 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
|
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
|
||||||
# generate a username that uses the domain (webfinger format)
|
# generate a username that uses the domain (webfinger format)
|
||||||
actor_parts = urlparse(self.remote_id)
|
actor_parts = urlparse(self.remote_id)
|
||||||
self.username = "%s@%s" % (self.username, actor_parts.netloc)
|
self.username = f"{self.username}@{actor_parts.netloc}"
|
||||||
|
|
||||||
# this user already exists, no need to populate fields
|
# this user already exists, no need to populate fields
|
||||||
if not created:
|
if not created:
|
||||||
|
if self.is_active:
|
||||||
|
self.deactivation_date = None
|
||||||
|
elif not self.deactivation_date:
|
||||||
|
self.deactivation_date = timezone.now()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -314,7 +324,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
"""this model doesn't inherit bookwyrm model, so here we are"""
|
"""this model doesn't inherit bookwyrm model, so here we are"""
|
||||||
return "/user/%s" % (self.localname or self.username)
|
# pylint: disable=consider-using-f-string
|
||||||
|
return "/user/{:s}".format(self.localname or self.username)
|
||||||
|
|
||||||
def create_shelves(self):
|
def create_shelves(self):
|
||||||
"""default shelves for a new user"""
|
"""default shelves for a new user"""
|
||||||
|
@ -355,7 +366,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
# self.owner is set by the OneToOneField on User
|
# self.owner is set by the OneToOneField on User
|
||||||
return "%s/#main-key" % self.owner.remote_id
|
return f"{self.owner.remote_id}/#main-key"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""create a key pair"""
|
"""create a key pair"""
|
||||||
|
@ -392,7 +403,7 @@ class AnnualGoal(BookWyrmModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""put the year in the path"""
|
"""put the year in the path"""
|
||||||
return "{:s}/goal/{:d}".format(self.user.remote_id, self.year)
|
return f"{self.user.remote_id}/goal/{self.year}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def books(self):
|
def books(self):
|
||||||
|
@ -448,7 +459,7 @@ def get_or_create_remote_server(domain):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = get_data("https://%s/.well-known/nodeinfo" % domain)
|
data = get_data(f"https://{domain}/.well-known/nodeinfo")
|
||||||
try:
|
try:
|
||||||
nodeinfo_url = data.get("links")[0].get("href")
|
nodeinfo_url = data.get("links")[0].get("href")
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
|
|
|
@ -220,6 +220,7 @@ def generate_default_inner_img():
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
|
# pylint: disable=too-many-statements
|
||||||
def generate_preview_image(
|
def generate_preview_image(
|
||||||
texts=None, picture=None, rating=None, show_instance_layer=True
|
texts=None, picture=None, rating=None, show_instance_layer=True
|
||||||
):
|
):
|
||||||
|
@ -237,7 +238,8 @@ def generate_preview_image(
|
||||||
|
|
||||||
# Color
|
# Color
|
||||||
if BG_COLOR in ["use_dominant_color_light", "use_dominant_color_dark"]:
|
if BG_COLOR in ["use_dominant_color_light", "use_dominant_color_dark"]:
|
||||||
image_bg_color = "rgb(%s, %s, %s)" % dominant_color
|
red, green, blue = dominant_color
|
||||||
|
image_bg_color = f"rgb({red}, {green}, {blue})"
|
||||||
|
|
||||||
# Adjust color
|
# Adjust color
|
||||||
image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)]
|
image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)]
|
||||||
|
@ -315,7 +317,8 @@ def save_and_cleanup(image, instance=None):
|
||||||
"""Save and close the file"""
|
"""Save and close the file"""
|
||||||
if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
|
if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
|
||||||
return False
|
return False
|
||||||
file_name = "%s-%s.jpg" % (str(instance.id), str(uuid4()))
|
uuid = uuid4()
|
||||||
|
file_name = f"{instance.id}-{uuid}.jpg"
|
||||||
image_buffer = BytesIO()
|
image_buffer = BytesIO()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -412,7 +415,7 @@ def generate_user_preview_image_task(user_id):
|
||||||
|
|
||||||
texts = {
|
texts = {
|
||||||
"text_one": user.display_name,
|
"text_one": user.display_name,
|
||||||
"text_three": "@{}@{}".format(user.localname, settings.DOMAIN),
|
"text_three": f"@{user.localname}@{settings.DOMAIN}",
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.avatar:
|
if user.avatar:
|
||||||
|
|
|
@ -48,7 +48,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
||||||
return
|
return
|
||||||
|
|
||||||
self.tag_stack = self.tag_stack[:-1]
|
self.tag_stack = self.tag_stack[:-1]
|
||||||
self.output.append(("tag", "</%s>" % tag))
|
self.output.append(("tag", f"</{tag}>"))
|
||||||
|
|
||||||
def handle_data(self, data):
|
def handle_data(self, data):
|
||||||
"""extract the answer, if we're in an answer tag"""
|
"""extract the answer, if we're in an answer tag"""
|
||||||
|
|
|
@ -13,7 +13,7 @@ VERSION = "0.0.1"
|
||||||
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 = "19447742"
|
JS_CACHE = "7f2343cf"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
@ -23,7 +23,7 @@ EMAIL_HOST_USER = env("EMAIL_HOST_USER")
|
||||||
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
||||||
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
|
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
|
||||||
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False)
|
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False)
|
||||||
DEFAULT_FROM_EMAIL = "admin@{:s}".format(env("DOMAIN"))
|
DEFAULT_FROM_EMAIL = f"admin@{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 = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
@ -77,7 +77,8 @@ MIDDLEWARE = [
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"bookwyrm.timezone_middleware.TimezoneMiddleware",
|
"bookwyrm.middleware.TimezoneMiddleware",
|
||||||
|
"bookwyrm.middleware.IPBlocklistMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
@ -126,7 +127,7 @@ DATABASES = {
|
||||||
"USER": env("POSTGRES_USER", "fedireads"),
|
"USER": env("POSTGRES_USER", "fedireads"),
|
||||||
"PASSWORD": env("POSTGRES_PASSWORD", "fedireads"),
|
"PASSWORD": env("POSTGRES_PASSWORD", "fedireads"),
|
||||||
"HOST": env("POSTGRES_HOST", ""),
|
"HOST": env("POSTGRES_HOST", ""),
|
||||||
"PORT": env("POSTGRES_PORT", 5432),
|
"PORT": env("PGPORT", 5432),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,11 +178,8 @@ USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
|
agent = requests.utils.default_user_agent()
|
||||||
requests.utils.default_user_agent(),
|
USER_AGENT = f"{agent} (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
|
||||||
VERSION,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Imagekit generated thumbnails
|
# Imagekit generated thumbnails
|
||||||
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
|
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
|
||||||
|
@ -212,11 +210,11 @@ if USE_S3:
|
||||||
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
|
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
|
||||||
# S3 Static settings
|
# S3 Static settings
|
||||||
STATIC_LOCATION = "static"
|
STATIC_LOCATION = "static"
|
||||||
STATIC_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, STATIC_LOCATION)
|
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
|
||||||
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
|
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
|
||||||
# S3 Media settings
|
# S3 Media settings
|
||||||
MEDIA_LOCATION = "images"
|
MEDIA_LOCATION = "images"
|
||||||
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIA_LOCATION)
|
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
||||||
# I don't know if it's used, but the site crashes without it
|
# I don't know if it's used, but the site crashes without it
|
||||||
|
@ -226,5 +224,5 @@ else:
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
MEDIA_FULL_URL = "%s://%s%s" % (PROTOCOL, DOMAIN, MEDIA_URL)
|
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||||
|
|
|
@ -26,21 +26,21 @@ def make_signature(sender, destination, date, digest):
|
||||||
"""uses a private key to sign an outgoing message"""
|
"""uses a private key to sign an outgoing message"""
|
||||||
inbox_parts = urlparse(destination)
|
inbox_parts = urlparse(destination)
|
||||||
signature_headers = [
|
signature_headers = [
|
||||||
"(request-target): post %s" % inbox_parts.path,
|
f"(request-target): post {inbox_parts.path}",
|
||||||
"host: %s" % inbox_parts.netloc,
|
f"host: {inbox_parts.netloc}",
|
||||||
"date: %s" % date,
|
f"date: {date}",
|
||||||
"digest: %s" % digest,
|
f"digest: {digest}",
|
||||||
]
|
]
|
||||||
message_to_sign = "\n".join(signature_headers)
|
message_to_sign = "\n".join(signature_headers)
|
||||||
signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key))
|
signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key))
|
||||||
signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8")))
|
signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8")))
|
||||||
signature = {
|
signature = {
|
||||||
"keyId": "%s#main-key" % sender.remote_id,
|
"keyId": f"{sender.remote_id}#main-key",
|
||||||
"algorithm": "rsa-sha256",
|
"algorithm": "rsa-sha256",
|
||||||
"headers": "(request-target) host date digest",
|
"headers": "(request-target) host date digest",
|
||||||
"signature": b64encode(signed_message).decode("utf8"),
|
"signature": b64encode(signed_message).decode("utf8"),
|
||||||
}
|
}
|
||||||
return ",".join('%s="%s"' % (k, v) for (k, v) in signature.items())
|
return ",".join(f'{k}="{v}"' for (k, v) in signature.items())
|
||||||
|
|
||||||
|
|
||||||
def make_digest(data):
|
def make_digest(data):
|
||||||
|
@ -58,7 +58,7 @@ def verify_digest(request):
|
||||||
elif algorithm == "SHA-512":
|
elif algorithm == "SHA-512":
|
||||||
hash_function = hashlib.sha512
|
hash_function = hashlib.sha512
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unsupported hash function: {}".format(algorithm))
|
raise ValueError(f"Unsupported hash function: {algorithm}")
|
||||||
|
|
||||||
expected = hash_function(request.body).digest()
|
expected = hash_function(request.body).digest()
|
||||||
if b64decode(digest) != expected:
|
if b64decode(digest) != expected:
|
||||||
|
@ -95,18 +95,18 @@ class Signature:
|
||||||
def verify(self, public_key, request):
|
def verify(self, public_key, request):
|
||||||
"""verify rsa signature"""
|
"""verify rsa signature"""
|
||||||
if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE:
|
if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE:
|
||||||
raise ValueError("Request too old: %s" % (request.headers["date"],))
|
raise ValueError(f"Request too old: {request.headers['date']}")
|
||||||
public_key = RSA.import_key(public_key)
|
public_key = RSA.import_key(public_key)
|
||||||
|
|
||||||
comparison_string = []
|
comparison_string = []
|
||||||
for signed_header_name in self.headers.split(" "):
|
for signed_header_name in self.headers.split(" "):
|
||||||
if signed_header_name == "(request-target)":
|
if signed_header_name == "(request-target)":
|
||||||
comparison_string.append("(request-target): post %s" % request.path)
|
comparison_string.append(f"(request-target): post {request.path}")
|
||||||
else:
|
else:
|
||||||
if signed_header_name == "digest":
|
if signed_header_name == "digest":
|
||||||
verify_digest(request)
|
verify_digest(request)
|
||||||
comparison_string.append(
|
comparison_string.append(
|
||||||
"%s: %s" % (signed_header_name, request.headers[signed_header_name])
|
f"{signed_header_name}: {request.headers[signed_header_name]}"
|
||||||
)
|
)
|
||||||
comparison_string = "\n".join(comparison_string)
|
comparison_string = "\n".join(comparison_string)
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,32 @@ body {
|
||||||
display: inline !important;
|
display: inline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=file]::file-selector-button {
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dbdbdb;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #363636;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
height: 2.5em;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-bottom: calc(0.5em - 1px);
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
padding-top: calc(0.5em - 1px);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=file]::file-selector-button:hover {
|
||||||
|
border-color: #b5b5b5;
|
||||||
|
color: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
/** Shelving
|
/** Shelving
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
@ -352,6 +378,13 @@ body {
|
||||||
right: 1em;
|
right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tooltips
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/** States
|
/** States
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -64,9 +64,21 @@ let StatusCache = new class {
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
submitStatus(event) {
|
submitStatus(event) {
|
||||||
event.preventDefault();
|
|
||||||
const form = event.currentTarget;
|
const form = event.currentTarget;
|
||||||
const trigger = event.submitter;
|
let trigger = event.submitter;
|
||||||
|
|
||||||
|
// Safari doesn't understand "submitter"
|
||||||
|
if (!trigger) {
|
||||||
|
trigger = event.currentTarget.querySelector("button[type=submit]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This allows the form to submit in the old fashioned way if there's a problem
|
||||||
|
|
||||||
|
if (!trigger || !form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
BookWyrm.addRemoveClass(form, 'is-processing', true);
|
BookWyrm.addRemoveClass(form, 'is-processing', true);
|
||||||
trigger.setAttribute('disabled', null);
|
trigger.setAttribute('disabled', null);
|
||||||
|
|
|
@ -24,8 +24,8 @@ class SuggestedUsers(RedisStore):
|
||||||
def store_id(self, user): # pylint: disable=no-self-use
|
def store_id(self, user): # pylint: disable=no-self-use
|
||||||
"""the key used to store this user's recs"""
|
"""the key used to store this user's recs"""
|
||||||
if isinstance(user, int):
|
if isinstance(user, int):
|
||||||
return "{:d}-suggestions".format(user)
|
return f"{user}-suggestions"
|
||||||
return "{:d}-suggestions".format(user.id)
|
return f"{user.id}-suggestions"
|
||||||
|
|
||||||
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
|
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
|
||||||
"""calculate mutuals count and shared books count from rank"""
|
"""calculate mutuals count and shared books count from rank"""
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% trans "Help" as button_text %}
|
{% trans "Help" as button_text %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text class="ml-3 is-rounded is-small is-white p-0 pb-1" icon="question-circle is-size-6" controls_text=controls_text controls_uid=controls_uid %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text class="ml-3 is-rounded is-small is-white p-0 pb-1" icon="question-circle is-size-6" controls_text=controls_text controls_uid=controls_uid %}
|
||||||
|
|
||||||
<aside class="notification is-hidden transition-y is-pulled-left mb-2" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
|
<aside class="tooltip notification is-hidden transition-y is-pulled-left mb-2" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
|
||||||
{% trans "Close" as button_text %}
|
{% trans "Close" as button_text %}
|
||||||
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
|
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
{% if not draft %}
|
{% if not draft %}
|
||||||
{% include 'snippets/create_status.html' %}
|
{% include 'snippets/create_status.html' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'snippets/create_status/status.html' %}
|
{% include 'snippets/create_status/status.html' with no_script=True %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,14 +15,15 @@
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% with tile_classes="tile is-child box has-background-white-ter is-clipped" %}
|
||||||
<div class="tile is-ancestor">
|
<div class="tile is-ancestor">
|
||||||
<div class="tile is-6 is-parent">
|
<div class="tile is-6 is-parent">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/large-book.html' with status=large_activities.0 %}
|
{% include 'discover/large-book.html' with status=large_activities.0 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-6 is-parent">
|
<div class="tile is-6 is-parent">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/large-book.html' with status=large_activities.1 %}
|
{% include 'discover/large-book.html' with status=large_activities.1 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,18 +32,18 @@
|
||||||
<div class="tile is-ancestor">
|
<div class="tile is-ancestor">
|
||||||
<div class="tile is-vertical is-6">
|
<div class="tile is-vertical is-6">
|
||||||
<div class="tile is-parent">
|
<div class="tile is-parent">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/large-book.html' with status=large_activities.2 %}
|
{% include 'discover/large-book.html' with status=large_activities.2 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
<div class="tile is-parent is-6">
|
<div class="tile is-parent is-6">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/small-book.html' with status=small_activities.0 %}
|
{% include 'discover/small-book.html' with status=small_activities.0 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-parent is-6">
|
<div class="tile is-parent is-6">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/small-book.html' with status=small_activities.1 %}
|
{% include 'discover/small-book.html' with status=small_activities.1 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,18 +52,18 @@
|
||||||
<div class="tile is-vertical is-6">
|
<div class="tile is-vertical is-6">
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
<div class="tile is-parent is-6">
|
<div class="tile is-parent is-6">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/small-book.html' with status=small_activities.2 %}
|
{% include 'discover/small-book.html' with status=small_activities.2 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-parent is-6">
|
<div class="tile is-parent is-6">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/small-book.html' with status=small_activities.3 %}
|
{% include 'discover/small-book.html' with status=small_activities.3 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-parent">
|
<div class="tile is-parent">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/large-book.html' with status=large_activities.3 %}
|
{% include 'discover/large-book.html' with status=large_activities.3 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,16 +72,17 @@
|
||||||
|
|
||||||
<div class="tile is-ancestor">
|
<div class="tile is-ancestor">
|
||||||
<div class="tile is-6 is-parent">
|
<div class="tile is-6 is-parent">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/large-book.html' with status=large_activities.4 %}
|
{% include 'discover/large-book.html' with status=large_activities.4 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-6 is-parent">
|
<div class="tile is-6 is-parent">
|
||||||
<div class="tile is-child box has-background-white-ter">
|
<div class="{{ tile_classes }}">
|
||||||
{% include 'discover/large-book.html' with status=large_activities.5 %}
|
{% include 'discover/large-book.html' with status=large_activities.5 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endwith %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=partner %}
|
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=partner no_script=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
data-action="toggle-all"
|
data-action="toggle-all"
|
||||||
data-target="failed-imports"
|
data-target="failed_imports"
|
||||||
/>
|
/>
|
||||||
{% trans "Select all" %}
|
{% trans "Select all" %}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<h1 class="title">{% trans "Create an Account" %}</h1>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% if valid %}
|
{% if valid %}
|
||||||
<h1 class="title">{% trans "Create an Account" %}</h1>
|
|
||||||
<div>
|
<div>
|
||||||
<form name="register" method="post" action="/register">
|
<form name="register" method="post" action="/register">
|
||||||
<input type=hidden name="invite_code" value="{{ invite.code }}">
|
<input type=hidden name="invite_code" value="{{ invite.code }}">
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="block">
|
<div class="box">
|
||||||
{% include 'snippets/about.html' %}
|
{% include 'snippets/about.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'landing/landing_layout.html' %}
|
{% extends 'landing/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'landing/landing_layout.html' %}
|
{% extends 'landing/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
|
|
@ -40,38 +40,41 @@
|
||||||
<div class="tile is-5 is-parent">
|
<div class="tile is-5 is-parent">
|
||||||
{% if not request.user.is_authenticated %}
|
{% if not request.user.is_authenticated %}
|
||||||
<div class="tile is-child box has-background-primary-light content">
|
<div class="tile is-child box has-background-primary-light content">
|
||||||
|
<h2 class="title">
|
||||||
|
{% if site.allow_registration %}
|
||||||
|
{% blocktrans with name=site.name %}Join {{ name }}{% endblocktrans %}
|
||||||
|
{% elif site.allow_invite_requests %}
|
||||||
|
{% trans "Request an Invitation" %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans with name=site.name%}{{ name}} registration is closed{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{% if site.allow_registration %}
|
{% if site.allow_registration %}
|
||||||
<h2 class="title">{% blocktrans with name=site.name %}Join {{ name }}{% endblocktrans %}</h2>
|
<form name="register" method="post" action="/register">
|
||||||
<form name="register" method="post" action="/register">
|
{% include 'snippets/register_form.html' %}
|
||||||
{% include 'snippets/register_form.html' %}
|
</form>
|
||||||
</form>
|
{% elif site.allow_invite_requests %}
|
||||||
|
{% if request_received %}
|
||||||
|
<p>
|
||||||
|
{% trans "Thank you! Your request has been received." %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ site.invite_request_text }}</p>
|
||||||
|
<form name="invite-request" action="{% url 'invite-request' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="block">
|
||||||
|
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
||||||
|
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
||||||
|
{% for error in request_form.email.errors %}
|
||||||
|
<p class="help is-danger">{{ error|escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<p>{{ site.registration_closed_text|safe}}</p>
|
||||||
<h2 class="title">{% trans "This instance is closed" %}</h2>
|
|
||||||
<p>{{ site.registration_closed_text|safe}}</p>
|
|
||||||
|
|
||||||
{% if site.allow_invite_requests %}
|
|
||||||
{% if request_received %}
|
|
||||||
<p>
|
|
||||||
{% trans "Thank you! Your request has been received." %}
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<h3>{% trans "Request an Invitation" %}</h3>
|
|
||||||
<form name="invite-request" action="{% url 'invite-request' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="block">
|
|
||||||
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
|
||||||
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
|
||||||
{% for error in request_form.email.errors %}
|
|
||||||
<p class="help is-danger">{{ error|escape }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
|
@ -4,7 +4,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{% get_lang %}">
|
<html lang="{% get_lang %}">
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title>
|
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
|
<link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
|
||||||
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
||||||
|
@ -19,8 +19,8 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<meta name="twitter:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
|
<meta name="twitter:title" content="{% if title %}{{ title }} - {% endif %}{{ site.name }}">
|
||||||
<meta name="og:title" content="{% if title %}{{ title }} | {% endif %}{{ site.name }}">
|
<meta name="og:title" content="{% if title %}{{ title }} - {% endif %}{{ site.name }}">
|
||||||
<meta name="twitter:description" content="{{ site.instance_tagline }}">
|
<meta name="twitter:description" content="{{ site.instance_tagline }}">
|
||||||
<meta name="og:description" content="{{ site.instance_tagline }}">
|
<meta name="og:description" content="{{ site.instance_tagline }}">
|
||||||
|
|
||||||
|
@ -39,7 +39,12 @@
|
||||||
<form class="navbar-item column" action="{% url 'search' %}">
|
<form class="navbar-item column" action="{% url 'search' %}">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input aria-label="{% trans 'Search for a book or user' %}" id="search_input" class="input" type="text" name="q" placeholder="{% trans 'Search for a book or user' %}" value="{{ query }}">
|
{% if user.is_authenticated %}
|
||||||
|
{% trans "Search for a book, user, or list" as search_placeholder %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Search for a book" as search_placeholder %}
|
||||||
|
{% endif %}
|
||||||
|
<input aria-label="{{ search_placeholder }}" id="search_input" class="input" type="text" name="q" placeholder="{{ search_placeholder }}" value="{{ query }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button" type="submit">
|
<button class="button" type="submit">
|
||||||
|
@ -123,7 +128,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.bookwyrm.moderate_user %}
|
{% if perms.bookwyrm.moderate_user %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'settings-users' %}" class="navbar-item">
|
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
||||||
{% trans 'Admin' %}
|
{% trans 'Admin' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -45,8 +45,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if list.id %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
{% trans "Delete list" as button_text %}
|
{% trans "Delete list" as button_text %}
|
||||||
{% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_list" controls_uid=list.id focus="modal_title_delete_list" %}
|
{% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_list" controls_uid=list.id focus="modal_title_delete_list" %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,69 +4,65 @@
|
||||||
{% block title %}{% trans "Login" %}{% endblock %}
|
{% block title %}{% trans "Login" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="columns">
|
<h1 class="title">{% trans "Log in" %}</h1>
|
||||||
<div class="column">
|
<div class="columns is-multiline">
|
||||||
<div class="box">
|
<div class="column is-half">
|
||||||
<h1 class="title">{% trans "Log in" %}</h1>
|
{% if login_form.non_field_errors %}
|
||||||
{% if login_form.non_field_errors %}
|
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
|
||||||
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if show_confirmed_email %}
|
{% if show_confirmed_email %}
|
||||||
<p class="notification is-success">{% trans "Success! Email address confirmed." %}</p>
|
<p class="notification is-success">{% trans "Success! Email address confirmed." %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form name="login" method="post" action="/login">
|
<form name="login" method="post" action="/login">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if show_confirmed_email %}<input type="hidden" name="first_login" value="true">{% endif %}
|
{% if show_confirmed_email %}<input type="hidden" name="first_login" value="true">{% endif %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_localname">{% trans "Username:" %}</label>
|
<label class="label" for="id_localname">{% trans "Username:" %}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{{ login_form.localname }}
|
{{ login_form.localname }}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
</div>
|
||||||
<label class="label" for="id_password">{% trans "Password:" %}</label>
|
<div class="field">
|
||||||
<div class="control">
|
<label class="label" for="id_password">{% trans "Password:" %}</label>
|
||||||
{{ login_form.password }}
|
<div class="control">
|
||||||
</div>
|
{{ login_form.password }}
|
||||||
{% for error in login_form.password.errors %}
|
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped">
|
{% for error in login_form.password.errors %}
|
||||||
<div class="control">
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="field is-grouped">
|
||||||
<small><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></small>
|
<div class="control">
|
||||||
</div>
|
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div class="control">
|
||||||
</div>
|
<small><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
{% if site.allow_registration %}
|
||||||
|
<div class="column is-half">
|
||||||
<div class="box has-background-primary-light">
|
<div class="box has-background-primary-light">
|
||||||
{% if site.allow_registration %}
|
|
||||||
<h2 class="title">{% trans "Create an Account" %}</h2>
|
<h2 class="title">{% trans "Create an Account" %}</h2>
|
||||||
<form name="register" method="post" action="/register">
|
<form name="register" method="post" action="/register">
|
||||||
{% include 'snippets/register_form.html' %}
|
{% include 'snippets/register_form.html' %}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
</div>
|
||||||
<h2 class="title">{% trans "This instance is closed" %}</h2>
|
</div>
|
||||||
<p>{% trans "Contact an administrator to get an invite" %}</p>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="box">
|
||||||
|
{% include 'snippets/about.html' %}
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
|
||||||
<div class="box">
|
|
||||||
{% include 'snippets/about.html' %}
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -14,40 +14,46 @@
|
||||||
{% include 'settings/announcement_form.html' with controls_text="create_announcement" %}
|
{% include 'settings/announcement_form.html' with controls_text="create_announcement" %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="table is-striped">
|
<div class="block">
|
||||||
<tr>
|
<table class="table is-striped">
|
||||||
<th>
|
<tr>
|
||||||
{% url 'settings-announcements' as url %}
|
<th>
|
||||||
{% trans "Date added" as text %}
|
{% url 'settings-announcements' as url %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
{% trans "Date added" as text %}
|
||||||
</th>
|
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||||
<th>
|
</th>
|
||||||
{% trans "Preview" as text %}
|
<th>
|
||||||
{% include 'snippets/table-sort-header.html' with field="preview" sort=sort text=text %}
|
{% trans "Preview" as text %}
|
||||||
</th>
|
{% include 'snippets/table-sort-header.html' with field="preview" sort=sort text=text %}
|
||||||
<th>
|
</th>
|
||||||
{% trans "Start date" as text %}
|
<th>
|
||||||
{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}
|
{% trans "Start date" as text %}
|
||||||
</th>
|
{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}
|
||||||
<th>
|
</th>
|
||||||
{% trans "End date" as text %}
|
<th>
|
||||||
{% include 'snippets/table-sort-header.html' with field="end_date" sort=sort text=text %}
|
{% trans "End date" as text %}
|
||||||
</th>
|
{% include 'snippets/table-sort-header.html' with field="end_date" sort=sort text=text %}
|
||||||
<th>
|
</th>
|
||||||
{% trans "Status" as text %}
|
<th>
|
||||||
{% include 'snippets/table-sort-header.html' with field="active" sort=sort text=text %}
|
{% trans "Status" as text %}
|
||||||
</th>
|
{% include 'snippets/table-sort-header.html' with field="active" sort=sort text=text %}
|
||||||
</tr>
|
</th>
|
||||||
{% for announcement in announcements %}
|
</tr>
|
||||||
<tr>
|
{% for announcement in announcements %}
|
||||||
<td>{{ announcement.created_date|naturalday }}</td>
|
<tr>
|
||||||
<td><a href="{% url 'settings-announcements' announcement.id %}">{{ announcement.preview }}</a></td>
|
<td>{{ announcement.created_date|naturalday }}</td>
|
||||||
<td>{{ announcement.start_date|naturaltime|default:'' }}</td>
|
<td><a href="{% url 'settings-announcements' announcement.id %}">{{ announcement.preview }}</a></td>
|
||||||
<td>{{ announcement.end_date|naturaltime|default:'' }}</td>
|
<td>{{ announcement.start_date|naturaltime|default:'' }}</td>
|
||||||
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
|
<td>{{ announcement.end_date|naturaltime|default:'' }}</td>
|
||||||
</tr>
|
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</table>
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if not announcements %}
|
||||||
|
<p><em>{% trans "No announcements found." %}</em></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include 'snippets/pagination.html' with page=announcements path=request.path %}
|
{% include 'snippets/pagination.html' with page=announcements path=request.path %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
120
bookwyrm/templates/settings/dashboard.html
Normal file
120
bookwyrm/templates/settings/dashboard.html
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{% extends 'settings/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Dashboard" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% trans "Dashboard" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
<div class="columns block has-text-centered">
|
||||||
|
<div class="column is-3">
|
||||||
|
<div class="notification">
|
||||||
|
<h3>{% trans "Total users" %}</h3>
|
||||||
|
<p class="title is-5">{{ users|intcomma }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-3">
|
||||||
|
<div class="notification">
|
||||||
|
<h3>{% trans "Active this month" %}</h3>
|
||||||
|
<p class="title is-5">{{ active_users|intcomma }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-3">
|
||||||
|
<div class="notification">
|
||||||
|
<h3>{% trans "Statuses" %}</h3>
|
||||||
|
<p class="title is-5">{{ statuses|intcomma }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-3">
|
||||||
|
<div class="notification">
|
||||||
|
<h3>{% trans "Works" %}</h3>
|
||||||
|
<p class="title is-5">{{ works|intcomma }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns block is-multiline">
|
||||||
|
{% if reports %}
|
||||||
|
<div class="column">
|
||||||
|
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
|
||||||
|
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
||||||
|
{{ display_count }} open report
|
||||||
|
{% plural %}
|
||||||
|
{{ display_count }} open reports
|
||||||
|
{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||||
|
<div class="column">
|
||||||
|
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light">
|
||||||
|
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
||||||
|
{{ display_count }} invite request
|
||||||
|
{% plural %}
|
||||||
|
{{ display_count }} invite requests
|
||||||
|
{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block content">
|
||||||
|
<h2>{% trans "Instance Activity" %}</h2>
|
||||||
|
|
||||||
|
<form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis">
|
||||||
|
<div class="is-flex is-align-items-flex-end">
|
||||||
|
<div class="ml-1 mr-1">
|
||||||
|
<label class="label">
|
||||||
|
{% trans "Start date:" %}
|
||||||
|
<input class="input" type="date" name="start" value="{{ start }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="ml-1 mr-1">
|
||||||
|
<label class="label">
|
||||||
|
{% trans "End date:" %}
|
||||||
|
<input class="input" type="date" name="end" value="{{ end }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="ml-1 mr-1">
|
||||||
|
<label class="label">
|
||||||
|
{% trans "Interval:" %}
|
||||||
|
<div class="select">
|
||||||
|
<select name="days">
|
||||||
|
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
|
||||||
|
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="ml-1 mr-1">
|
||||||
|
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h3>{% trans "User signup activity" %}</h3>
|
||||||
|
<div class="box">
|
||||||
|
<canvas id="user_stats"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<h3>{% trans "Status activity" %}</h3>
|
||||||
|
<div class="box">
|
||||||
|
<canvas id="status_stats"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
||||||
|
{% include 'settings/dashboard_user_chart.html' %}
|
||||||
|
{% include 'settings/dashboard_status_chart.html' %}
|
||||||
|
{% endblock %}
|
26
bookwyrm/templates/settings/dashboard_status_chart.html
Normal file
26
bookwyrm/templates/settings/dashboard_status_chart.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<script>
|
||||||
|
const status_labels = [{% for label in status_stats.labels %}"{{ label }}",{% endfor %}];
|
||||||
|
const status_data = {
|
||||||
|
labels: status_labels,
|
||||||
|
datasets: [{
|
||||||
|
label: '{% trans "Statuses posted" %}',
|
||||||
|
backgroundColor: 'rgb(255, 99, 132)',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
data: {{ status_stats.total }},
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
// === include 'setup' then 'config' above ===
|
||||||
|
|
||||||
|
const status_config = {
|
||||||
|
type: 'bar',
|
||||||
|
data: status_data,
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
var statusStats = new Chart(
|
||||||
|
document.getElementById('status_stats'),
|
||||||
|
status_config
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
29
bookwyrm/templates/settings/dashboard_user_chart.html
Normal file
29
bookwyrm/templates/settings/dashboard_user_chart.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<script>
|
||||||
|
const labels = [{% for label in user_stats.labels %}"{{ label }}",{% endfor %}];
|
||||||
|
const data = {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: '{% trans "Total" %}',
|
||||||
|
backgroundColor: 'rgb(255, 99, 132)',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
data: {{ user_stats.total }},
|
||||||
|
}, {
|
||||||
|
label: '{% trans "Active this month" %}',
|
||||||
|
backgroundColor: 'rgb(75, 192, 192)',
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
data: {{ user_stats.active }},
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
type: 'line',
|
||||||
|
data: data,
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
var userStats = new Chart(
|
||||||
|
document.getElementById('user_stats'),
|
||||||
|
config
|
||||||
|
);
|
||||||
|
</script>
|
|
@ -8,7 +8,7 @@
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<form name="add-domain" method="post" action="{% url 'settings-email-blocks' %}">
|
<form name="add-domain" method="post" action="{% url 'settings-email-blocks' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="label" for="id_event_date">{% trans "Domain:" %}</label>
|
<label class="label" for="id_domain">{% trans "Domain:" %}</label>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="button is-disabled">@</div>
|
<div class="button is-disabled">@</div>
|
||||||
|
|
|
@ -22,10 +22,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-federation' as url %}
|
{% url 'settings-federation' as url %}
|
||||||
<th>
|
<th>
|
||||||
{% trans "Domain" as text %}
|
{% trans "Domain" %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
|
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th>{% trans "Users" %}</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Options" %}
|
{% trans "Options" %}
|
||||||
</th>
|
</th>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
<dt>{% trans "Status:" %}</dt>
|
<dt>{% trans "Status:" %}</dt>
|
||||||
<dd>{{ server.status }}</dd>
|
<dd>{{ server.get_status_display }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,19 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
{% url 'settings-federation' status='federated' as url %}
|
||||||
|
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
||||||
|
<a href="{{ url }}">{% trans "Federated" %}</a>
|
||||||
|
</li>
|
||||||
|
{% url 'settings-federation' status='blocked' as url %}
|
||||||
|
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||||
|
<a href="{{ url }}">{% trans "Blocked" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table is-striped">
|
<table class="table is-striped">
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-federation' as url %}
|
{% url 'settings-federation' as url %}
|
||||||
|
@ -20,21 +33,30 @@
|
||||||
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Date federated" as text %}
|
{% trans "Date added" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Software" as text %}
|
{% trans "Software" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Users" %}
|
||||||
|
</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>{% trans "Status" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for server in servers %}
|
{% for server in servers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||||
<td>{{ server.created_date }}</td>
|
<td>{{ server.created_date }}</td>
|
||||||
<td>{{ server.application_type }} ({{ server.application_version }})</td>
|
<td>
|
||||||
<td>{{ server.status }}</td>
|
{% if server.application_type %}
|
||||||
|
{{ server.application_type }}
|
||||||
|
{% if server.application_version %}({{ server.application_version }}){% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ server.user_set.count }}</td>
|
||||||
|
<td>{{ server.get_status_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
36
bookwyrm/templates/settings/ip_address_form.html
Normal file
36
bookwyrm/templates/settings/ip_address_form.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends 'components/inline_form.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% trans "Add IP address" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<form name="add-address" method="post" action="{% url 'settings-ip-blocks' %}">
|
||||||
|
<div class="block">
|
||||||
|
{% trans "Use IP address blocks with caution, and consider using blocks only temporarily, as IP addresses are often shared or change hands. If you block your own IP, you will not be able to access this page." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_address">
|
||||||
|
{% trans "IP Address:" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for error in form.address.errors %}
|
||||||
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
47
bookwyrm/templates/settings/ip_blocklist.html
Normal file
47
bookwyrm/templates/settings/ip_blocklist.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{% extends 'settings/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "IP Address Blocklist" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% trans "IP Address Blocklist" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block edit-button %}
|
||||||
|
{% trans "Add IP address" as button_text %}
|
||||||
|
{% include 'snippets/toggle/open_button.html' with controls_text="add_address" icon_with_text="plus" text=button_text focus="add_address_header" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
{% include 'settings/ip_address_form.html' with controls_text="add_address" class="block" %}
|
||||||
|
|
||||||
|
<p class="notification block">
|
||||||
|
{% trans "Any traffic from this IP address will get a 404 response when trying to access any part of the application." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table is-striped is-fullwidth">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% trans "Address" %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Options" %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{% for address in addresses %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ address.address }}</td>
|
||||||
|
<td>
|
||||||
|
<form name="remove-{{ address.id }}" action="{% url 'settings-ip-blocks-delete' address.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% trans "Delete" as button_text %}
|
||||||
|
<button class="button" type="submit">
|
||||||
|
<span class="icon icon-x" title="{{ button_text }}" aria-hidden="true"></span>
|
||||||
|
<span class="is-hidden-mobile">{{ button_text }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
8
bookwyrm/templates/settings/ip_tooltip.html
Normal file
8
bookwyrm/templates/settings/ip_tooltip.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'components/tooltip.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block tooltip_content %}
|
||||||
|
|
||||||
|
{% trans "You can block IP ranges using CIDR syntax." %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -18,6 +18,13 @@
|
||||||
|
|
||||||
<div class="block columns">
|
<div class="block columns">
|
||||||
<nav class="menu column is-one-quarter">
|
<nav class="menu column is-one-quarter">
|
||||||
|
<h2 class="menu-label">
|
||||||
|
{% url 'settings-dashboard' as url %}
|
||||||
|
<a
|
||||||
|
href="{{ url }}"
|
||||||
|
{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}
|
||||||
|
>{% trans "Dashboard" %}</a>
|
||||||
|
</h2>
|
||||||
{% if perms.bookwyrm.create_invites %}
|
{% if perms.bookwyrm.create_invites %}
|
||||||
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
|
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
|
@ -51,6 +58,10 @@
|
||||||
{% url 'settings-email-blocks' as url %}
|
{% url 'settings-email-blocks' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Blocklist" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Blocklist" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'settings-ip-blocks' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "IP Address Blocklist" %}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.bookwyrm.edit_instance_settings %}
|
{% if perms.bookwyrm.edit_instance_settings %}
|
||||||
|
|
|
@ -107,6 +107,13 @@
|
||||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||||
{{ site_form.registration_closed_text }}
|
{{ site_form.registration_closed_text }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||||
|
{{ site_form.invite_request_text }}
|
||||||
|
{% for error in site_form.invite_request_text.errors %}
|
||||||
|
<p class="help is-danger">{{ error|escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="block">
|
<footer class="block">
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="control{% if not parent_status.content_warning and not draft.content_warning %} is-hidden{% endif %}" id="spoilers_{{ uuid }}">
|
<div
|
||||||
<label class="is-sr-only" for="id_content_warning_{{ uuid }}">{% trans "Spoiler alert:" %}</label>
|
class="field{% if not reply_parent.content_warning and not draft.content_warning %} is-hidden{% endif %}"
|
||||||
|
id="spoilers_{{ uuid }}{{ local_uuid }}"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
for="id_content_warning_{{ uuid }}{{ local_uuid }}"
|
||||||
|
>
|
||||||
|
{% trans "Content warning:" %}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="content_warning"
|
name="content_warning"
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
class="input"
|
class="input"
|
||||||
id="id_content_warning_{{ uuid }}"
|
id="id_content_warning_{{ uuid }}{{ local_uuid }}"
|
||||||
placeholder="{% trans 'Spoilers ahead!' %}"
|
placeholder="{% trans 'Spoilers ahead!' %}"
|
||||||
value="{% firstof draft.content_warning parent_status.content_warning '' %}"
|
value="{% firstof draft.content_warning reply_parent.content_warning '' %}"
|
||||||
data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"
|
data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="is-hidden"
|
class="is-hidden"
|
||||||
name="sensitive"
|
name="sensitive"
|
||||||
id="id_show_spoilers_{{ uuid }}"
|
id="id_show_spoilers_{{ uuid }}{{ local_uuid }}"
|
||||||
{% if draft.content_warning or status.content_warning %}checked{% endif %}
|
{% if draft.content_warning or status.content_warning %}checked{% endif %}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"
|
data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"
|
||||||
>
|
>
|
||||||
{% trans "Include spoiler alert" as button_text %}
|
{% trans "Include spoiler alert" as button_text %}
|
||||||
{% firstof draft.content_warning status.content_warning as pressed %}
|
{% firstof draft.content_warning status.content_warning as pressed %}
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %}
|
{% firstof local_uuid '' as local_uuid %}
|
||||||
|
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid|add:local_uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,13 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
||||||
|
|
||||||
{% block form_open %}
|
{% block form_open %}
|
||||||
{# default form tag syntax, can be overriddden #}
|
{# default form tag syntax, can be overriddden #}
|
||||||
<form class="is-flex-grow-1 submit-status" name="{{ type }}" action="/post/{{ type }}" method="post" id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}">
|
<form
|
||||||
|
class="is-flex-grow-1{% if not no_script %} submit-status{% endif %}"
|
||||||
|
name="{{ type }}"
|
||||||
|
action="/post/{{ type }}"
|
||||||
|
method="post"
|
||||||
|
id="tab_{{ type }}_{{ book.id }}{{ reply_parent.id }}"
|
||||||
|
>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -25,23 +31,19 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
||||||
<input type="hidden" name="reply_parent" value="{% firstof draft.reply_parent.id reply_parent.id %}">
|
<input type="hidden" name="reply_parent" value="{% firstof draft.reply_parent.id reply_parent.id %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% include "snippets/create_status/content_warning_field.html" %}
|
||||||
|
|
||||||
|
{# fields that go between the content warnings and the content field (ie, quote) #}
|
||||||
|
{% block pre_content_additions %}{% endblock %}
|
||||||
|
|
||||||
|
<label class="label" for="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}">
|
||||||
|
{% block content_label %}
|
||||||
|
{% trans "Comment:" %}
|
||||||
|
{% endblock %}
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
{% include "snippets/create_status/content_field.html" with placeholder=placeholder %}
|
||||||
{% include "snippets/create_status/content_warning_field.html" %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# fields that go between the content warnings and the content field (ie, quote) #}
|
|
||||||
{% block pre_content_additions %}{% endblock %}
|
|
||||||
|
|
||||||
<label class="label" for="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}">
|
|
||||||
{% block content_label %}
|
|
||||||
{% trans "Comment:" %}
|
|
||||||
{% endblock %}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="control">
|
|
||||||
{% include "snippets/create_status/content_field.html" with placeholder=placeholder %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# additional fields that go after the content block (ie, progress) #}
|
{# additional fields that go after the content block (ie, progress) #}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<form method="post" name="goal" action="{{ request.user.local_path }}/goal/{{ year }}">
|
<form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
|
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
|
|
|
@ -12,4 +12,5 @@
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="mention_books" value="{{ book.id }}">
|
<input type="hidden" name="mention_books" value="{{ book.id }}">
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
min="0"
|
min="0"
|
||||||
name="progress"
|
name="progress"
|
||||||
size="3"
|
size="3"
|
||||||
value="{{ readthrough.progress|default:'' }}">
|
value="{{ readthrough.progress|default:'' }}"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="control select">
|
<div class="control select">
|
||||||
<select name="progress_mode" aria-label="Progress mode">
|
<select name="progress_mode" aria-label="Progress mode">
|
||||||
|
|
|
@ -67,12 +67,18 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if status.content_warning %}
|
{% if status.content_warning %}
|
||||||
<div>
|
<div class="notification p-2 is-clearfix is-warning is-light">
|
||||||
<p>{{ status.content_warning }}</p>
|
<p class="is-pulled-left is-flex">
|
||||||
|
{% trans "Content warning" as text %}
|
||||||
|
<span class="icon icon-warning" title="{{ text }}">
|
||||||
|
<span class="is-sr-only">{{ text }}</span>
|
||||||
|
</span>
|
||||||
|
{{ status.content_warning }}
|
||||||
|
</p>
|
||||||
|
|
||||||
{% trans "Show more" as button_text %}
|
{% trans "Show status" as button_text %}
|
||||||
|
|
||||||
{% with text=button_text class="is-small" controls_text="show_status_cw" controls_uid=status.id %}
|
{% with text=button_text class="is-small is-pulled-right" icon_with_text="arrow-down" controls_text="show_status_cw" controls_uid=status.id %}
|
||||||
{% include 'snippets/toggle/open_button.html' %}
|
{% include 'snippets/toggle/open_button.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,17 +90,9 @@
|
||||||
class="is-hidden"
|
class="is-hidden"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
{% if status.content_warning %}
|
|
||||||
{% trans "Show less" as button_text %}
|
|
||||||
|
|
||||||
{% with text=button_text class="is-small" controls_text="show_status_cw" controls_uid=status.id %}
|
|
||||||
{% include 'snippets/toggle/close_button.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if status.quote %}
|
{% if status.quote %}
|
||||||
<div class="quote block">
|
<div class="quote block">
|
||||||
<blockquote dir="auto" class="content mb-2">{{ status.quote|safe }}</blockquote>
|
<blockquote dir="auto" class="content mb-2 preserve-whitespace">{{ status.quote|safe }}</blockquote>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
— {% include 'snippets/book_titleby.html' with book=status.book %}
|
— {% include 'snippets/book_titleby.html' with book=status.book %}
|
||||||
|
@ -141,6 +139,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if status.content_warning %}
|
||||||
|
{% trans "Hide status" as button_text %}
|
||||||
|
|
||||||
|
{% with text=button_text class="is-small" controls_text="show_status_cw" controls_uid=status.id icon_with_text="arrow-up" %}
|
||||||
|
{% include 'snippets/toggle/close_button.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{% if not no_trim and trimmed != full %}
|
{% if not no_trim and trimmed != full %}
|
||||||
<div id="hide_full_{{ uuid }}">
|
<div id="hide_full_{{ uuid }}">
|
||||||
<div class="content" id="trimmed_{{ uuid }}">
|
<div class="content" id="trimmed_{{ uuid }}">
|
||||||
<div dir="auto">{{ trimmed }}</div>
|
<div dir="auto" class="preserve-whitespace">{{ trimmed }}</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if not hide_more %}
|
{% if not hide_more %}
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div
|
<div
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
class="preserve-whitespace"
|
||||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||||
>
|
>
|
||||||
{{ full }}
|
{{ full }}
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div
|
<div
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
class="preserve-whitespace"
|
||||||
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
{% if itemprop %}itemprop="{{ itemprop }}"{% endif %}
|
||||||
>
|
>
|
||||||
{{ full }}
|
{{ full }}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<p class="notification is-warning">
|
<p class="notification is-warning">
|
||||||
{% trans "Inactive" %}
|
{% trans "Inactive" %}
|
||||||
{% if user.deactivation_reason %}
|
{% if user.deactivation_reason %}
|
||||||
({{ user.deactivation_reason }})
|
<span class="help">({% trans user.get_deactivation_reason_display %})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -12,7 +12,7 @@ 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"""
|
||||||
queryset = views.helpers.privacy_filter(
|
queryset = views.helpers.privacy_filter(
|
||||||
user, models.Review.objects.filter(book__in=book.parent_work.editions.all())
|
user, models.Review.objects.filter(book__parent_work__editions=book)
|
||||||
)
|
)
|
||||||
return queryset.aggregate(Avg("rating"))["rating__avg"]
|
return queryset.aggregate(Avg("rating"))["rating__avg"]
|
||||||
|
|
||||||
|
@ -70,10 +70,13 @@ def related_status(notification):
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def active_shelf(context, book):
|
def active_shelf(context, book):
|
||||||
"""check what shelf a user has a book on, if any"""
|
"""check what shelf a user has a book on, if any"""
|
||||||
|
if hasattr(book, "current_shelves"):
|
||||||
|
return book.current_shelves[0] if len(book.current_shelves) else {"book": book}
|
||||||
|
|
||||||
shelf = (
|
shelf = (
|
||||||
models.ShelfBook.objects.filter(
|
models.ShelfBook.objects.filter(
|
||||||
shelf__user=context["request"].user,
|
shelf__user=context["request"].user,
|
||||||
book__in=book.parent_work.editions.all(),
|
book__parent_work__editions=book,
|
||||||
)
|
)
|
||||||
.select_related("book", "shelf")
|
.select_related("book", "shelf")
|
||||||
.first()
|
.first()
|
||||||
|
@ -84,8 +87,11 @@ def active_shelf(context, book):
|
||||||
@register.simple_tag(takes_context=False)
|
@register.simple_tag(takes_context=False)
|
||||||
def latest_read_through(book, user):
|
def latest_read_through(book, user):
|
||||||
"""the most recent read activity"""
|
"""the most recent read activity"""
|
||||||
|
if hasattr(book, "active_readthroughs"):
|
||||||
|
return book.active_readthroughs[0] if len(book.active_readthroughs) else None
|
||||||
|
|
||||||
return (
|
return (
|
||||||
models.ReadThrough.objects.filter(user=user, book=book)
|
models.ReadThrough.objects.filter(user=user, book=book, is_active=True)
|
||||||
.order_by("-start_date")
|
.order_by("-start_date")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
@ -97,4 +103,4 @@ def mutuals_count(context, user):
|
||||||
viewer = context["request"].user
|
viewer = context["request"].user
|
||||||
if not viewer.is_authenticated:
|
if not viewer.is_authenticated:
|
||||||
return None
|
return None
|
||||||
return user.followers.filter(id__in=viewer.following.all()).count()
|
return user.followers.filter(followers=viewer).count()
|
||||||
|
|
|
@ -12,7 +12,7 @@ register = template.Library()
|
||||||
@register.filter(name="uuid")
|
@register.filter(name="uuid")
|
||||||
def get_uuid(identifier):
|
def get_uuid(identifier):
|
||||||
"""for avoiding clashing ids when there are many forms"""
|
"""for avoiding clashing ids when there are many forms"""
|
||||||
return "%s%s" % (identifier, uuid4())
|
return f"{identifier}{uuid4()}"
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="username")
|
@register.filter(name="username")
|
||||||
|
@ -50,7 +50,7 @@ def truncatepath(value, arg):
|
||||||
length = int(arg)
|
length = int(arg)
|
||||||
except ValueError: # invalid literal for int()
|
except ValueError: # invalid literal for int()
|
||||||
return path_list[-1] # Fail silently.
|
return path_list[-1] # Fail silently.
|
||||||
return "%s/…%s" % (path_list[0], path_list[-1][-length:])
|
return f"{path_list[0]}/…{path_list[-1][-length:]}"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
@register.simple_tag(takes_context=False)
|
||||||
|
@ -60,7 +60,7 @@ def get_book_cover_thumbnail(book, size="medium", ext="jpg"):
|
||||||
if size == "":
|
if size == "":
|
||||||
size = "medium"
|
size = "medium"
|
||||||
try:
|
try:
|
||||||
cover_thumbnail = getattr(book, "cover_bw_book_%s_%s" % (size, ext))
|
cover_thumbnail = getattr(book, f"cover_bw_book_{size}_{ext}")
|
||||||
return cover_thumbnail.url
|
return cover_thumbnail.url
|
||||||
except OSError:
|
except OSError:
|
||||||
return static("images/no_cover.jpg")
|
return static("images/no_cover.jpg")
|
||||||
|
|
95
bookwyrm/tests/models/test_site.py
Normal file
95
bookwyrm/tests/models/test_site.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
""" testing models """
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bookwyrm import models, settings
|
||||||
|
|
||||||
|
|
||||||
|
class SiteModels(TestCase):
|
||||||
|
"""tests for site models"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""we need basic test data and mocks"""
|
||||||
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
):
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"mouse@local.com",
|
||||||
|
"mouse@mouse.com",
|
||||||
|
"mouseword",
|
||||||
|
local=True,
|
||||||
|
localname="mouse",
|
||||||
|
remote_id="https://example.com/users/mouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_site_settings_absent(self):
|
||||||
|
"""create and load site settings"""
|
||||||
|
self.assertFalse(models.SiteSettings.objects.exists())
|
||||||
|
result = models.SiteSettings.get()
|
||||||
|
self.assertTrue(models.SiteSettings.objects.exists())
|
||||||
|
self.assertEqual(result.id, 1)
|
||||||
|
self.assertEqual(result.name, "BookWyrm")
|
||||||
|
|
||||||
|
def test_site_settings_present(self):
|
||||||
|
"""load site settings"""
|
||||||
|
models.SiteSettings.objects.create(id=1, name="Fish Town")
|
||||||
|
result = models.SiteSettings.get()
|
||||||
|
self.assertEqual(result.id, 1)
|
||||||
|
self.assertEqual(result.name, "Fish Town")
|
||||||
|
self.assertEqual(models.SiteSettings.objects.all().count(), 1)
|
||||||
|
|
||||||
|
def test_site_invite(self):
|
||||||
|
"""default invite"""
|
||||||
|
invite = models.SiteInvite.objects.create(
|
||||||
|
user=self.local_user,
|
||||||
|
)
|
||||||
|
self.assertTrue(invite.valid())
|
||||||
|
|
||||||
|
def test_site_invite_with_limit(self):
|
||||||
|
"""with use limit"""
|
||||||
|
# valid
|
||||||
|
invite = models.SiteInvite.objects.create(user=self.local_user, use_limit=1)
|
||||||
|
self.assertTrue(invite.valid())
|
||||||
|
|
||||||
|
# invalid
|
||||||
|
invite = models.SiteInvite.objects.create(user=self.local_user, use_limit=0)
|
||||||
|
self.assertFalse(invite.valid())
|
||||||
|
invite = models.SiteInvite.objects.create(
|
||||||
|
user=self.local_user, use_limit=1, times_used=1
|
||||||
|
)
|
||||||
|
self.assertFalse(invite.valid())
|
||||||
|
|
||||||
|
def test_site_invite_with_expiry(self):
|
||||||
|
"""with expiration date"""
|
||||||
|
date = timezone.now() + timedelta(days=1)
|
||||||
|
invite = models.SiteInvite.objects.create(user=self.local_user, expiry=date)
|
||||||
|
self.assertTrue(invite.valid())
|
||||||
|
|
||||||
|
date = timezone.now() - timedelta(days=1)
|
||||||
|
invite = models.SiteInvite.objects.create(user=self.local_user, expiry=date)
|
||||||
|
self.assertFalse(invite.valid())
|
||||||
|
|
||||||
|
def test_site_invite_link(self):
|
||||||
|
"""invite link generator"""
|
||||||
|
invite = models.SiteInvite.objects.create(user=self.local_user, code="hello")
|
||||||
|
self.assertEqual(invite.link, f"https://{settings.DOMAIN}/invite/hello")
|
||||||
|
|
||||||
|
def test_invite_request(self):
|
||||||
|
"""someone wants an invite"""
|
||||||
|
# normal and good
|
||||||
|
request = models.InviteRequest.objects.create(email="mouse.reeve@gmail.com")
|
||||||
|
self.assertIsNone(request.invite)
|
||||||
|
|
||||||
|
# already in use
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
request = models.InviteRequest.objects.create(email="mouse@mouse.com")
|
||||||
|
|
||||||
|
def test_password_reset(self):
|
||||||
|
"""password reset token"""
|
||||||
|
token = models.PasswordReset.objects.create(user=self.local_user, code="hello")
|
||||||
|
self.assertTrue(token.valid())
|
||||||
|
self.assertEqual(token.link, f"https://{settings.DOMAIN}/password-reset/hello")
|
|
@ -20,6 +20,7 @@ from bookwyrm.preview_images import (
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
|
# pylint: disable=consider-using-with
|
||||||
class PreviewImages(TestCase):
|
class PreviewImages(TestCase):
|
||||||
"""every response to a get request, html or json"""
|
"""every response to a get request, html or json"""
|
||||||
|
|
||||||
|
@ -120,3 +121,11 @@ class PreviewImages(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.local_user.preview_image.height, settings.PREVIEW_IMG_HEIGHT
|
self.local_user.preview_image.height, settings.PREVIEW_IMG_HEIGHT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_generate_user_preview_images_task(self, *args, **kwargs):
|
||||||
|
"""test task's external calls"""
|
||||||
|
with patch("bookwyrm.preview_images.generate_preview_image") as generate_mock:
|
||||||
|
generate_user_preview_image_task(self.local_user.id)
|
||||||
|
args = generate_mock.call_args.kwargs
|
||||||
|
self.assertEqual(args["texts"]["text_one"], "possum")
|
||||||
|
self.assertEqual(args["texts"]["text_three"], f"@possum@{settings.DOMAIN}")
|
||||||
|
|
|
@ -9,6 +9,7 @@ import responses
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.http import Http404
|
||||||
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
|
||||||
|
@ -133,8 +134,8 @@ class BookViews(TestCase):
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
with patch("bookwyrm.views.books.is_api_request") as is_api:
|
||||||
is_api.return_value = False
|
is_api.return_value = False
|
||||||
result = view(request, 0)
|
with self.assertRaises(Http404):
|
||||||
self.assertEqual(result.status_code, 404)
|
view(request, 0)
|
||||||
|
|
||||||
def test_book_page_work_id(self):
|
def test_book_page_work_id(self):
|
||||||
"""there are so many views, this just makes sure it LOADS"""
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
|
38
bookwyrm/tests/views/test_dashboard.py
Normal file
38
bookwyrm/tests/views/test_dashboard.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
""" test for app action functionality """
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardViews(TestCase):
|
||||||
|
"""every response to a get request, html or json"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""we need basic test data and mocks"""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
):
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"mouse@local.com",
|
||||||
|
"mouse@mouse.mouse",
|
||||||
|
"password",
|
||||||
|
local=True,
|
||||||
|
localname="mouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
|
def test_dashboard(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.Dashboard.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
result.render()
|
||||||
|
self.assertEqual(result.status_code, 200)
|
|
@ -75,7 +75,7 @@ class FederationViews(TestCase):
|
||||||
|
|
||||||
self.assertEqual(server.status, "federated")
|
self.assertEqual(server.status, "federated")
|
||||||
|
|
||||||
view = views.federation.block_server
|
view = views.block_server
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
@ -121,7 +121,7 @@ class FederationViews(TestCase):
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
|
||||||
with patch("bookwyrm.suggested_users.bulk_add_instance_task.delay") as mock:
|
with patch("bookwyrm.suggested_users.bulk_add_instance_task.delay") as mock:
|
||||||
views.federation.unblock_server(request, server.id)
|
views.unblock_server(request, server.id)
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
self.assertEqual(mock.call_args[0][0], server.id)
|
self.assertEqual(mock.call_args[0][0], server.id)
|
||||||
|
|
||||||
|
|
|
@ -43,12 +43,14 @@ class PasswordViews(TestCase):
|
||||||
def test_password_reset_request_post(self):
|
def test_password_reset_request_post(self):
|
||||||
"""send 'em an email"""
|
"""send 'em an email"""
|
||||||
request = self.factory.post("", {"email": "aa@bb.ccc"})
|
request = self.factory.post("", {"email": "aa@bb.ccc"})
|
||||||
|
request.user = self.anonymous_user
|
||||||
view = views.PasswordResetRequest.as_view()
|
view = views.PasswordResetRequest.as_view()
|
||||||
resp = view(request)
|
resp = view(request)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
resp.render()
|
resp.render()
|
||||||
|
|
||||||
request = self.factory.post("", {"email": "mouse@mouse.com"})
|
request = self.factory.post("", {"email": "mouse@mouse.com"})
|
||||||
|
request.user = self.anonymous_user
|
||||||
with patch("bookwyrm.emailing.send_email.delay"):
|
with patch("bookwyrm.emailing.send_email.delay"):
|
||||||
resp = view(request)
|
resp = view(request)
|
||||||
resp.render()
|
resp.render()
|
||||||
|
|
|
@ -113,6 +113,7 @@ class ReadingViews(TestCase):
|
||||||
{
|
{
|
||||||
"post-status": True,
|
"post-status": True,
|
||||||
"privacy": "followers",
|
"privacy": "followers",
|
||||||
|
"start_date": readthrough.start_date,
|
||||||
"finish_date": timezone.now().isoformat(),
|
"finish_date": timezone.now().isoformat(),
|
||||||
"id": readthrough.id,
|
"id": readthrough.id,
|
||||||
},
|
},
|
||||||
|
|
|
@ -101,7 +101,7 @@ class StatusViews(TestCase):
|
||||||
"""@mention a user in a post"""
|
"""@mention a user in a post"""
|
||||||
view = views.CreateStatus.as_view()
|
view = views.CreateStatus.as_view()
|
||||||
user = models.User.objects.create_user(
|
user = models.User.objects.create_user(
|
||||||
"rat@%s" % DOMAIN,
|
f"rat@{DOMAIN}",
|
||||||
"rat@rat.com",
|
"rat@rat.com",
|
||||||
"password",
|
"password",
|
||||||
local=True,
|
local=True,
|
||||||
|
@ -124,7 +124,7 @@ class StatusViews(TestCase):
|
||||||
self.assertEqual(list(status.mention_users.all()), [user])
|
self.assertEqual(list(status.mention_users.all()), [user])
|
||||||
self.assertEqual(models.Notification.objects.get().user, user)
|
self.assertEqual(models.Notification.objects.get().user, user)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
status.content, '<p>hi <a href="%s">@rat</a></p>' % user.remote_id
|
status.content, f'<p>hi <a href="{user.remote_id}">@rat</a></p>'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_handle_status_reply_with_mentions(self, *_):
|
def test_handle_status_reply_with_mentions(self, *_):
|
||||||
|
@ -224,13 +224,13 @@ class StatusViews(TestCase):
|
||||||
def test_find_mentions(self, *_):
|
def test_find_mentions(self, *_):
|
||||||
"""detect and look up @ mentions of users"""
|
"""detect and look up @ mentions of users"""
|
||||||
user = models.User.objects.create_user(
|
user = models.User.objects.create_user(
|
||||||
"nutria@%s" % DOMAIN,
|
f"nutria@{DOMAIN}",
|
||||||
"nutria@nutria.com",
|
"nutria@nutria.com",
|
||||||
"password",
|
"password",
|
||||||
local=True,
|
local=True,
|
||||||
localname="nutria",
|
localname="nutria",
|
||||||
)
|
)
|
||||||
self.assertEqual(user.username, "nutria@%s" % DOMAIN)
|
self.assertEqual(user.username, f"nutria@{DOMAIN}")
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
list(views.status.find_mentions("@nutria"))[0], ("@nutria", user)
|
list(views.status.find_mentions("@nutria"))[0], ("@nutria", user)
|
||||||
|
@ -263,19 +263,19 @@ class StatusViews(TestCase):
|
||||||
self.assertEqual(list(views.status.find_mentions("@beep@beep.com")), [])
|
self.assertEqual(list(views.status.find_mentions("@beep@beep.com")), [])
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
list(views.status.find_mentions("@nutria@%s" % DOMAIN))[0],
|
list(views.status.find_mentions(f"@nutria@{DOMAIN}"))[0],
|
||||||
("@nutria@%s" % DOMAIN, user),
|
(f"@nutria@{DOMAIN}", user),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_format_links_simple_url(self, *_):
|
def test_format_links_simple_url(self, *_):
|
||||||
"""find and format urls into a tags"""
|
"""find and format urls into a tags"""
|
||||||
url = "http://www.fish.com/"
|
url = "http://www.fish.com/"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url), '<a href="%s">www.fish.com/</a>' % url
|
views.status.format_links(url), f'<a href="{url}">www.fish.com/</a>'
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links("(%s)" % url),
|
views.status.format_links(f"({url})"),
|
||||||
'(<a href="%s">www.fish.com/</a>)' % url,
|
f'(<a href="{url}">www.fish.com/</a>)',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_format_links_paragraph_break(self, *_):
|
def test_format_links_paragraph_break(self, *_):
|
||||||
|
@ -292,8 +292,8 @@ http://www.fish.com/"""
|
||||||
"""find and format urls into a tags"""
|
"""find and format urls into a tags"""
|
||||||
url = "http://www.fish.com/"
|
url = "http://www.fish.com/"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links("(%s)" % url),
|
views.status.format_links(f"({url})"),
|
||||||
'(<a href="%s">www.fish.com/</a>)' % url,
|
f'(<a href="{url}">www.fish.com/</a>)',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_format_links_special_chars(self, *_):
|
def test_format_links_special_chars(self, *_):
|
||||||
|
@ -301,27 +301,27 @@ http://www.fish.com/"""
|
||||||
url = "https://archive.org/details/dli.granth.72113/page/n25/mode/2up"
|
url = "https://archive.org/details/dli.granth.72113/page/n25/mode/2up"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url),
|
views.status.format_links(url),
|
||||||
'<a href="%s">'
|
f'<a href="{url}">'
|
||||||
"archive.org/details/dli.granth.72113/page/n25/mode/2up</a>" % url,
|
"archive.org/details/dli.granth.72113/page/n25/mode/2up</a>",
|
||||||
)
|
)
|
||||||
url = "https://openlibrary.org/search?q=arkady+strugatsky&mode=everything"
|
url = "https://openlibrary.org/search?q=arkady+strugatsky&mode=everything"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url),
|
views.status.format_links(url),
|
||||||
'<a href="%s">openlibrary.org/search'
|
f'<a href="{url}">openlibrary.org/search'
|
||||||
"?q=arkady+strugatsky&mode=everything</a>" % url,
|
"?q=arkady+strugatsky&mode=everything</a>",
|
||||||
)
|
)
|
||||||
url = "https://tech.lgbt/@bookwyrm"
|
url = "https://tech.lgbt/@bookwyrm"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url), '<a href="%s">tech.lgbt/@bookwyrm</a>' % url
|
views.status.format_links(url), f'<a href="{url}">tech.lgbt/@bookwyrm</a>'
|
||||||
)
|
)
|
||||||
url = "https://users.speakeasy.net/~lion/nb/book.pdf"
|
url = "https://users.speakeasy.net/~lion/nb/book.pdf"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url),
|
views.status.format_links(url),
|
||||||
'<a href="%s">users.speakeasy.net/~lion/nb/book.pdf</a>' % url,
|
f'<a href="{url}">users.speakeasy.net/~lion/nb/book.pdf</a>',
|
||||||
)
|
)
|
||||||
url = "https://pkm.one/#/page/The%20Book%20which%20launched%20a%201000%20Note%20taking%20apps"
|
url = "https://pkm.one/#/page/The%20Book%20launched%20a%201000%20Note%20apps"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
views.status.format_links(url), '<a href="%s">%s</a>' % (url, url[8:])
|
views.status.format_links(url), f'<a href="{url}">{url[8:]}</a>'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_to_markdown(self, *_):
|
def test_to_markdown(self, *_):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.test.client import RequestFactory
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
|
|
||||||
|
|
||||||
class UserViews(TestCase):
|
class WellknownViews(TestCase):
|
||||||
"""view user and edit profile"""
|
"""view user and edit profile"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -95,3 +95,22 @@ class UserViews(TestCase):
|
||||||
self.assertIsInstance(result, JsonResponse)
|
self.assertIsInstance(result, JsonResponse)
|
||||||
self.assertEqual(data["stats"]["user_count"], 2)
|
self.assertEqual(data["stats"]["user_count"], 2)
|
||||||
self.assertEqual(models.User.objects.count(), 3)
|
self.assertEqual(models.User.objects.count(), 3)
|
||||||
|
|
||||||
|
def test_peers(self):
|
||||||
|
"""who's federating with whom"""
|
||||||
|
models.FederatedServer.objects.create(
|
||||||
|
server_name="test.server",
|
||||||
|
status="federated",
|
||||||
|
)
|
||||||
|
models.FederatedServer.objects.create(
|
||||||
|
server_name="another.test.server",
|
||||||
|
status="blocked",
|
||||||
|
)
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.anonymous_user
|
||||||
|
|
||||||
|
result = views.peers(request)
|
||||||
|
data = json.loads(result.getvalue())
|
||||||
|
self.assertIsInstance(result, JsonResponse)
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0], "test.server")
|
||||||
|
|
|
@ -7,8 +7,8 @@ from django.views.generic.base import TemplateView
|
||||||
from bookwyrm import settings, views
|
from bookwyrm import settings, views
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
USER_PATH = r"^user/(?P<username>%s)" % regex.USERNAME
|
USER_PATH = rf"^user/(?P<username>{regex.USERNAME})"
|
||||||
LOCAL_USER_PATH = r"^user/(?P<username>%s)" % regex.LOCALNAME
|
LOCAL_USER_PATH = rf"^user/(?P<username>{regex.LOCALNAME})"
|
||||||
|
|
||||||
status_types = [
|
status_types = [
|
||||||
"status",
|
"status",
|
||||||
|
@ -19,7 +19,9 @@ status_types = [
|
||||||
"boost",
|
"boost",
|
||||||
"generatednote",
|
"generatednote",
|
||||||
]
|
]
|
||||||
STATUS_PATH = r"%s/(%s)/(?P<status_id>\d+)" % (USER_PATH, "|".join(status_types))
|
|
||||||
|
STATUS_TYPES_STRING = "|".join(status_types)
|
||||||
|
STATUS_PATH = rf"{USER_PATH}/({STATUS_TYPES_STRING})/(?P<status_id>\d+)"
|
||||||
|
|
||||||
BOOK_PATH = r"^book/(?P<book_id>\d+)"
|
BOOK_PATH = r"^book/(?P<book_id>\d+)"
|
||||||
|
|
||||||
|
@ -33,8 +35,8 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
# federation endpoints
|
# federation endpoints
|
||||||
re_path(r"^inbox/?$", views.Inbox.as_view()),
|
re_path(r"^inbox/?$", views.Inbox.as_view()),
|
||||||
re_path(r"%s/inbox/?$" % LOCAL_USER_PATH, views.Inbox.as_view()),
|
re_path(rf"{LOCAL_USER_PATH}/inbox/?$", views.Inbox.as_view()),
|
||||||
re_path(r"%s/outbox/?$" % LOCAL_USER_PATH, views.Outbox.as_view()),
|
re_path(rf"{LOCAL_USER_PATH}/outbox/?$", views.Outbox.as_view()),
|
||||||
re_path(r"^\.well-known/webfinger/?$", views.webfinger),
|
re_path(r"^\.well-known/webfinger/?$", views.webfinger),
|
||||||
re_path(r"^\.well-known/nodeinfo/?$", views.nodeinfo_pointer),
|
re_path(r"^\.well-known/nodeinfo/?$", views.nodeinfo_pointer),
|
||||||
re_path(r"^\.well-known/host-meta/?$", views.host_meta),
|
re_path(r"^\.well-known/host-meta/?$", views.host_meta),
|
||||||
|
@ -55,7 +57,7 @@ urlpatterns = [
|
||||||
views.ConfirmEmailCode.as_view(),
|
views.ConfirmEmailCode.as_view(),
|
||||||
name="confirm-email-code",
|
name="confirm-email-code",
|
||||||
),
|
),
|
||||||
re_path(r"resend-link", views.resend_link, name="resend-link"),
|
re_path(r"^resend-link/?$", views.resend_link, name="resend-link"),
|
||||||
re_path(r"^logout/?$", views.Logout.as_view(), name="logout"),
|
re_path(r"^logout/?$", views.Logout.as_view(), name="logout"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^password-reset/?$",
|
r"^password-reset/?$",
|
||||||
|
@ -66,6 +68,9 @@ urlpatterns = [
|
||||||
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
|
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
|
||||||
),
|
),
|
||||||
# admin
|
# admin
|
||||||
|
re_path(
|
||||||
|
r"^settings/dashboard/?$", views.Dashboard.as_view(), name="settings-dashboard"
|
||||||
|
),
|
||||||
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
|
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/announcements/?$",
|
r"^settings/announcements/?$",
|
||||||
|
@ -84,7 +89,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/email-preview/?$",
|
r"^settings/email-preview/?$",
|
||||||
views.site.email_preview,
|
views.admin.site.email_preview,
|
||||||
name="settings-email-preview",
|
name="settings-email-preview",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
|
@ -96,7 +101,7 @@ urlpatterns = [
|
||||||
name="settings-user",
|
name="settings-user",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/federation/?$",
|
r"^settings/federation/(?P<status>(federated|blocked))?/?$",
|
||||||
views.Federation.as_view(),
|
views.Federation.as_view(),
|
||||||
name="settings-federation",
|
name="settings-federation",
|
||||||
),
|
),
|
||||||
|
@ -107,12 +112,12 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/federation/(?P<server>\d+)/block?$",
|
r"^settings/federation/(?P<server>\d+)/block?$",
|
||||||
views.federation.block_server,
|
views.block_server,
|
||||||
name="settings-federated-server-block",
|
name="settings-federated-server-block",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/federation/(?P<server>\d+)/unblock?$",
|
r"^settings/federation/(?P<server>\d+)/unblock?$",
|
||||||
views.federation.unblock_server,
|
views.unblock_server,
|
||||||
name="settings-federated-server-unblock",
|
name="settings-federated-server-unblock",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
|
@ -152,6 +157,16 @@ urlpatterns = [
|
||||||
views.EmailBlocklist.as_view(),
|
views.EmailBlocklist.as_view(),
|
||||||
name="settings-email-blocks-delete",
|
name="settings-email-blocks-delete",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^settings/ip-blocklist/?$",
|
||||||
|
views.IPBlocklist.as_view(),
|
||||||
|
name="settings-ip-blocks",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^settings/ip-blocks/(?P<block_id>\d+)/delete/?$",
|
||||||
|
views.IPBlocklist.as_view(),
|
||||||
|
name="settings-ip-blocks-delete",
|
||||||
|
),
|
||||||
# moderation
|
# moderation
|
||||||
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
|
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
|
||||||
re_path(
|
re_path(
|
||||||
|
@ -208,12 +223,12 @@ urlpatterns = [
|
||||||
name="get-started-users",
|
name="get-started-users",
|
||||||
),
|
),
|
||||||
# feeds
|
# feeds
|
||||||
re_path(r"^(?P<tab>{:s})/?$".format(STREAMS), views.Feed.as_view()),
|
re_path(rf"^(?P<tab>{STREAMS})/?$", views.Feed.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages"
|
r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages"
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^direct-messages/(?P<username>%s)?$" % regex.USERNAME,
|
rf"^direct-messages/(?P<username>{regex.USERNAME})?$",
|
||||||
views.DirectMessage.as_view(),
|
views.DirectMessage.as_view(),
|
||||||
name="direct-messages-user",
|
name="direct-messages-user",
|
||||||
),
|
),
|
||||||
|
@ -223,22 +238,22 @@ urlpatterns = [
|
||||||
re_path(r"^import/?$", views.Import.as_view(), name="import"),
|
re_path(r"^import/?$", views.Import.as_view(), name="import"),
|
||||||
re_path(r"^import/(\d+)/?$", views.ImportStatus.as_view(), name="import-status"),
|
re_path(r"^import/(\d+)/?$", views.ImportStatus.as_view(), name="import-status"),
|
||||||
# users
|
# users
|
||||||
re_path(r"%s\.json$" % USER_PATH, views.User.as_view()),
|
re_path(rf"{USER_PATH}\.json$", views.User.as_view()),
|
||||||
re_path(r"%s/?$" % USER_PATH, views.User.as_view(), name="user-feed"),
|
re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"),
|
||||||
re_path(r"%s/rss" % USER_PATH, views.rss_feed.RssFeed(), name="user-rss"),
|
re_path(rf"{USER_PATH}/rss/?$", views.rss_feed.RssFeed(), name="user-rss"),
|
||||||
re_path(
|
re_path(
|
||||||
r"%s/followers(.json)?/?$" % USER_PATH,
|
rf"{USER_PATH}/followers(.json)?/?$",
|
||||||
views.Followers.as_view(),
|
views.Followers.as_view(),
|
||||||
name="user-followers",
|
name="user-followers",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"%s/following(.json)?/?$" % USER_PATH,
|
rf"{USER_PATH}/following(.json)?/?$",
|
||||||
views.Following.as_view(),
|
views.Following.as_view(),
|
||||||
name="user-following",
|
name="user-following",
|
||||||
),
|
),
|
||||||
re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"),
|
re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"),
|
||||||
# lists
|
# lists
|
||||||
re_path(r"%s/lists/?$" % USER_PATH, 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"),
|
||||||
|
@ -260,14 +275,14 @@ urlpatterns = [
|
||||||
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
|
re_path(r"^save-list/(?P<list_id>\d+)/?$", views.save_list, name="list-save"),
|
||||||
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
|
re_path(r"^unsave-list/(?P<list_id>\d+)/?$", views.unsave_list, name="list-unsave"),
|
||||||
# User books
|
# User books
|
||||||
re_path(r"%s/books/?$" % USER_PATH, views.Shelf.as_view(), name="user-shelves"),
|
re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^%s/(helf|books)/(?P<shelf_identifier>[\w-]+)(.json)?/?$" % USER_PATH,
|
rf"^{USER_PATH}/(helf|books)/(?P<shelf_identifier>[\w-]+)(.json)?/?$",
|
||||||
views.Shelf.as_view(),
|
views.Shelf.as_view(),
|
||||||
name="shelf",
|
name="shelf",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^%s/(books|shelf)/(?P<shelf_identifier>[\w-]+)(.json)?/?$" % LOCAL_USER_PATH,
|
rf"^{LOCAL_USER_PATH}/(books|shelf)/(?P<shelf_identifier>[\w-]+)(.json)?/?$",
|
||||||
views.Shelf.as_view(),
|
views.Shelf.as_view(),
|
||||||
name="shelf",
|
name="shelf",
|
||||||
),
|
),
|
||||||
|
@ -277,7 +292,7 @@ urlpatterns = [
|
||||||
re_path(r"^unshelve/?$", views.unshelve),
|
re_path(r"^unshelve/?$", views.unshelve),
|
||||||
# goals
|
# goals
|
||||||
re_path(
|
re_path(
|
||||||
r"%s/goal/(?P<year>\d{4})/?$" % USER_PATH,
|
rf"{LOCAL_USER_PATH}/goal/(?P<year>\d+)/?$",
|
||||||
views.Goal.as_view(),
|
views.Goal.as_view(),
|
||||||
name="user-goal",
|
name="user-goal",
|
||||||
),
|
),
|
||||||
|
@ -294,10 +309,10 @@ urlpatterns = [
|
||||||
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
||||||
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
|
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
|
||||||
# statuses
|
# statuses
|
||||||
re_path(r"%s(.json)?/?$" % STATUS_PATH, views.Status.as_view(), name="status"),
|
re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"),
|
||||||
re_path(r"%s/activity/?$" % STATUS_PATH, views.Status.as_view(), name="status"),
|
re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"),
|
||||||
re_path(
|
re_path(
|
||||||
r"%s/replies(.json)?/?$" % STATUS_PATH, views.Replies.as_view(), name="replies"
|
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^post/?$",
|
r"^post/?$",
|
||||||
|
@ -327,17 +342,17 @@ urlpatterns = [
|
||||||
re_path(r"^boost/(?P<status_id>\d+)/?$", views.Boost.as_view()),
|
re_path(r"^boost/(?P<status_id>\d+)/?$", views.Boost.as_view()),
|
||||||
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(r"%s(.json)?/?$" % BOOK_PATH, views.Book.as_view(), name="book"),
|
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
|
||||||
re_path(
|
re_path(
|
||||||
r"%s/(?P<user_statuses>review|comment|quote)/?$" % BOOK_PATH,
|
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(r"%s/edit/?$" % BOOK_PATH, views.EditBook.as_view(), name="edit-book"),
|
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
|
||||||
re_path(r"%s/confirm/?$" % BOOK_PATH, views.ConfirmEditBook.as_view()),
|
re_path(rf"{BOOK_PATH}/confirm/?$", views.ConfirmEditBook.as_view()),
|
||||||
re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"),
|
re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"),
|
||||||
re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()),
|
re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()),
|
||||||
re_path(r"%s/editions(.json)?/?$" % BOOK_PATH, 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"
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
DOMAIN = r"[\w_\-\.]+\.[a-z]{2,}"
|
DOMAIN = r"[\w_\-\.]+\.[a-z]{2,}"
|
||||||
LOCALNAME = r"@?[a-zA-Z_\-\.0-9]+"
|
LOCALNAME = r"@?[a-zA-Z_\-\.0-9]+"
|
||||||
STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
|
STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
|
||||||
USERNAME = r"%s(@%s)?" % (LOCALNAME, DOMAIN)
|
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"
|
||||||
STRICT_USERNAME = r"\B%s(@%s)?\b" % (STRICT_LOCALNAME, DOMAIN)
|
STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b"
|
||||||
FULL_USERNAME = r"%s@%s\b" % (LOCALNAME, DOMAIN)
|
FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b"
|
||||||
# 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]+;"
|
||||||
|
|
|
@ -1,40 +1,14 @@
|
||||||
""" make sure all our nice views are available """
|
""" make sure all our nice views are available """
|
||||||
from .announcements import Announcements, Announcement, delete_announcement
|
from .admin.announcements import Announcements, Announcement, delete_announcement
|
||||||
from .author import Author, EditAuthor
|
from .admin.dashboard import Dashboard
|
||||||
from .block import Block, unblock
|
from .admin.federation import Federation, FederatedServer
|
||||||
from .books import Book, EditBook, ConfirmEditBook
|
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||||
from .books import upload_cover, add_description, resolve_book
|
from .admin.federation import block_server, unblock_server
|
||||||
from .directory import Directory
|
from .admin.email_blocklist import EmailBlocklist
|
||||||
from .discover import Discover
|
from .admin.ip_blocklist import IPBlocklist
|
||||||
from .edit_user import EditUser, DeleteUser
|
from .admin.invite import ManageInvites, Invite, InviteRequest
|
||||||
from .editions import Editions, switch_edition
|
from .admin.invite import ManageInviteRequests, ignore_invite_request
|
||||||
from .email_blocklist import EmailBlocklist
|
from .admin.reports import (
|
||||||
from .federation import Federation, FederatedServer
|
|
||||||
from .federation import AddFederatedServer, ImportServerBlocklist
|
|
||||||
from .federation import block_server, unblock_server
|
|
||||||
from .feed import DirectMessage, Feed, Replies, Status
|
|
||||||
from .follow import follow, unfollow
|
|
||||||
from .follow import accept_follow_request, delete_follow_request
|
|
||||||
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
|
||||||
from .goal import Goal, hide_goal
|
|
||||||
from .import_data import Import, ImportStatus
|
|
||||||
from .inbox import Inbox
|
|
||||||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
|
||||||
from .invite import ManageInvites, Invite, InviteRequest
|
|
||||||
from .invite import ManageInviteRequests, ignore_invite_request
|
|
||||||
from .isbn import Isbn
|
|
||||||
from .landing import About, Home, Landing
|
|
||||||
from .list import Lists, SavedLists, List, Curate, UserLists
|
|
||||||
from .list import save_list, unsave_list
|
|
||||||
from .list import delete_list
|
|
||||||
from .login import Login, Logout
|
|
||||||
from .notifications import Notifications
|
|
||||||
from .outbox import Outbox
|
|
||||||
from .reading import edit_readthrough, create_readthrough
|
|
||||||
from .reading import delete_readthrough, delete_progressupdate
|
|
||||||
from .reading import ReadingStatus
|
|
||||||
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
|
|
||||||
from .reports import (
|
|
||||||
Report,
|
Report,
|
||||||
Reports,
|
Reports,
|
||||||
make_report,
|
make_report,
|
||||||
|
@ -43,15 +17,42 @@ from .reports import (
|
||||||
unsuspend_user,
|
unsuspend_user,
|
||||||
moderator_delete_user,
|
moderator_delete_user,
|
||||||
)
|
)
|
||||||
|
from .admin.site import Site
|
||||||
|
from .admin.user_admin import UserAdmin, UserAdminList
|
||||||
|
from .author import Author, EditAuthor
|
||||||
|
from .block import Block, unblock
|
||||||
|
from .books import Book, EditBook, ConfirmEditBook
|
||||||
|
from .books import upload_cover, add_description, resolve_book
|
||||||
|
from .directory import Directory
|
||||||
|
from .discover import Discover
|
||||||
|
from .edit_user import EditUser, DeleteUser
|
||||||
|
from .editions import Editions, switch_edition
|
||||||
|
from .feed import DirectMessage, Feed, Replies, Status
|
||||||
|
from .follow import follow, unfollow
|
||||||
|
from .follow import accept_follow_request, delete_follow_request
|
||||||
|
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
|
||||||
|
from .goal import Goal, hide_goal
|
||||||
|
from .import_data import Import, ImportStatus
|
||||||
|
from .inbox import Inbox
|
||||||
|
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||||
|
from .isbn import Isbn
|
||||||
|
from .landing import About, Home, Landing
|
||||||
|
from .list import Lists, SavedLists, List, Curate, UserLists
|
||||||
|
from .list import save_list, unsave_list, delete_list
|
||||||
|
from .login import Login, Logout
|
||||||
|
from .notifications import Notifications
|
||||||
|
from .outbox import Outbox
|
||||||
|
from .reading import edit_readthrough, create_readthrough
|
||||||
|
from .reading import delete_readthrough, delete_progressupdate
|
||||||
|
from .reading import ReadingStatus
|
||||||
|
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
|
||||||
from .rss_feed import RssFeed
|
from .rss_feed import RssFeed
|
||||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .shelf import Shelf
|
from .shelf import Shelf
|
||||||
from .shelf import create_shelf, delete_shelf
|
from .shelf import create_shelf, delete_shelf
|
||||||
from .shelf import shelve, unshelve
|
from .shelf import shelve, unshelve
|
||||||
from .site import Site
|
|
||||||
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
||||||
from .updates import get_notification_count, get_unread_status_count
|
from .updates import get_notification_count, get_unread_status_count
|
||||||
from .user import User, Followers, Following, hide_suggestions
|
from .user import User, Followers, Following, hide_suggestions
|
||||||
from .user_admin import UserAdmin, UserAdminList
|
|
||||||
from .wellknown import *
|
from .wellknown import *
|
||||||
|
|
0
bookwyrm/views/admin/__init__.py
Normal file
0
bookwyrm/views/admin/__init__.py
Normal file
|
@ -31,6 +31,7 @@ class Announcements(View):
|
||||||
"end_date",
|
"end_date",
|
||||||
"active",
|
"active",
|
||||||
]
|
]
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||||
announcements = announcements.order_by(sort)
|
announcements = announcements.order_by(sort)
|
||||||
data = {
|
data = {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue