forked from mirrors/bookwyrm
Merge branch 'main' into book-format-choices
This commit is contained in:
commit
bc87856c2e
199 changed files with 7647 additions and 3289 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)
|
||||||
|
@ -213,14 +213,14 @@ class ActivityObject:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def set_related_field(
|
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"""
|
||||||
|
@ -355,7 +358,7 @@ def populate_streams_on_account_create(sender, instance, created, *args, **kwarg
|
||||||
if not created or not instance.local:
|
if not created or not instance.local:
|
||||||
return
|
return
|
||||||
|
|
||||||
for stream in streams.values():
|
for stream in streams:
|
||||||
populate_stream_task.delay(stream, instance.id)
|
populate_stream_task.delay(stream, instance.id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -395,7 +398,7 @@ def remove_statuses_on_unshelve(sender, instance, *args, **kwargs):
|
||||||
# ---- TASKS
|
# ---- TASKS
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def add_book_statuses_task(user_id, book_id):
|
def add_book_statuses_task(user_id, book_id):
|
||||||
"""add statuses related to a book on shelve"""
|
"""add statuses related to a book on shelve"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -403,7 +406,7 @@ def add_book_statuses_task(user_id, book_id):
|
||||||
BooksStream().add_book_statuses(user, book)
|
BooksStream().add_book_statuses(user, book)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def remove_book_statuses_task(user_id, book_id):
|
def remove_book_statuses_task(user_id, book_id):
|
||||||
"""remove statuses about a book from a user's books feed"""
|
"""remove statuses about a book from a user's books feed"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -411,7 +414,7 @@ def remove_book_statuses_task(user_id, book_id):
|
||||||
BooksStream().remove_book_statuses(user, book)
|
BooksStream().remove_book_statuses(user, book)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def populate_stream_task(stream, user_id):
|
def populate_stream_task(stream, user_id):
|
||||||
"""background task for populating an empty activitystream"""
|
"""background task for populating an empty activitystream"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -419,7 +422,7 @@ def populate_stream_task(stream, user_id):
|
||||||
stream.populate_streams(user)
|
stream.populate_streams(user)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def remove_status_task(status_ids):
|
def remove_status_task(status_ids):
|
||||||
"""remove a status from any stream it might be in"""
|
"""remove a status from any stream it might be in"""
|
||||||
# this can take an id or a list of ids
|
# this can take an id or a list of ids
|
||||||
|
@ -432,15 +435,19 @@ def remove_status_task(status_ids):
|
||||||
stream.remove_object_from_related_stores(status)
|
stream.remove_object_from_related_stores(status)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="high_priority")
|
||||||
def add_status_task(status_id, increment_unread=False):
|
def add_status_task(status_id, increment_unread=False):
|
||||||
"""remove a status from any stream it might 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)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
|
def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
"""remove all statuses by a user from a viewer's stream"""
|
"""remove all statuses by a user from a viewer's stream"""
|
||||||
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
||||||
|
@ -450,9 +457,9 @@ def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
stream.remove_user_statuses(viewer, user)
|
stream.remove_user_statuses(viewer, user)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def add_user_statuses_task(viewer_id, user_id, stream_list=None):
|
def add_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
"""remove all statuses by a user from a viewer's stream"""
|
"""add all statuses by a user to a viewer's stream"""
|
||||||
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
||||||
viewer = models.User.objects.get(id=viewer_id)
|
viewer = models.User.objects.get(id=viewer_id)
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -460,7 +467,7 @@ def add_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
stream.add_user_statuses(viewer, user)
|
stream.add_user_statuses(viewer, user)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def handle_boost_task(boost_id):
|
def handle_boost_task(boost_id):
|
||||||
"""remove the original post and other, earlier boosts"""
|
"""remove the original post and other, earlier boosts"""
|
||||||
instance = models.Status.objects.get(id=boost_id)
|
instance = models.Status.objects.get(id=boost_id)
|
||||||
|
@ -469,7 +476,7 @@ def handle_boost_task(boost_id):
|
||||||
old_versions = models.Boost.objects.filter(
|
old_versions = models.Boost.objects.filter(
|
||||||
boosted_status__id=boosted.id,
|
boosted_status__id=boosted.id,
|
||||||
created_date__lt=instance.created_date,
|
created_date__lt=instance.created_date,
|
||||||
).values_list("id", flat=True)
|
)
|
||||||
|
|
||||||
for stream in streams.values():
|
for stream in streams.values():
|
||||||
audience = stream.get_stores_for_object(instance)
|
audience = stream.get_stores_for_object(instance)
|
||||||
|
|
|
@ -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,17 +109,17 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
return load_connector(connector_info)
|
return load_connector(connector_info)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def load_more_data(connector_id, book_id):
|
def load_more_data(connector_id, book_id):
|
||||||
"""background the work of getting all 10,000 editions of LoTR"""
|
"""background the work of getting all 10,000 editions of LoTR"""
|
||||||
connector_info = models.Connector.objects.get(id=connector_id)
|
connector_info = models.Connector.objects.get(id=connector_id)
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -15,4 +15,5 @@ def site_settings(request): # pylint: disable=unused-argument
|
||||||
"media_full_url": settings.MEDIA_FULL_URL,
|
"media_full_url": settings.MEDIA_FULL_URL,
|
||||||
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
|
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
|
||||||
"request_protocol": request_protocol,
|
"request_protocol": request_protocol,
|
||||||
|
"js_cache": settings.JS_CACHE,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,23 +48,17 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="high_priority")
|
||||||
def send_email(recipient, subject, html_content, text_content):
|
def send_email(recipient, subject, html_content, text_content):
|
||||||
"""use a task to send the email"""
|
"""use a task to send the email"""
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
|
|
|
@ -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
|
||||||
|
@ -134,6 +140,7 @@ class EditUserForm(CustomForm):
|
||||||
"email",
|
"email",
|
||||||
"summary",
|
"summary",
|
||||||
"show_goal",
|
"show_goal",
|
||||||
|
"show_suggested_users",
|
||||||
"manually_approves_followers",
|
"manually_approves_followers",
|
||||||
"default_post_privacy",
|
"default_post_privacy",
|
||||||
"discoverable",
|
"discoverable",
|
||||||
|
@ -253,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"))]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -298,6 +302,18 @@ class ReportForm(CustomForm):
|
||||||
fields = ["user", "reporter", "statuses", "note"]
|
fields = ["user", "reporter", "statuses", "note"]
|
||||||
|
|
||||||
|
|
||||||
|
class EmailBlocklistForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.EmailBlocklist
|
||||||
|
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
|
||||||
|
@ -61,7 +62,7 @@ class Importer:
|
||||||
job.save()
|
job.save()
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def import_data(source, job_id):
|
def import_data(source, job_id):
|
||||||
"""does the actual lookup work in a celery task"""
|
"""does the actual lookup work in a celery task"""
|
||||||
job = ImportJob.objects.get(id=job_id)
|
job = ImportJob.objects.get(id=job_id)
|
||||||
|
@ -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)
|
18
bookwyrm/migrations/0089_user_show_suggested_users.py
Normal file
18
bookwyrm/migrations/0089_user_show_suggested_users.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-08 16:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0088_auto_20210905_2233"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="show_suggested_users",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
45
bookwyrm/migrations/0090_auto_20210908_2346.py
Normal file
45
bookwyrm/migrations/0090_auto_20210908_2346.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-08 23:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0089_user_show_suggested_users"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
bookwyrm/migrations/0090_emailblocklist.py
Normal file
32
bookwyrm/migrations/0090_emailblocklist.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-08 22:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0089_user_show_suggested_users"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="EmailBlocklist",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("domain", models.CharField(max_length=255, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("-created_date",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-09 00:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0090_auto_20210908_2346"),
|
||||||
|
("bookwyrm", "0090_emailblocklist"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-10 18:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0091_merge_0090_auto_20210908_2346_0090_emailblocklist"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sitesettings",
|
||||||
|
name="instance_short_description",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-10 19:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0092_sitesettings_instance_short_description"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sitesettings",
|
||||||
|
name="instance_short_description",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
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>.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
@ -24,8 +23,12 @@ from .federated_server import FederatedServer
|
||||||
|
|
||||||
from .import_job import ImportJob, ImportItem
|
from .import_job import ImportJob, ImportItem
|
||||||
|
|
||||||
from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest
|
from .site import SiteSettings, SiteInvite
|
||||||
|
from .site import PasswordReset, InviteRequest
|
||||||
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()
|
||||||
|
@ -502,7 +502,7 @@ def unfurl_related_field(related_field, sort_field=None):
|
||||||
return related_field.remote_id
|
return related_field.remote_id
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def broadcast_task(sender_id, activity, recipients):
|
def broadcast_task(sender_id, activity, recipients):
|
||||||
"""the celery task for broadcast"""
|
"""the celery task for broadcast"""
|
||||||
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
||||||
|
@ -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,20 +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_deletion",
|
("domain_block", _("Domain block")),
|
||||||
"domain_block",
|
]
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def new_access_code():
|
def new_access_code():
|
||||||
|
@ -33,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"""
|
||||||
|
@ -47,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?"""
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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
|
||||||
|
from django.db import transaction
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from model_utils import FieldTracker
|
from model_utils import FieldTracker
|
||||||
|
@ -164,9 +165,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 +178,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 +218,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -375,4 +377,6 @@ def preview_image(instance, *args, **kwargs):
|
||||||
changed_fields = instance.field_tracker.changed()
|
changed_fields = instance.field_tracker.changed()
|
||||||
|
|
||||||
if len(changed_fields) > 0:
|
if len(changed_fields) > 0:
|
||||||
generate_edition_preview_image_task.delay(instance.id)
|
transaction.on_commit(
|
||||||
|
lambda: generate_edition_preview_image_task.delay(instance.id)
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -28,7 +28,7 @@ class FederatedServer(BookWyrmModel):
|
||||||
def block(self):
|
def block(self):
|
||||||
"""block a server"""
|
"""block a server"""
|
||||||
self.status = "blocked"
|
self.status = "blocked"
|
||||||
self.save()
|
self.save(update_fields=["status"])
|
||||||
|
|
||||||
# deactivate all associated users
|
# deactivate all associated users
|
||||||
self.user_set.filter(is_active=True).update(
|
self.user_set.filter(is_active=True).update(
|
||||||
|
@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel):
|
||||||
def unblock(self):
|
def unblock(self):
|
||||||
"""unblock a server"""
|
"""unblock a server"""
|
||||||
self.status = "federated"
|
self.status = "federated"
|
||||||
self.save()
|
self.save(update_fields=["status"])
|
||||||
|
|
||||||
self.user_set.filter(deactivation_reason="domain_block").update(
|
self.user_set.filter(deactivation_reason="domain_block").update(
|
||||||
is_active=True, deactivation_reason=None
|
is_active=True, deactivation_reason=None
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -174,6 +174,7 @@ class ImportItem(models.Model):
|
||||||
if start_date and start_date is not None and not self.date_read:
|
if start_date and start_date is not None and not self.date_read:
|
||||||
return [ReadThrough(start_date=start_date)]
|
return [ReadThrough(start_date=start_date)]
|
||||||
if self.date_read:
|
if self.date_read:
|
||||||
|
start_date = start_date if start_date < self.date_read else None
|
||||||
return [
|
return [
|
||||||
ReadThrough(
|
ReadThrough(
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
|
@ -183,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
|
||||||
|
|
||||||
|
@ -30,8 +29,7 @@ class ReadThrough(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)
|
||||||
|
|
||||||
def create_update(self):
|
def create_update(self):
|
||||||
|
@ -65,6 +63,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"""
|
||||||
|
|
|
@ -20,10 +20,17 @@ class SiteSettings(models.Model):
|
||||||
max_length=150, default="Social Reading and Reviewing"
|
max_length=150, default="Social Reading and Reviewing"
|
||||||
)
|
)
|
||||||
instance_description = models.TextField(default="This instance has no description.")
|
instance_description = models.TextField(default="This instance has no description.")
|
||||||
|
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
# 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.")
|
||||||
|
@ -80,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):
|
||||||
|
@ -120,7 +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}"
|
||||||
|
|
||||||
|
|
||||||
# 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",
|
||||||
|
@ -122,16 +122,21 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
last_active_date = models.DateTimeField(default=timezone.now)
|
last_active_date = models.DateTimeField(default=timezone.now)
|
||||||
manually_approves_followers = fields.BooleanField(default=False)
|
manually_approves_followers = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
# options to turn features on and off
|
||||||
show_goal = models.BooleanField(default=True)
|
show_goal = models.BooleanField(default=True)
|
||||||
|
show_suggested_users = models.BooleanField(default=True)
|
||||||
discoverable = fields.BooleanField(default=False)
|
discoverable = fields.BooleanField(default=False)
|
||||||
|
|
||||||
preferred_timezone = models.CharField(
|
preferred_timezone = models.CharField(
|
||||||
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
||||||
default=str(pytz.utc),
|
default=str(pytz.utc),
|
||||||
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"
|
||||||
|
@ -147,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):
|
||||||
|
@ -189,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"
|
||||||
|
@ -218,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,
|
||||||
|
@ -261,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
|
||||||
|
|
||||||
|
@ -274,30 +288,47 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
transaction.on_commit(lambda: set_remote_server.delay(self.id))
|
transaction.on_commit(lambda: set_remote_server.delay(self.id))
|
||||||
return
|
return
|
||||||
|
|
||||||
# populate fields for local users
|
with transaction.atomic():
|
||||||
link = site_link()
|
# populate fields for local users
|
||||||
self.remote_id = f"{link}/user/{self.localname}"
|
link = site_link()
|
||||||
self.followers_url = f"{self.remote_id}/followers"
|
self.remote_id = f"{link}/user/{self.localname}"
|
||||||
self.inbox = f"{self.remote_id}/inbox"
|
self.followers_url = f"{self.remote_id}/followers"
|
||||||
self.shared_inbox = f"{link}/inbox"
|
self.inbox = f"{self.remote_id}/inbox"
|
||||||
self.outbox = f"{self.remote_id}/outbox"
|
self.shared_inbox = f"{link}/inbox"
|
||||||
|
self.outbox = f"{self.remote_id}/outbox"
|
||||||
|
|
||||||
# an id needs to be set before we can proceed with related models
|
# an id needs to be set before we can proceed with related models
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# make users editors by default
|
||||||
|
try:
|
||||||
|
self.groups.add(Group.objects.get(name="editor"))
|
||||||
|
except Group.DoesNotExist:
|
||||||
|
# this should only happen in tests
|
||||||
|
pass
|
||||||
|
|
||||||
|
# create keys and shelves for new local users
|
||||||
|
self.key_pair = KeyPair.objects.create(
|
||||||
|
remote_id=f"{self.remote_id}/#main-key"
|
||||||
|
)
|
||||||
|
self.save(broadcast=False, update_fields=["key_pair"])
|
||||||
|
|
||||||
|
self.create_shelves()
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
"""deactivate rather than delete a user"""
|
||||||
|
self.is_active = False
|
||||||
|
# skip the logic in this class's save()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# make users editors by default
|
@property
|
||||||
try:
|
def local_path(self):
|
||||||
self.groups.add(Group.objects.get(name="editor"))
|
"""this model doesn't inherit bookwyrm model, so here we are"""
|
||||||
except Group.DoesNotExist:
|
# pylint: disable=consider-using-f-string
|
||||||
# this should only happen in tests
|
return "/user/{:s}".format(self.localname or self.username)
|
||||||
pass
|
|
||||||
|
|
||||||
# create keys and shelves for new local users
|
|
||||||
self.key_pair = KeyPair.objects.create(
|
|
||||||
remote_id="%s/#main-key" % self.remote_id
|
|
||||||
)
|
|
||||||
self.save(broadcast=False, update_fields=["key_pair"])
|
|
||||||
|
|
||||||
|
def create_shelves(self):
|
||||||
|
"""default shelves for a new user"""
|
||||||
shelves = [
|
shelves = [
|
||||||
{
|
{
|
||||||
"name": "To Read",
|
"name": "To Read",
|
||||||
|
@ -321,17 +352,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
editable=False,
|
editable=False,
|
||||||
).save(broadcast=False)
|
).save(broadcast=False)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
"""deactivate rather than delete a user"""
|
|
||||||
self.is_active = False
|
|
||||||
# skip the logic in this class's save()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def local_path(self):
|
|
||||||
"""this model doesn't inherit bookwyrm model, so here we are"""
|
|
||||||
return "/user/%s" % (self.localname or self.username)
|
|
||||||
|
|
||||||
|
|
||||||
class KeyPair(ActivitypubMixin, BookWyrmModel):
|
class KeyPair(ActivitypubMixin, BookWyrmModel):
|
||||||
"""public and private keys for a user"""
|
"""public and private keys for a user"""
|
||||||
|
@ -346,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"""
|
||||||
|
@ -383,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):
|
||||||
|
@ -420,7 +440,7 @@ class AnnualGoal(BookWyrmModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def set_remote_server(user_id):
|
def set_remote_server(user_id):
|
||||||
"""figure out the user's remote server in the background"""
|
"""figure out the user's remote server in the background"""
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
|
@ -439,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):
|
||||||
|
@ -459,7 +479,7 @@ def get_or_create_remote_server(domain):
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def get_remote_reviews(outbox):
|
def get_remote_reviews(outbox):
|
||||||
"""ingest reviews by a new remote bookwyrm user"""
|
"""ingest reviews by a new remote bookwyrm user"""
|
||||||
outbox_page = outbox + "?page=true&type=Review"
|
outbox_page = outbox + "?page=true&type=Review"
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -352,7 +355,7 @@ def save_and_cleanup(image, instance=None):
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def generate_site_preview_image_task():
|
def generate_site_preview_image_task():
|
||||||
"""generate preview_image for the website"""
|
"""generate preview_image for the website"""
|
||||||
if not settings.ENABLE_PREVIEW_IMAGES:
|
if not settings.ENABLE_PREVIEW_IMAGES:
|
||||||
|
@ -377,7 +380,7 @@ def generate_site_preview_image_task():
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def generate_edition_preview_image_task(book_id):
|
def generate_edition_preview_image_task(book_id):
|
||||||
"""generate preview_image for a book"""
|
"""generate preview_image for a book"""
|
||||||
if not settings.ENABLE_PREVIEW_IMAGES:
|
if not settings.ENABLE_PREVIEW_IMAGES:
|
||||||
|
@ -402,7 +405,7 @@ def generate_edition_preview_image_task(book_id):
|
||||||
save_and_cleanup(image, instance=book)
|
save_and_cleanup(image, instance=book)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def generate_user_preview_image_task(user_id):
|
def generate_user_preview_image_task(user_id):
|
||||||
"""generate preview_image for a book"""
|
"""generate preview_image for a book"""
|
||||||
if not settings.ENABLE_PREVIEW_IMAGES:
|
if not settings.ENABLE_PREVIEW_IMAGES:
|
||||||
|
@ -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,16 +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")
|
||||||
|
|
||||||
# celery
|
JS_CACHE = "7f2343cf"
|
||||||
CELERY_BROKER = "redis://:{}@redis_broker:{}/0".format(
|
|
||||||
requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")), env("REDIS_BROKER_PORT")
|
|
||||||
)
|
|
||||||
CELERY_RESULT_BACKEND = "redis://:{}@redis_broker:{}/0".format(
|
|
||||||
requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")), env("REDIS_BROKER_PORT")
|
|
||||||
)
|
|
||||||
CELERY_ACCEPT_CONTENT = ["application/json"]
|
|
||||||
CELERY_TASK_SERIALIZER = "json"
|
|
||||||
CELERY_RESULT_SERIALIZER = "json"
|
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
@ -32,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__)))
|
||||||
|
@ -86,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",
|
||||||
]
|
]
|
||||||
|
@ -135,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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,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)
|
||||||
|
@ -221,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
|
||||||
|
@ -235,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
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
@ -96,7 +122,7 @@ body {
|
||||||
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
||||||
.shelf-option:disabled > *::after {
|
.shelf-option:disabled > *::after {
|
||||||
font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||||
content: "\e918";
|
content: "\e919"; /* icon-check */
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,21 +193,36 @@ body {
|
||||||
|
|
||||||
/* All stars are visually filled by default. */
|
/* All stars are visually filled by default. */
|
||||||
.form-rate-stars .icon::before {
|
.form-rate-stars .icon::before {
|
||||||
content: '\e9d9';
|
content: '\e9d9'; /* icon-star-full */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons directly following half star inputs are marked as half */
|
||||||
|
.form-rate-stars input.half:checked ~ .icon::before {
|
||||||
|
content: '\e9d8'; /* icon-star-half */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
.form-rate-stars input.half:checked + input + .icon:hover::before {
|
||||||
|
content: '\e9d8' !important; /* icon-star-half */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons directly following half check inputs that follow the checked input are emptied. */
|
||||||
|
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
|
||||||
|
content: '\e9d7'; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following inputs that follow the checked input are emptied. */
|
/* Icons directly following inputs that follow the checked input are emptied. */
|
||||||
.form-rate-stars input:checked ~ input + .icon::before {
|
.form-rate-stars input:checked ~ input + .icon::before {
|
||||||
content: '\e9d7';
|
content: '\e9d7'; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
||||||
.form-rate-stars:hover .icon.icon::before {
|
.form-rate-stars:hover .icon.icon::before {
|
||||||
content: '\e9d9';
|
content: '\e9d9' !important; /* icon-star-full */
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-rate-stars .icon:hover ~ .icon::before {
|
.form-rate-stars .icon:hover ~ .icon::before {
|
||||||
content: '\e9d7';
|
content: '\e9d7' !important; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Book covers
|
/** Book covers
|
||||||
|
@ -292,17 +333,59 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote > blockquote::before {
|
.quote > blockquote::before {
|
||||||
content: "\e906";
|
content: "\e907"; /* icon-quote-open */
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote > blockquote::after {
|
.quote > blockquote::after {
|
||||||
content: "\e905";
|
content: "\e906"; /* icon-quote-close */
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* States
|
/** Animations and transitions
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
@keyframes turning {
|
||||||
|
from { transform: rotateZ(0deg); }
|
||||||
|
to { transform: rotateZ(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-processing .icon-spinner::before {
|
||||||
|
animation: turning 1.5s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-spinner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-processing .icon-spinner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.is-processing .icon::before {
|
||||||
|
transition-duration: 0.001ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transient notification
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#live-messages {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1em;
|
||||||
|
right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tooltips
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** States
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
/* "disabled" for non-buttons */
|
/* "disabled" for non-buttons */
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
196
bookwyrm/static/css/vendor/icons.css
vendored
196
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -1,10 +1,10 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'icomoon';
|
font-family: 'icomoon';
|
||||||
src: url('../fonts/icomoon.eot?19nagi');
|
src: url('../fonts/icomoon.eot?36x4a3');
|
||||||
src: url('../fonts/icomoon.eot?19nagi#iefix') format('embedded-opentype'),
|
src: url('../fonts/icomoon.eot?36x4a3#iefix') format('embedded-opentype'),
|
||||||
url('../fonts/icomoon.ttf?19nagi') format('truetype'),
|
url('../fonts/icomoon.ttf?36x4a3') format('truetype'),
|
||||||
url('../fonts/icomoon.woff?19nagi') format('woff'),
|
url('../fonts/icomoon.woff?36x4a3') format('woff'),
|
||||||
url('../fonts/icomoon.svg?19nagi#icomoon') format('svg');
|
url('../fonts/icomoon.svg?36x4a3#icomoon') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: block;
|
font-display: block;
|
||||||
|
@ -25,6 +25,90 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-book:before {
|
||||||
|
content: "\e901";
|
||||||
|
}
|
||||||
|
.icon-envelope:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
.icon-arrow-right:before {
|
||||||
|
content: "\e903";
|
||||||
|
}
|
||||||
|
.icon-bell:before {
|
||||||
|
content: "\e904";
|
||||||
|
}
|
||||||
|
.icon-x:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-quote-close:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
.icon-quote-open:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
.icon-image:before {
|
||||||
|
content: "\e908";
|
||||||
|
}
|
||||||
|
.icon-pencil:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-list:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
.icon-unlock:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
.icon-globe:before {
|
||||||
|
content: "\e90c";
|
||||||
|
}
|
||||||
|
.icon-lock:before {
|
||||||
|
content: "\e90d";
|
||||||
|
}
|
||||||
|
.icon-chain-broken:before {
|
||||||
|
content: "\e90e";
|
||||||
|
}
|
||||||
|
.icon-chain:before {
|
||||||
|
content: "\e90f";
|
||||||
|
}
|
||||||
|
.icon-comments:before {
|
||||||
|
content: "\e910";
|
||||||
|
}
|
||||||
|
.icon-comment:before {
|
||||||
|
content: "\e911";
|
||||||
|
}
|
||||||
|
.icon-boost:before {
|
||||||
|
content: "\e912";
|
||||||
|
}
|
||||||
|
.icon-arrow-left:before {
|
||||||
|
content: "\e913";
|
||||||
|
}
|
||||||
|
.icon-arrow-up:before {
|
||||||
|
content: "\e914";
|
||||||
|
}
|
||||||
|
.icon-arrow-down:before {
|
||||||
|
content: "\e915";
|
||||||
|
}
|
||||||
|
.icon-local:before {
|
||||||
|
content: "\e917";
|
||||||
|
}
|
||||||
|
.icon-dots-three:before {
|
||||||
|
content: "\e918";
|
||||||
|
}
|
||||||
|
.icon-check:before {
|
||||||
|
content: "\e919";
|
||||||
|
}
|
||||||
|
.icon-dots-three-vertical:before {
|
||||||
|
content: "\e91a";
|
||||||
|
}
|
||||||
|
.icon-bookmark:before {
|
||||||
|
content: "\e91b";
|
||||||
|
}
|
||||||
|
.icon-warning:before {
|
||||||
|
content: "\e91c";
|
||||||
|
}
|
||||||
|
.icon-rss:before {
|
||||||
|
content: "\e91d";
|
||||||
|
}
|
||||||
.icon-graphic-heart:before {
|
.icon-graphic-heart:before {
|
||||||
content: "\e91e";
|
content: "\e91e";
|
||||||
}
|
}
|
||||||
|
@ -34,102 +118,6 @@
|
||||||
.icon-graphic-banknote:before {
|
.icon-graphic-banknote:before {
|
||||||
content: "\e920";
|
content: "\e920";
|
||||||
}
|
}
|
||||||
.icon-warning:before {
|
|
||||||
content: "\e91b";
|
|
||||||
}
|
|
||||||
.icon-book:before {
|
|
||||||
content: "\e900";
|
|
||||||
}
|
|
||||||
.icon-bookmark:before {
|
|
||||||
content: "\e91a";
|
|
||||||
}
|
|
||||||
.icon-rss:before {
|
|
||||||
content: "\e91d";
|
|
||||||
}
|
|
||||||
.icon-envelope:before {
|
|
||||||
content: "\e901";
|
|
||||||
}
|
|
||||||
.icon-arrow-right:before {
|
|
||||||
content: "\e902";
|
|
||||||
}
|
|
||||||
.icon-bell:before {
|
|
||||||
content: "\e903";
|
|
||||||
}
|
|
||||||
.icon-x:before {
|
|
||||||
content: "\e904";
|
|
||||||
}
|
|
||||||
.icon-quote-close:before {
|
|
||||||
content: "\e905";
|
|
||||||
}
|
|
||||||
.icon-quote-open:before {
|
|
||||||
content: "\e906";
|
|
||||||
}
|
|
||||||
.icon-image:before {
|
|
||||||
content: "\e907";
|
|
||||||
}
|
|
||||||
.icon-pencil:before {
|
|
||||||
content: "\e908";
|
|
||||||
}
|
|
||||||
.icon-list:before {
|
|
||||||
content: "\e909";
|
|
||||||
}
|
|
||||||
.icon-unlock:before {
|
|
||||||
content: "\e90a";
|
|
||||||
}
|
|
||||||
.icon-unlisted:before {
|
|
||||||
content: "\e90a";
|
|
||||||
}
|
|
||||||
.icon-globe:before {
|
|
||||||
content: "\e90b";
|
|
||||||
}
|
|
||||||
.icon-public:before {
|
|
||||||
content: "\e90b";
|
|
||||||
}
|
|
||||||
.icon-lock:before {
|
|
||||||
content: "\e90c";
|
|
||||||
}
|
|
||||||
.icon-followers:before {
|
|
||||||
content: "\e90c";
|
|
||||||
}
|
|
||||||
.icon-chain-broken:before {
|
|
||||||
content: "\e90d";
|
|
||||||
}
|
|
||||||
.icon-chain:before {
|
|
||||||
content: "\e90e";
|
|
||||||
}
|
|
||||||
.icon-comments:before {
|
|
||||||
content: "\e90f";
|
|
||||||
}
|
|
||||||
.icon-comment:before {
|
|
||||||
content: "\e910";
|
|
||||||
}
|
|
||||||
.icon-boost:before {
|
|
||||||
content: "\e911";
|
|
||||||
}
|
|
||||||
.icon-arrow-left:before {
|
|
||||||
content: "\e912";
|
|
||||||
}
|
|
||||||
.icon-arrow-up:before {
|
|
||||||
content: "\e913";
|
|
||||||
}
|
|
||||||
.icon-arrow-down:before {
|
|
||||||
content: "\e914";
|
|
||||||
}
|
|
||||||
.icon-home:before {
|
|
||||||
content: "\e915";
|
|
||||||
}
|
|
||||||
.icon-local:before {
|
|
||||||
content: "\e916";
|
|
||||||
}
|
|
||||||
.icon-dots-three:before {
|
|
||||||
content: "\e917";
|
|
||||||
}
|
|
||||||
.icon-check:before {
|
|
||||||
content: "\e918";
|
|
||||||
}
|
|
||||||
.icon-dots-three-vertical:before {
|
|
||||||
content: "\e919";
|
|
||||||
}
|
|
||||||
.icon-search:before {
|
.icon-search:before {
|
||||||
content: "\e986";
|
content: "\e986";
|
||||||
}
|
}
|
||||||
|
@ -148,3 +136,9 @@
|
||||||
.icon-plus:before {
|
.icon-plus:before {
|
||||||
content: "\ea0a";
|
content: "\ea0a";
|
||||||
}
|
}
|
||||||
|
.icon-question-circle:before {
|
||||||
|
content: "\e900";
|
||||||
|
}
|
||||||
|
.icon-spinner:before {
|
||||||
|
content: "\e97a";
|
||||||
|
}
|
||||||
|
|
|
@ -301,7 +301,10 @@ let BookWyrm = new class {
|
||||||
ajaxPost(form) {
|
ajaxPost(form) {
|
||||||
return fetch(form.action, {
|
return fetch(form.action, {
|
||||||
method : "POST",
|
method : "POST",
|
||||||
body: new FormData(form)
|
body: new FormData(form),
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
236
bookwyrm/static/js/status_cache.js
Normal file
236
bookwyrm/static/js/status_cache.js
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
/* exported StatusCache */
|
||||||
|
/* globals BookWyrm */
|
||||||
|
|
||||||
|
let StatusCache = new class {
|
||||||
|
constructor() {
|
||||||
|
document.querySelectorAll('[data-cache-draft]')
|
||||||
|
.forEach(t => t.addEventListener('change', this.updateDraft.bind(this)));
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-cache-draft]')
|
||||||
|
.forEach(t => this.populateDraft(t));
|
||||||
|
|
||||||
|
document.querySelectorAll('.submit-status')
|
||||||
|
.forEach(button => button.addEventListener(
|
||||||
|
'submit',
|
||||||
|
this.submitStatus.bind(this))
|
||||||
|
);
|
||||||
|
|
||||||
|
document.querySelectorAll('.form-rate-stars label.icon')
|
||||||
|
.forEach(button => button.addEventListener('click', this.toggleStar.bind(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update localStorage copy of drafted status
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
updateDraft(event) {
|
||||||
|
// Used in set reading goal
|
||||||
|
let key = event.target.dataset.cacheDraft;
|
||||||
|
let value = event.target.value;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle display of a DOM node based on its value in the localStorage.
|
||||||
|
*
|
||||||
|
* @param {object} node - DOM node to toggle.
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
populateDraft(node) {
|
||||||
|
// Used in set reading goal
|
||||||
|
let key = node.dataset.cacheDraft;
|
||||||
|
let value = window.localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a status with ajax
|
||||||
|
*
|
||||||
|
* @param {} event
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
submitStatus(event) {
|
||||||
|
const form = event.currentTarget;
|
||||||
|
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);
|
||||||
|
trigger.setAttribute('disabled', null);
|
||||||
|
|
||||||
|
BookWyrm.ajaxPost(form).finally(() => {
|
||||||
|
// Change icon to remove ongoing activity on the current UI.
|
||||||
|
// Enable back the element used to submit the form.
|
||||||
|
BookWyrm.addRemoveClass(form, 'is-processing', false);
|
||||||
|
trigger.removeAttribute('disabled');
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
this.submitStatusSuccess(form);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.warn(error);
|
||||||
|
this.announceMessage('status-error-message');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a message in the live region
|
||||||
|
*
|
||||||
|
* @param {String} the id of the message dom element
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
announceMessage(message_id) {
|
||||||
|
const element = document.getElementById(message_id);
|
||||||
|
let copy = element.cloneNode(true);
|
||||||
|
|
||||||
|
copy.id = null;
|
||||||
|
element.insertAdjacentElement('beforebegin', copy);
|
||||||
|
|
||||||
|
BookWyrm.addRemoveClass(copy, 'is-hidden', false);
|
||||||
|
setTimeout(function() {
|
||||||
|
copy.remove();
|
||||||
|
}, 10000, copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success state for a posted status
|
||||||
|
*
|
||||||
|
* @param {Object} the html form that was submitted
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
submitStatusSuccess(form) {
|
||||||
|
// Clear form data
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
// Clear localstorage
|
||||||
|
form.querySelectorAll('[data-cache-draft]')
|
||||||
|
.forEach(node => window.localStorage.removeItem(node.dataset.cacheDraft));
|
||||||
|
|
||||||
|
// Close modals
|
||||||
|
let modal = form.closest(".modal.is-active");
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.getElementsByClassName("modal-close")[0].click();
|
||||||
|
|
||||||
|
// Update shelve buttons
|
||||||
|
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
|
||||||
|
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close reply panel
|
||||||
|
let reply = form.closest(".reply-panel");
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
document.querySelector("[data-controls=" + reply.id + "]").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.announceMessage('status-success-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change which buttons are available for a shelf
|
||||||
|
*
|
||||||
|
* @param {Object} html button dom element
|
||||||
|
* @param {String} the identifier of the selected shelf
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
cycleShelveButtons(button, identifier) {
|
||||||
|
// Pressed button
|
||||||
|
let shelf = button.querySelector("[data-shelf-identifier='" + identifier + "']");
|
||||||
|
let next_identifier = shelf.dataset.shelfNext;
|
||||||
|
|
||||||
|
// Set all buttons to hidden
|
||||||
|
button.querySelectorAll("[data-shelf-identifier]")
|
||||||
|
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||||
|
|
||||||
|
// Button that should be visible now
|
||||||
|
let next = button.querySelector("[data-shelf-identifier=" + next_identifier + "]");
|
||||||
|
|
||||||
|
// Show the desired button
|
||||||
|
BookWyrm.addRemoveClass(next, "is-hidden", false);
|
||||||
|
|
||||||
|
// ------ update the dropdown buttons
|
||||||
|
// Remove existing hidden class
|
||||||
|
button.querySelectorAll("[data-shelf-dropdown-identifier]")
|
||||||
|
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false));
|
||||||
|
|
||||||
|
// Remove existing disabled states
|
||||||
|
button.querySelectorAll("[data-shelf-dropdown-identifier] button")
|
||||||
|
.forEach(item => item.disabled = false);
|
||||||
|
|
||||||
|
next_identifier = next_identifier == 'complete' ? 'read' : next_identifier;
|
||||||
|
|
||||||
|
// Disable the current state
|
||||||
|
button.querySelector(
|
||||||
|
"[data-shelf-dropdown-identifier=" + identifier + "] button"
|
||||||
|
).disabled = true;
|
||||||
|
|
||||||
|
let main_button = button.querySelector(
|
||||||
|
"[data-shelf-dropdown-identifier=" + next_identifier + "]"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hide the option that's shown as the main button
|
||||||
|
BookWyrm.addRemoveClass(main_button, "is-hidden", true);
|
||||||
|
|
||||||
|
// Just hide the other two menu options, idk what to do with them
|
||||||
|
button.querySelectorAll("[data-extra-options]")
|
||||||
|
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||||
|
|
||||||
|
// Close menu
|
||||||
|
let menu = button.querySelector(".dropdown-trigger[aria-expanded=true]");
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
menu.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reveal half-stars
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
toggleStar(event) {
|
||||||
|
const label = event.currentTarget;
|
||||||
|
let wholeStar = document.getElementById(label.getAttribute("for"));
|
||||||
|
|
||||||
|
if (wholeStar.checked) {
|
||||||
|
event.preventDefault();
|
||||||
|
let halfStar = document.getElementById(label.dataset.forHalf);
|
||||||
|
|
||||||
|
wholeStar.checked = null;
|
||||||
|
halfStar.checked = "checked";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
|
@ -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"""
|
||||||
|
@ -86,10 +86,12 @@ class SuggestedUsers(RedisStore):
|
||||||
values = self.get_store(self.store_id(user), withscores=True)
|
values = self.get_store(self.store_id(user), withscores=True)
|
||||||
results = []
|
results = []
|
||||||
# annotate users with mutuals and shared book counts
|
# annotate users with mutuals and shared book counts
|
||||||
for user_id, rank in values[:5]:
|
for user_id, rank in values:
|
||||||
counts = self.get_counts_from_rank(rank)
|
counts = self.get_counts_from_rank(rank)
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(
|
||||||
|
id=user_id, is_active=True, bookwyrm_user=True
|
||||||
|
)
|
||||||
except models.User.DoesNotExist as err:
|
except models.User.DoesNotExist as err:
|
||||||
# if this happens, the suggestions are janked way up
|
# if this happens, the suggestions are janked way up
|
||||||
logger.exception(err)
|
logger.exception(err)
|
||||||
|
@ -97,6 +99,8 @@ class SuggestedUsers(RedisStore):
|
||||||
user.mutuals = counts["mutuals"]
|
user.mutuals = counts["mutuals"]
|
||||||
# user.shared_books = counts["shared_books"]
|
# user.shared_books = counts["shared_books"]
|
||||||
results.append(user)
|
results.append(user)
|
||||||
|
if len(results) >= 5:
|
||||||
|
break
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,13 +182,21 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs):
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.User)
|
@receiver(signals.post_save, sender=models.User)
|
||||||
# pylint: disable=unused-argument, too-many-arguments
|
# pylint: disable=unused-argument, too-many-arguments
|
||||||
def add_new_user(sender, instance, created, update_fields=None, **kwargs):
|
def update_user(sender, instance, created, update_fields=None, **kwargs):
|
||||||
"""a new user, wow how cool"""
|
"""an updated user, neat"""
|
||||||
# a new user is found, create suggestions for them
|
# a new user is found, create suggestions for them
|
||||||
if created and instance.local:
|
if created and instance.local:
|
||||||
rerank_suggestions_task.delay(instance.id)
|
rerank_suggestions_task.delay(instance.id)
|
||||||
|
|
||||||
if update_fields and not "discoverable" in update_fields:
|
# we know what fields were updated and discoverability didn't change
|
||||||
|
if not instance.bookwyrm_user or (
|
||||||
|
update_fields and not "discoverable" in update_fields
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# deleted the user
|
||||||
|
if not created and not instance.is_active:
|
||||||
|
remove_user_task.delay(instance.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# this happens on every save, not just when discoverability changes, annoyingly
|
# this happens on every save, not just when discoverability changes, annoyingly
|
||||||
|
@ -194,28 +206,61 @@ def add_new_user(sender, instance, created, update_fields=None, **kwargs):
|
||||||
remove_user_task.delay(instance.id)
|
remove_user_task.delay(instance.id)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@receiver(signals.post_save, sender=models.FederatedServer)
|
||||||
|
def domain_level_update(sender, instance, created, update_fields=None, **kwargs):
|
||||||
|
"""remove users on a domain block"""
|
||||||
|
if (
|
||||||
|
not update_fields
|
||||||
|
or "status" not in update_fields
|
||||||
|
or instance.application_type != "bookwyrm"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
if instance.status == "blocked":
|
||||||
|
bulk_remove_instance_task.delay(instance.id)
|
||||||
|
return
|
||||||
|
bulk_add_instance_task.delay(instance.id)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------- TASKS
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue="low_priority")
|
||||||
def rerank_suggestions_task(user_id):
|
def rerank_suggestions_task(user_id):
|
||||||
"""do the hard work in celery"""
|
"""do the hard work in celery"""
|
||||||
suggested_users.rerank_user_suggestions(user_id)
|
suggested_users.rerank_user_suggestions(user_id)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def rerank_user_task(user_id, update_only=False):
|
def rerank_user_task(user_id, update_only=False):
|
||||||
"""do the hard work in celery"""
|
"""do the hard work in celery"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
suggested_users.rerank_obj(user, update_only=update_only)
|
suggested_users.rerank_obj(user, update_only=update_only)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="low_priority")
|
||||||
def remove_user_task(user_id):
|
def remove_user_task(user_id):
|
||||||
"""do the hard work in celery"""
|
"""do the hard work in celery"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
suggested_users.remove_object_from_related_stores(user)
|
suggested_users.remove_object_from_related_stores(user)
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="medium_priority")
|
||||||
def remove_suggestion_task(user_id, suggested_user_id):
|
def remove_suggestion_task(user_id, suggested_user_id):
|
||||||
"""remove a specific user from a specific user's suggestions"""
|
"""remove a specific user from a specific user's suggestions"""
|
||||||
suggested_user = models.User.objects.get(id=suggested_user_id)
|
suggested_user = models.User.objects.get(id=suggested_user_id)
|
||||||
suggested_users.remove_suggestion(user_id, suggested_user)
|
suggested_users.remove_suggestion(user_id, suggested_user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue="low_priority")
|
||||||
|
def bulk_remove_instance_task(instance_id):
|
||||||
|
"""remove a bunch of users from recs"""
|
||||||
|
for user in models.User.objects.filter(federated_server__id=instance_id):
|
||||||
|
suggested_users.remove_object_from_related_stores(user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue="low_priority")
|
||||||
|
def bulk_add_instance_task(instance_id):
|
||||||
|
"""remove a bunch of users from recs"""
|
||||||
|
for user in models.User.objects.filter(federated_server__id=instance_id):
|
||||||
|
suggested_users.rerank_obj(user, update_only=False)
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
import os
|
import os
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
|
||||||
from bookwyrm import settings
|
from celerywyrm import settings
|
||||||
|
|
||||||
# set the default Django settings module for the 'celery' program.
|
# set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings")
|
||||||
app = Celery(
|
app = Celery(
|
||||||
"tasks", broker=settings.CELERY_BROKER, backend=settings.CELERY_RESULT_BACKEND
|
"tasks", broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND
|
||||||
)
|
)
|
||||||
|
|
|
@ -325,5 +325,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{% static "js/vendor/tabs.js" %}"></script>
|
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
11
bookwyrm/templates/components/tooltip.html
Normal file
11
bookwyrm/templates/components/tooltip.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
|
||||||
|
|
||||||
|
{% block tooltip_content %}{% endblock %}
|
||||||
|
</aside>
|
|
@ -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">
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
|
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if request.user.show_goal and not goal and tab.key == streams.first.key %}
|
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
||||||
{% now 'Y' as year %}
|
{% now 'Y' as year %}
|
||||||
<section class="block">
|
<section class="block">
|
||||||
{% include 'snippets/goal_card.html' with year=year %}
|
{% include 'snippets/goal_card.html' with year=year %}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
|
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
|
||||||
|
|
||||||
{% if suggested_users %}
|
{% if request.user.show_suggested_users and suggested_users %}
|
||||||
{# suggested users for when things are very lonely #}
|
{# suggested users for when things are very lonely #}
|
||||||
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
|
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
{% for activity in activities %}
|
{% for activity in activities %}
|
||||||
|
|
||||||
{% if not activities.number > 1 and forloop.counter0 == 2 and suggested_users %}
|
{% if request.user.show_suggested_users and not activities.number > 1 and forloop.counter0 == 2 and suggested_users %}
|
||||||
{# suggested users on the first page, two statuses down #}
|
{# suggested users on the first page, two statuses down #}
|
||||||
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
|
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -106,5 +106,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{% static "js/vendor/tabs.js" %}"></script>
|
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-5">{% trans "Who to follow" %}</h2>
|
<header class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="title is-5">{% trans "Who to follow" %}</h2>
|
||||||
|
</div>
|
||||||
|
<form class="column is-narrow" action="{% url 'hide-suggestions' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% trans "Don't show suggested users" as button_text %}
|
||||||
|
<button type="submit" class="delete" title="{{ button_text }}">{{ button_text }}</button>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
{% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
|
{% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
|
||||||
<a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a>
|
<a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -12,9 +12,14 @@
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<label class="label" for="source">
|
|
||||||
{% trans "Data source:" %}
|
<div class="field">
|
||||||
</label>
|
<label class="label is-pulled-left" for="source">
|
||||||
|
{% trans "Data source:" %}
|
||||||
|
</label>
|
||||||
|
{% include 'import/tooltip.html' with controls_text="goodreads-tooltip" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="select block">
|
<div class="select block">
|
||||||
<select name="source" id="source">
|
<select name="source" id="source">
|
||||||
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>
|
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>
|
|
@ -40,7 +40,7 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2 class="title is-4">{% trans "Failed to load" %}</h2>
|
<h2 class="title is-4">{% trans "Failed to load" %}</h2>
|
||||||
{% if not job.retry %}
|
{% if not job.retry %}
|
||||||
<form name="retry" action="/import/{{ job.id }}" method="post">
|
<form name="retry" action="/import/{{ job.id }}" method="post" class="box">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% with failed_count=failed_items|length %}
|
{% with failed_count=failed_items|length %}
|
||||||
|
@ -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>
|
||||||
|
@ -156,5 +156,5 @@
|
||||||
{% endspaceless %}{% endblock %}
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{% static "js/check_all.js" %}"></script>
|
<script src="{% static "js/check_all.js" %}?v={{ js_cache }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
8
bookwyrm/templates/import/tooltip.html
Normal file
8
bookwyrm/templates/import/tooltip.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'components/tooltip.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block tooltip_content %}
|
||||||
|
|
||||||
|
{% trans 'You can download your GoodReads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener">Import/Export page</a> of your GoodReads account.' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -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" %}">
|
||||||
|
@ -17,8 +17,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 }}">
|
||||||
|
|
||||||
|
@ -37,7 +37,12 @@
|
||||||
<form class="navbar-item column" action="/search/">
|
<form class="navbar-item column" action="/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">
|
||||||
|
@ -121,7 +126,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>
|
||||||
|
@ -210,6 +215,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role="region" aria-live="polite" id="live-messages">
|
||||||
|
<p id="status-success-message" class="live-message is-sr-only is-hidden">{% trans "Successfully posted status" %}</p>
|
||||||
|
<p id="status-error-message" class="live-message notification is-danger p-3 pr-5 pl-5 is-hidden">{% trans "Error posting status" %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
@ -249,8 +259,11 @@
|
||||||
<script>
|
<script>
|
||||||
var csrf_token = '{{ csrf_token }}';
|
var csrf_token = '{{ csrf_token }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static "js/bookwyrm.js" %}"></script>
|
|
||||||
<script src="{% static "js/localstorage.js" %}"></script>
|
<script src="{% static "js/bookwyrm.js" %}?v={{ js_cache }}"></script>
|
||||||
|
<script src="{% static "js/localstorage.js" %}?v={{ js_cache }}"></script>
|
||||||
|
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
|
|
|
@ -43,9 +43,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="checkbox label" for="id_show_goal">
|
<label class="checkbox label" for="id_show_goal">
|
||||||
{% trans "Show set reading goal prompt in feed:" %}
|
{% trans "Show reading goal prompt in feed:" %}
|
||||||
{{ form.show_goal }}
|
{{ form.show_goal }}
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox label" for="id_show_goal">
|
||||||
|
{% trans "Show suggested users:" %}
|
||||||
|
{{ form.show_suggested_users }}
|
||||||
|
</label>
|
||||||
|
<label class="checkbox label" for="id_discoverable">
|
||||||
|
{% trans "Show this account in suggested users:" %}
|
||||||
|
{{ form.discoverable }}
|
||||||
|
</label>
|
||||||
|
{% url 'directory' as path %}
|
||||||
|
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="checkbox label" for="id_manually_approves_followers">
|
<label class="checkbox label" for="id_manually_approves_followers">
|
||||||
|
@ -61,14 +71,6 @@
|
||||||
{{ form.default_post_privacy }}
|
{{ form.default_post_privacy }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
|
||||||
<label class="checkbox label" for="id_discoverable">
|
|
||||||
{% trans "Show this account in suggested users:" %}
|
|
||||||
{{ form.discoverable }}
|
|
||||||
</label>
|
|
||||||
{% url 'directory' as path %}
|
|
||||||
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
|
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}{% load humanize %}
|
{% load i18n %}{% load humanize %}
|
||||||
{% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %}
|
{% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -13,21 +13,21 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}">
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_preview">Preview:</label>
|
<label class="label" for="id_preview">{% trans "Preview:" %}</label>
|
||||||
{{ form.preview }}
|
{{ form.preview }}
|
||||||
{% for error in form.preview.errors %}
|
{% for error in form.preview.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_content">Content:</label>
|
<label class="label" for="id_content">{% trans "Content:" %}</label>
|
||||||
{{ form.content }}
|
{{ form.content }}
|
||||||
{% for error in form.content.errors %}
|
{% for error in form.content.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_event_date">Event date:</label>
|
<label class="label" for="id_event_date">{% trans "Event date:" %}</label>
|
||||||
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
|
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
|
||||||
{% for error in form.event_date.errors %}
|
{% for error in form.event_date.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_start_date">Start date:</label>
|
<label class="label" for="id_start_date">{% trans "Start date:" %}</label>
|
||||||
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
|
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
|
||||||
{% for error in form.start_date.errors %}
|
{% for error in form.start_date.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_end_date">End date:</label>
|
<label class="label" for="id_end_date">{% trans "End date:" %}</label>
|
||||||
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
|
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
|
||||||
{% for error in form.end_date.errors %}
|
{% for error in form.end_date.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<p>
|
<p>
|
||||||
<label class="label" for="id_active">Active:</label>
|
<label class="label" for="id_active">{% trans "Active:" %}</label>
|
||||||
{{ form.active }}
|
{{ form.active }}
|
||||||
{% for error in form.active.errors %}
|
{% for error in form.active.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}{% load humanize %}
|
{% load i18n %}{% load humanize %}
|
||||||
{% block title %}{% trans "Announcements" %}{% endblock %}
|
{% block title %}{% trans "Announcements" %}{% 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>
|
30
bookwyrm/templates/settings/domain_form.html
Normal file
30
bookwyrm/templates/settings/domain_form.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends 'components/inline_form.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% trans "Add domain" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<form name="add-domain" method="post" action="{% url 'settings-email-blocks' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label class="label" for="id_domain">{% trans "Domain:" %}</label>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
<div class="button is-disabled">@</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
{{ form.domain }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for error in form.domain.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 %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}{% trans "Add instance" %}{% endblock %}
|
{% block title %}{% trans "Add instance" %}{% endblock %}
|
||||||
|
|
||||||
|
|
61
bookwyrm/templates/settings/email_blocklist.html
Normal file
61
bookwyrm/templates/settings/email_blocklist.html
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{% extends 'settings/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Email Blocklist" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% trans "Email Blocklist" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block edit-button %}
|
||||||
|
{% trans "Add domain" as button_text %}
|
||||||
|
{% include 'snippets/toggle/open_button.html' with controls_text="add_domain" icon_with_text="plus" text=button_text focus="add_domain_header" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
{% include 'settings/domain_form.html' with controls_text="add_domain" class="block" %}
|
||||||
|
|
||||||
|
<p class="notification block">
|
||||||
|
{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table is-striped is-fullwidth">
|
||||||
|
<tr>
|
||||||
|
{% url 'settings-federation' as url %}
|
||||||
|
<th>
|
||||||
|
{% trans "Domain" %}
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Users" %}</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Options" %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ domain.domain }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'settings-users' %}?email=@{{ domain.domain }}">
|
||||||
|
{% with user_count=domain.users.count %}
|
||||||
|
{% blocktrans trimmed count conter=user_count with display_count=user_count|intcomma %}
|
||||||
|
{{ display_count }} user
|
||||||
|
{% plural %}
|
||||||
|
{{ display_count }} users
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endwith %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form name="remove-{{ domain.id }}" action="{% url 'settings-email-blocks-delete' domain.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 %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}{% trans "Federated Instances" %}{% endblock %}
|
{% block title %}{% trans "Federated Instances" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -32,12 +39,6 @@
|
||||||
{% url 'settings-invites' as alt_url %}
|
{% url 'settings-invites' as alt_url %}
|
||||||
<a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
<a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.bookwyrm.moderate_user %}
|
|
||||||
<li>
|
|
||||||
{% url 'settings-reports' as url %}
|
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.bookwyrm.control_federation %}
|
{% if perms.bookwyrm.control_federation %}
|
||||||
<li>
|
<li>
|
||||||
{% url 'settings-federation' as url %}
|
{% url 'settings-federation' as url %}
|
||||||
|
@ -46,6 +47,23 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.bookwyrm.moderate_user %}
|
||||||
|
<h2 class="menu-label">{% trans "Moderation" %}</h2>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
{% url 'settings-reports' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% 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>
|
||||||
|
</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>
|
||||||
|
{% endif %}
|
||||||
{% if perms.bookwyrm.edit_instance_settings %}
|
{% if perms.bookwyrm.edit_instance_settings %}
|
||||||
<h2 class="menu-label">{% trans "Instance Settings" %}</h2>
|
<h2 class="menu-label">{% trans "Instance Settings" %}</h2>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% block header %}{% trans "Invite Requests" %}{% endblock %}
|
{% block header %}{% trans "Invite Requests" %}{% endblock %}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue