Merge branch 'main' into totp-window

This commit is contained in:
Hugh Rundle 2023-02-27 18:21:39 +11:00 committed by GitHub
commit bba0d09fa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 187 additions and 50 deletions

View file

@ -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=

View file

@ -53,6 +53,7 @@ class QuotationForm(CustomForm):
"sensitive", "sensitive",
"privacy", "privacy",
"position", "position",
"endposition",
"position_mode", "position_mode",
] ]

View 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",
),
),
]

View file

@ -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)

View file

@ -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(

View file

@ -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,

View file

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env() env = Env()
env.read_env() env.read_env()
DOMAIN = env("DOMAIN") DOMAIN = env("DOMAIN")
VERSION = "0.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)

View file

@ -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")

View file

@ -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;
}
}

View file

@ -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"

View file

@ -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>

View file

@ -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 %}

View file

@ -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,
}); });

View file

@ -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,
}); });

View file

@ -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,
}); });

View file

@ -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,

View file

@ -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");

View file

@ -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,
}); });

View file

@ -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,
}); });

View file

@ -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,
}); });

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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'),
{ {

View file

@ -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'),

View file

@ -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'),

View file

@ -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'),

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -99,9 +99,9 @@
&mdash; {% include 'snippets/book_titleby.html' with book=status.book %} &mdash; {% 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 %}

View file

@ -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 %}

View file

@ -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"""

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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