mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 11:01:12 +00:00
Merge branch 'main' into totp-window
This commit is contained in:
commit
bba0d09fa4
41 changed files with 187 additions and 50 deletions
|
@ -126,3 +126,8 @@ HTTP_X_FORWARDED_PROTO=false
|
||||||
# which will be accepted.
|
# which will be accepted.
|
||||||
TWO_FACTOR_LOGIN_VALIDITY_WINDOW=2
|
TWO_FACTOR_LOGIN_VALIDITY_WINDOW=2
|
||||||
TWO_FACTOR_LOGIN_MAX_SECONDS=60
|
TWO_FACTOR_LOGIN_MAX_SECONDS=60
|
||||||
|
|
||||||
|
# Additional hosts to allow in the Content-Security-Policy, "self" (should be DOMAIN)
|
||||||
|
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
|
||||||
|
# Value should be a comma-separated list of host names.
|
||||||
|
CSP_ADDITIONAL_HOSTS=
|
||||||
|
|
|
@ -53,6 +53,7 @@ class QuotationForm(CustomForm):
|
||||||
"sensitive",
|
"sensitive",
|
||||||
"privacy",
|
"privacy",
|
||||||
"position",
|
"position",
|
||||||
|
"endposition",
|
||||||
"position_mode",
|
"position_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
35
bookwyrm/migrations/0174_auto_20230130_1240.py
Normal file
35
bookwyrm/migrations/0174_auto_20230130_1240.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 3.2.16 on 2023-01-30 12:40
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
("bookwyrm", "0173_default_user_auth_group_setting"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="quotation",
|
||||||
|
name="endposition",
|
||||||
|
field=models.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(0)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sitesettings",
|
||||||
|
name="default_user_auth_group",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="auth.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -21,7 +21,7 @@ from django.utils.http import http_date
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import USER_AGENT, PAGE_LENGTH
|
from bookwyrm.settings import USER_AGENT, PAGE_LENGTH
|
||||||
from bookwyrm.signatures import make_signature, make_digest
|
from bookwyrm.signatures import make_signature, make_digest
|
||||||
from bookwyrm.tasks import app, MEDIUM
|
from bookwyrm.tasks import app, MEDIUM, BROADCAST
|
||||||
from bookwyrm.models.fields import ImageField, ManyToManyField
|
from bookwyrm.models.fields import ImageField, ManyToManyField
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -126,7 +126,7 @@ class ActivitypubMixin:
|
||||||
# there OUGHT to be only one match
|
# there OUGHT to be only one match
|
||||||
return match.first()
|
return match.first()
|
||||||
|
|
||||||
def broadcast(self, activity, sender, software=None, queue=MEDIUM):
|
def broadcast(self, activity, sender, software=None, queue=BROADCAST):
|
||||||
"""send out an activity"""
|
"""send out an activity"""
|
||||||
broadcast_task.apply_async(
|
broadcast_task.apply_async(
|
||||||
args=(
|
args=(
|
||||||
|
@ -198,7 +198,7 @@ class ActivitypubMixin:
|
||||||
class ObjectMixin(ActivitypubMixin):
|
class ObjectMixin(ActivitypubMixin):
|
||||||
"""add this mixin for object models that are AP serializable"""
|
"""add this mixin for object models that are AP serializable"""
|
||||||
|
|
||||||
def save(self, *args, created=None, software=None, priority=MEDIUM, **kwargs):
|
def save(self, *args, created=None, software=None, priority=BROADCAST, **kwargs):
|
||||||
"""broadcast created/updated/deleted objects as appropriate"""
|
"""broadcast created/updated/deleted objects as appropriate"""
|
||||||
broadcast = kwargs.get("broadcast", True)
|
broadcast = kwargs.get("broadcast", True)
|
||||||
# this bonus kwarg would cause an error in the base save method
|
# this bonus kwarg would cause an error in the base save method
|
||||||
|
@ -506,7 +506,7 @@ def unfurl_related_field(related_field, sort_field=None):
|
||||||
return related_field.remote_id
|
return related_field.remote_id
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
@app.task(queue=BROADCAST)
|
||||||
def broadcast_task(sender_id: int, activity: str, recipients: List[str]):
|
def broadcast_task(sender_id: int, activity: str, recipients: List[str]):
|
||||||
"""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)
|
||||||
|
|
|
@ -72,7 +72,7 @@ class SiteSettings(SiteModel):
|
||||||
invite_request_question = models.BooleanField(default=False)
|
invite_request_question = models.BooleanField(default=False)
|
||||||
require_confirm_email = models.BooleanField(default=True)
|
require_confirm_email = models.BooleanField(default=True)
|
||||||
default_user_auth_group = models.ForeignKey(
|
default_user_auth_group = models.ForeignKey(
|
||||||
auth_models.Group, null=True, blank=True, on_delete=models.PROTECT
|
auth_models.Group, null=True, blank=True, on_delete=models.RESTRICT
|
||||||
)
|
)
|
||||||
|
|
||||||
invite_question_text = models.CharField(
|
invite_question_text = models.CharField(
|
||||||
|
|
|
@ -329,6 +329,9 @@ class Quotation(BookStatus):
|
||||||
position = models.IntegerField(
|
position = models.IntegerField(
|
||||||
validators=[MinValueValidator(0)], null=True, blank=True
|
validators=[MinValueValidator(0)], null=True, blank=True
|
||||||
)
|
)
|
||||||
|
endposition = models.IntegerField(
|
||||||
|
validators=[MinValueValidator(0)], null=True, blank=True
|
||||||
|
)
|
||||||
position_mode = models.CharField(
|
position_mode = models.CharField(
|
||||||
max_length=3,
|
max_length=3,
|
||||||
choices=ProgressMode.choices,
|
choices=ProgressMode.choices,
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.5.4"
|
VERSION = "0.5.5"
|
||||||
|
|
||||||
RELEASE_API = env(
|
RELEASE_API = env(
|
||||||
"RELEASE_API",
|
"RELEASE_API",
|
||||||
|
@ -101,6 +101,7 @@ MIDDLEWARE = [
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"csp.middleware.CSPMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"bookwyrm.middleware.TimezoneMiddleware",
|
"bookwyrm.middleware.TimezoneMiddleware",
|
||||||
"bookwyrm.middleware.IPBlocklistMiddleware",
|
"bookwyrm.middleware.IPBlocklistMiddleware",
|
||||||
|
@ -329,12 +330,15 @@ IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = "bookwyrm.thumbnail_generation.Strategy"
|
||||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
CSP_ADDITIONAL_HOSTS = env.list("CSP_ADDITIONAL_HOSTS", [])
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
|
|
||||||
PROTOCOL = "http"
|
PROTOCOL = "http"
|
||||||
if USE_HTTPS:
|
if USE_HTTPS:
|
||||||
PROTOCOL = "https"
|
PROTOCOL = "https"
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
USE_S3 = env.bool("USE_S3", False)
|
USE_S3 = env.bool("USE_S3", False)
|
||||||
|
|
||||||
|
@ -358,11 +362,17 @@ if USE_S3:
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
STATIC_FULL_URL = STATIC_URL
|
STATIC_FULL_URL = STATIC_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
||||||
|
CSP_DEFAULT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
||||||
|
CSP_SCRIPT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
||||||
else:
|
else:
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
||||||
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
||||||
|
CSP_DEFAULT_SRC = ["'self'"] + CSP_ADDITIONAL_HOSTS
|
||||||
|
CSP_SCRIPT_SRC = ["'self'"] + CSP_ADDITIONAL_HOSTS
|
||||||
|
|
||||||
|
CSP_INCLUDE_NONCE_IN = ["script-src"]
|
||||||
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
|
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
|
||||||
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
|
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
|
||||||
|
|
|
@ -15,7 +15,7 @@ MAX_SIGNATURE_AGE = 300
|
||||||
def create_key_pair():
|
def create_key_pair():
|
||||||
"""a new public/private key pair, used for creating new users"""
|
"""a new public/private key pair, used for creating new users"""
|
||||||
random_generator = Random.new().read
|
random_generator = Random.new().read
|
||||||
key = RSA.generate(1024, random_generator)
|
key = RSA.generate(2048, random_generator)
|
||||||
private_key = key.export_key().decode("utf8")
|
private_key = key.export_key().decode("utf8")
|
||||||
public_key = key.public_key().export_key().decode("utf8")
|
public_key = key.public_key().export_key().decode("utf8")
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item {
|
.navbar-item {
|
||||||
// see ../components/_details.scss :: Navbar details
|
/* see ../components/_details.scss :: Navbar details */
|
||||||
padding-right: 1.75rem;
|
padding-right: 1.75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -109,3 +109,9 @@
|
||||||
max-height: 35em;
|
max-height: 35em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .button {
|
||||||
|
@include mobile {
|
||||||
|
font-size: $size-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,3 +16,5 @@ MEDIUM = "medium_priority"
|
||||||
HIGH = "high_priority"
|
HIGH = "high_priority"
|
||||||
# import items get their own queue because they're such a pain in the ass
|
# import items get their own queue because they're such a pain in the ass
|
||||||
IMPORTS = "imports"
|
IMPORTS = "imports"
|
||||||
|
# I keep making more queues?? this one broadcasting out
|
||||||
|
BROADCAST = "broadcast"
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notification has-background-body p-2 mb-2 clip-text">
|
<div class="notification has-background-body p-2 mb-2 clip-text">
|
||||||
{% include "snippets/status/content_status.html" with hide_book=True trim_length=70 hide_more=True %}
|
{% include "snippets/status/content_status.html" with hide_book=True trim_length=70 hide_more=True expand=False %}
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ status.remote_id }}">
|
<a href="{{ status.remote_id }}">
|
||||||
<span>{% trans "View status" %}</span>
|
<span>{% trans "View status" %}</span>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="is-main block">
|
<div class="is-main block">
|
||||||
{% include 'snippets/status/status.html' with status=status main=True %}
|
{% include 'snippets/status/status.html' with status=status main=True expand=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for child in children %}
|
{% for child in children %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
const tour = new Shepherd.Tour({
|
const tour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
const tour = new Shepherd.Tour({
|
const tour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
const initiateTour = new Shepherd.Tour({
|
const initiateTour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
{% load user_page_tags %}
|
{% load user_page_tags %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
|
|
||||||
const tour = new Shepherd.Tour({
|
const tour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
|
|
||||||
let localResult = document.querySelector(".local-book-search-result");
|
let localResult = document.querySelector(".local-book-search-result");
|
||||||
let remoteResult = document.querySelector(".remote-book-search-result");
|
let remoteResult = document.querySelector(".remote-book-search-result");
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
const tour = new Shepherd.Tour({
|
const tour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
const tour = new Shepherd.Tour({
|
const tour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
const tour = new Shepherd.Tour({
|
const tour = new Shepherd.Tour({
|
||||||
exitOnEsc: true,
|
exitOnEsc: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -183,7 +183,7 @@
|
||||||
{% include 'snippets/footer.html' %}
|
{% include 'snippets/footer.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
var csrf_token = '{{ csrf_token }}';
|
var csrf_token = '{{ csrf_token }}';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
|
<link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
function closeWindow() {
|
function closeWindow() {
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
var csrf_token = '{{ csrf_token }}';
|
var csrf_token = '{{ csrf_token }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'js/bookwyrm.js' %}?v={{ js_cache }}"></script>
|
<script src="{% static 'js/bookwyrm.js' %}?v={{ js_cache }}"></script>
|
||||||
|
|
|
@ -20,31 +20,37 @@
|
||||||
{% if queues %}
|
{% if queues %}
|
||||||
<section class="block content">
|
<section class="block content">
|
||||||
<h2>{% trans "Queues" %}</h2>
|
<h2>{% trans "Queues" %}</h2>
|
||||||
<div class="columns has-text-centered">
|
<div class="columns has-text-centered is-multiline">
|
||||||
<div class="column is-3">
|
<div class="column is-4">
|
||||||
<div class="notification">
|
<div class="notification">
|
||||||
<p class="header">{% trans "Low priority" %}</p>
|
<p class="header">{% trans "Low priority" %}</p>
|
||||||
<p class="title is-5">{{ queues.low_priority|intcomma }}</p>
|
<p class="title is-5">{{ queues.low_priority|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-4">
|
||||||
<div class="notification">
|
<div class="notification">
|
||||||
<p class="header">{% trans "Medium priority" %}</p>
|
<p class="header">{% trans "Medium priority" %}</p>
|
||||||
<p class="title is-5">{{ queues.medium_priority|intcomma }}</p>
|
<p class="title is-5">{{ queues.medium_priority|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-4">
|
||||||
<div class="notification">
|
<div class="notification">
|
||||||
<p class="header">{% trans "High priority" %}</p>
|
<p class="header">{% trans "High priority" %}</p>
|
||||||
<p class="title is-5">{{ queues.high_priority|intcomma }}</p>
|
<p class="title is-5">{{ queues.high_priority|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-6">
|
||||||
<div class="notification">
|
<div class="notification">
|
||||||
<p class="header">{% trans "Imports" %}</p>
|
<p class="header">{% trans "Imports" %}</p>
|
||||||
<p class="title is-5">{{ queues.imports|intcomma }}</p>
|
<p class="title is-5">{{ queues.imports|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column is-6">
|
||||||
|
<div class="notification">
|
||||||
|
<p class="header">{% trans "Broadcasts" %}</p>
|
||||||
|
<p class="title is-5">{{ queues.broadcast|intcomma }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
var registerStats = new Chart(
|
var registerStats = new Chart(
|
||||||
document.getElementById('register_stats'),
|
document.getElementById('register_stats'),
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
|
|
||||||
var statusStats = new Chart(
|
var statusStats = new Chart(
|
||||||
document.getElementById('status_stats'),
|
document.getElementById('status_stats'),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
|
|
||||||
var userStats = new Chart(
|
var userStats = new Chart(
|
||||||
document.getElementById('user_stats'),
|
document.getElementById('user_stats'),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<script>
|
<script nonce="{{request.csp_nonce}}">
|
||||||
|
|
||||||
var worksStats = new Chart(
|
var worksStats = new Chart(
|
||||||
document.getElementById('works_stats'),
|
document.getElementById('works_stats'),
|
||||||
|
|
|
@ -65,6 +65,22 @@ uuid: a unique identifier used to make html "id" attributes unique and clarify j
|
||||||
{% if not draft %}data-cache-draft="id_position_{{ book.id }}_{{ type }}"{% endif %}
|
{% if not draft %}data-cache-draft="id_position_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="button is-static">
|
||||||
|
{% trans "to" %}
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
aria-label="{% if draft.position_mode == 'PG' %}Page{% else %}Percent{% endif %}"
|
||||||
|
class="input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
name="endposition"
|
||||||
|
size="3"
|
||||||
|
value="{% firstof draft.endposition '' %}"
|
||||||
|
id="endposition_{{ uuid }}"
|
||||||
|
{% if not draft %}data-cache-draft="id_endposition_{{ book.id }}_{{ type }}"{% endif %}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
|
|
||||||
<span class="icon icon-arrow-left" aria-hidden="true"></span>
|
<span class="icon icon-arrow-left" aria-hidden="true"></span>
|
||||||
{% trans "Previous" %}
|
{% trans "Older" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
|
|
||||||
{% trans "Next" %}
|
{% trans "Newer" %}
|
||||||
<span class="icon icon-arrow-right" aria-hidden="true"></span>
|
<span class="icon icon-arrow-right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
{% join "report" report_uuid as modal_id %}
|
{% join "report" report_uuid as modal_id %}
|
||||||
<button
|
<button
|
||||||
class="button is-small is-danger is-light is-fullwidth"
|
class="button is-small is-danger is-light is-fullwidth {{ class }}"
|
||||||
type="button"
|
type="button"
|
||||||
data-modal-open="{{ modal_id }}"
|
data-modal-open="{{ modal_id }}"
|
||||||
{% if is_current %}disabled{% endif %}
|
{% if is_current %}disabled{% endif %}
|
||||||
|
|
|
@ -6,9 +6,8 @@
|
||||||
{% if status_type == 'GeneratedNote' or status_type == 'Rating' %}
|
{% if status_type == 'GeneratedNote' or status_type == 'Rating' %}
|
||||||
{% include 'snippets/status/generated_status.html' with status=status %}
|
{% include 'snippets/status/generated_status.html' with status=status %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'snippets/status/content_status.html' with status=status %}
|
{% include 'snippets/status/content_status.html' with status=status expand=expand %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -99,9 +99,9 @@
|
||||||
— {% include 'snippets/book_titleby.html' with book=status.book %}
|
— {% include 'snippets/book_titleby.html' with book=status.book %}
|
||||||
{% if status.position %}
|
{% if status.position %}
|
||||||
{% if status.position_mode == 'PG' %}
|
{% if status.position_mode == 'PG' %}
|
||||||
{% blocktrans with page=status.position|intcomma %}(Page {{ page }}){% endblocktrans %}
|
{% blocktrans with page=status.position|intcomma %}(Page {{ page }}{% endblocktrans%}{% if status.endposition and status.endposition != status.position %} - {% blocktrans with endpage=status.endposition|intcomma %}{{ endpage }}{% endblocktrans %}{% endif%})
|
||||||
{% else %}
|
{% else %}
|
||||||
{% blocktrans with percent=status.position %}({{ percent }}%){% endblocktrans %}
|
{% blocktrans with percent=status.position %}({{ percent }}%{% endblocktrans %}{% if status.endposition and status.endposition != status.position %}{% blocktrans with endpercent=status.endposition|intcomma %} - {{ endpercent }}%{% endblocktrans %}{% endif %})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
|
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
|
||||||
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
|
{% with full=status.content|safe no_trim=status.content_warning|default:expand itemprop="reviewBody" %}
|
||||||
{% include 'snippets/trimmed_text.html' %}
|
{% include 'snippets/trimmed_text.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -155,4 +155,3 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
{% trans "boosted" %}
|
{% trans "boosted" %}
|
||||||
{% include 'snippets/status/body.html' with status=status|boosted_status %}
|
{% include 'snippets/status/body.html' with status=status|boosted_status %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'snippets/status/body.html' with status=status %}
|
{% include 'snippets/status/body.html' with status=status expand=expand %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -257,6 +257,33 @@ class BookViews(TestCase):
|
||||||
self.assertEqual(mock.call_args[0][0], "https://openlibrary.org/book/123")
|
self.assertEqual(mock.call_args[0][0], "https://openlibrary.org/book/123")
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
|
||||||
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||||
|
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||||
|
def test_quotation_endposition(self, *_):
|
||||||
|
"""make sure the endposition is served as well"""
|
||||||
|
view = views.Book.as_view()
|
||||||
|
|
||||||
|
_ = models.Quotation.objects.create(
|
||||||
|
user=self.local_user,
|
||||||
|
book=self.book,
|
||||||
|
content="hi",
|
||||||
|
quote="wow",
|
||||||
|
position=12,
|
||||||
|
endposition=13,
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
|
||||||
|
with patch("bookwyrm.views.books.books.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.book.id, user_statuses="quotation")
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
print(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(result.context_data["statuses"].object_list[0].endposition, 13)
|
||||||
|
|
||||||
|
|
||||||
def _setup_cover_url():
|
def _setup_cover_url():
|
||||||
"""creates cover url mock"""
|
"""creates cover url mock"""
|
||||||
|
|
|
@ -11,6 +11,7 @@ from bookwyrm.tests.validate_html import validate_html
|
||||||
class DiscoverViews(TestCase):
|
class DiscoverViews(TestCase):
|
||||||
"""pages you land on without really trying"""
|
"""pages you land on without really trying"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""we need basic test data and mocks"""
|
"""we need basic test data and mocks"""
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
@ -43,7 +44,7 @@ class DiscoverViews(TestCase):
|
||||||
|
|
||||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||||
def test_discover_page(self, *_):
|
def test_discover_page_with_posts(self, *_):
|
||||||
"""there are so many views, this just makes sure it LOADS"""
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
view = views.Discover.as_view()
|
view = views.Discover.as_view()
|
||||||
request = self.factory.get("")
|
request = self.factory.get("")
|
||||||
|
@ -53,17 +54,34 @@ class DiscoverViews(TestCase):
|
||||||
title="hi", parent_work=models.Work.objects.create(title="work")
|
title="hi", parent_work=models.Work.objects.create(title="work")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
models.ReviewRating.objects.create(
|
||||||
|
book=book,
|
||||||
|
user=self.local_user,
|
||||||
|
rating=4,
|
||||||
|
)
|
||||||
|
models.Review.objects.create(
|
||||||
|
book=book,
|
||||||
|
user=self.local_user,
|
||||||
|
content="hello",
|
||||||
|
rating=4,
|
||||||
|
)
|
||||||
models.Comment.objects.create(
|
models.Comment.objects.create(
|
||||||
book=book,
|
book=book,
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
content="hello",
|
content="hello",
|
||||||
)
|
)
|
||||||
|
models.Quotation.objects.create(
|
||||||
|
book=book,
|
||||||
|
user=self.local_user,
|
||||||
|
quote="beep",
|
||||||
|
content="hello",
|
||||||
|
)
|
||||||
models.Status.objects.create(user=self.local_user, content="beep")
|
models.Status.objects.create(user=self.local_user, content="beep")
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"bookwyrm.activitystreams.ActivityStream.get_activity_stream"
|
"bookwyrm.activitystreams.ActivityStream.get_activity_stream"
|
||||||
) as mock:
|
) as mock:
|
||||||
mock.return_value = models.Status.objects.all()
|
mock.return_value = models.Status.objects.select_subclasses().all()
|
||||||
result = view(request)
|
result = view(request)
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.views.decorators.http import require_GET
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
from celerywyrm import settings
|
from celerywyrm import settings
|
||||||
from bookwyrm.tasks import app as celery
|
from bookwyrm.tasks import app as celery, LOW, MEDIUM, HIGH, IMPORTS, BROADCAST
|
||||||
|
|
||||||
r = redis.from_url(settings.REDIS_BROKER_URL)
|
r = redis.from_url(settings.REDIS_BROKER_URL)
|
||||||
|
|
||||||
|
@ -35,10 +35,11 @@ class CeleryStatus(View):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queues = {
|
queues = {
|
||||||
"low_priority": r.llen("low_priority"),
|
LOW: r.llen(LOW),
|
||||||
"medium_priority": r.llen("medium_priority"),
|
MEDIUM: r.llen(MEDIUM),
|
||||||
"high_priority": r.llen("high_priority"),
|
HIGH: r.llen(HIGH),
|
||||||
"imports": r.llen("imports"),
|
IMPORTS: r.llen(IMPORTS),
|
||||||
|
BROADCAST: r.llen(BROADCAST),
|
||||||
}
|
}
|
||||||
# pylint: disable=broad-except
|
# pylint: disable=broad-except
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|
|
@ -12,6 +12,8 @@ from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
|
from csp.decorators import csp_update
|
||||||
|
|
||||||
from bookwyrm import models, settings
|
from bookwyrm import models, settings
|
||||||
from bookwyrm.connectors.abstract_connector import get_data
|
from bookwyrm.connectors.abstract_connector import get_data
|
||||||
from bookwyrm.connectors.connector_manager import ConnectorException
|
from bookwyrm.connectors.connector_manager import ConnectorException
|
||||||
|
@ -27,6 +29,9 @@ from bookwyrm.utils import regex
|
||||||
class Dashboard(View):
|
class Dashboard(View):
|
||||||
"""admin overview"""
|
"""admin overview"""
|
||||||
|
|
||||||
|
@csp_update(
|
||||||
|
SCRIPT_SRC="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"
|
||||||
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""list of users"""
|
"""list of users"""
|
||||||
data = get_charts_and_stats(request)
|
data = get_charts_and_stats(request)
|
||||||
|
|
|
@ -8,6 +8,8 @@ from django.http import JsonResponse
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
|
from csp.decorators import csp_update
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.connectors import connector_manager
|
from bookwyrm.connectors import connector_manager
|
||||||
from bookwyrm.book_search import search, format_search_result
|
from bookwyrm.book_search import search, format_search_result
|
||||||
|
@ -21,6 +23,7 @@ from .helpers import handle_remote_webfinger
|
||||||
class Search(View):
|
class Search(View):
|
||||||
"""search users or books"""
|
"""search users or books"""
|
||||||
|
|
||||||
|
@csp_update(IMG_SRC="*")
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""that search bar up top"""
|
"""that search bar up top"""
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
|
|
|
@ -6,7 +6,7 @@ After=network.target postgresql.service redis.service
|
||||||
User=bookwyrm
|
User=bookwyrm
|
||||||
Group=bookwyrm
|
Group=bookwyrm
|
||||||
WorkingDirectory=/opt/bookwyrm/
|
WorkingDirectory=/opt/bookwyrm/
|
||||||
ExecStart=/opt/bookwyrm/venv/bin/celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority,import
|
ExecStart=/opt/bookwyrm/venv/bin/celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority,import,broadcast
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=inherit
|
StandardError=inherit
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ services:
|
||||||
build: .
|
build: .
|
||||||
networks:
|
networks:
|
||||||
- main
|
- main
|
||||||
command: celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority,imports
|
command: celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority,imports,broadcast
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- static_volume:/app/static
|
- static_volume:/app/static
|
||||||
|
|
|
@ -2,12 +2,13 @@ aiohttp==3.8.3
|
||||||
bleach==5.0.1
|
bleach==5.0.1
|
||||||
celery==5.2.7
|
celery==5.2.7
|
||||||
colorthief==0.2.1
|
colorthief==0.2.1
|
||||||
Django==3.2.17
|
Django==3.2.18
|
||||||
django-celery-beat==2.4.0
|
django-celery-beat==2.4.0
|
||||||
django-compressor==4.3.1
|
django-compressor==4.3.1
|
||||||
django-imagekit==4.1.0
|
django-imagekit==4.1.0
|
||||||
django-model-utils==4.3.1
|
django-model-utils==4.3.1
|
||||||
django-sass-processor==1.2.2
|
django-sass-processor==1.2.2
|
||||||
|
django-csp==3.7
|
||||||
environs==9.5.0
|
environs==9.5.0
|
||||||
flower==1.2.0
|
flower==1.2.0
|
||||||
libsass==0.22.0
|
libsass==0.22.0
|
||||||
|
|
Loading…
Reference in a new issue