Merge branch 'main' into admin-roles

This commit is contained in:
Mouse Reeve 2022-12-11 11:58:54 -08:00
commit 58e8c0b5ec
40 changed files with 644 additions and 61 deletions

View file

@ -19,6 +19,8 @@ class BookData(ActivityObject):
viaf: str = None viaf: str = None
wikidata: str = None wikidata: str = None
asin: str = None asin: str = None
aasin: str = None
isfdb: str = None
lastEditedBy: str = None lastEditedBy: str = None
links: List[str] = field(default_factory=lambda: []) links: List[str] = field(default_factory=lambda: [])
fileLinks: List[str] = field(default_factory=lambda: []) fileLinks: List[str] = field(default_factory=lambda: [])

View file

@ -18,6 +18,12 @@ def email_data():
} }
def test_email(user):
"""Just an admin checking if emails are sending"""
data = email_data()
send_email(user.email, *format_email("test", data))
def email_confirmation_email(user): def email_confirmation_email(user):
"""newly registered users confirm email address""" """newly registered users confirm email address"""
data = email_data() data = email_data()

View file

@ -21,6 +21,7 @@ class AuthorForm(CustomForm):
"inventaire_id", "inventaire_id",
"librarything_key", "librarything_key",
"goodreads_key", "goodreads_key",
"isfdb",
"isni", "isni",
] ]
widgets = { widgets = {

View file

@ -18,19 +18,28 @@ class CoverForm(CustomForm):
class EditionForm(CustomForm): class EditionForm(CustomForm):
class Meta: class Meta:
model = models.Edition model = models.Edition
exclude = [ fields = [
"remote_id", "title",
"origin_id", "subtitle",
"created_date", "description",
"updated_date", "series",
"edition_rank", "series_number",
"authors", "languages",
"parent_work", "subjects",
"shelves", "publishers",
"connector", "first_published_date",
"search_vector", "published_date",
"links", "cover",
"file_links", "physical_format",
"physical_format_detail",
"pages",
"isbn_13",
"isbn_10",
"openlibrary_key",
"inventaire_id",
"goodreads_key",
"oclc_number",
"asin",
] ]
widgets = { widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}), "title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
@ -73,10 +82,15 @@ class EditionForm(CustomForm):
"inventaire_id": forms.TextInput( "inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"} attrs={"aria-describedby": "desc_inventaire_id"}
), ),
"goodreads_key": forms.TextInput(
attrs={"aria-describedby": "desc_goodreads_key"}
),
"oclc_number": forms.TextInput( "oclc_number": forms.TextInput(
attrs={"aria-describedby": "desc_oclc_number"} attrs={"aria-describedby": "desc_oclc_number"}
), ),
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}), "ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
"AASIN": forms.TextInput(attrs={"aria-describedby": "desc_AASIN"}),
"isfdb": forms.TextInput(attrs={"aria-describedby": "desc_isfdb"}),
} }

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.16 on 2022-12-05 17:01
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0167_auto_20221125_1900"),
]
operations = [
migrations.AddField(
model_name="author",
name="aasin",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="aasin",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.16 on 2022-12-06 09:02
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0168_auto_20221205_1701"),
]
operations = [
migrations.AddField(
model_name="author",
name="isfdb",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="isfdb",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
]

View file

@ -24,6 +24,9 @@ class Author(BookDataModel):
gutenberg_id = fields.CharField( gutenberg_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True max_length=255, blank=True, null=True, deduplication_field=True
) )
isfdb = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
# idk probably other keys would be useful here? # idk probably other keys would be useful here?
born = fields.DateTimeField(blank=True, null=True) born = fields.DateTimeField(blank=True, null=True)
died = fields.DateTimeField(blank=True, null=True) died = fields.DateTimeField(blank=True, null=True)
@ -60,6 +63,11 @@ class Author(BookDataModel):
"""generate the url from the openlibrary id""" """generate the url from the openlibrary id"""
return f"https://openlibrary.org/authors/{self.openlibrary_key}" return f"https://openlibrary.org/authors/{self.openlibrary_key}"
@property
def isfdb_link(self):
"""generate the url from the isni id"""
return f"https://www.isfdb.org/cgi-bin/ea.cgi?{self.isfdb}"
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 f"https://{DOMAIN}/author/{self.id}" return f"https://{DOMAIN}/author/{self.id}"

View file

@ -55,6 +55,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
asin = fields.CharField( asin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True max_length=255, blank=True, null=True, deduplication_field=True
) )
aasin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
isfdb = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
search_vector = SearchVectorField(null=True) search_vector = SearchVectorField(null=True)
last_edited_by = fields.ForeignKey( last_edited_by = fields.ForeignKey(
@ -73,6 +79,11 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
"""generate the url from the inventaire id""" """generate the url from the inventaire id"""
return f"https://inventaire.io/entity/{self.inventaire_id}" return f"https://inventaire.io/entity/{self.inventaire_id}"
@property
def isfdb_link(self):
"""generate the url from the isfdb id"""
return f"https://www.isfdb.org/cgi-bin/title.cgi?{self.isfdb}"
class Meta: class Meta:
"""can't initialize this model, that wouldn't make sense""" """can't initialize this model, that wouldn't make sense"""

View file

@ -28,7 +28,7 @@
<meta itemprop="name" content="{{ author.name }}"> <meta itemprop="name" content="{{ author.name }}">
{% firstof author.aliases author.born author.died as details %} {% firstof author.aliases author.born author.died as details %}
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %} {% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni author.isfdb as links %}
{% if details or links %} {% if details or links %}
<div class="column is-3"> <div class="column is-3">
{% if details %} {% if details %}
@ -81,6 +81,14 @@
</div> </div>
{% endif %} {% endif %}
{% if author.isfdb %}
<div class="mt-1">
<a itemprop="sameAs" href="{{ author.isfdb_link }}" rel="nofollow noopener noreferrer" target="_blank">
{% trans "View on ISFDB" %}
</a>
</div>
{% endif %}
{% trans "Load data" as button_text %} {% trans "Load data" as button_text %}
{% if author.openlibrary_key %} {% if author.openlibrary_key %}
<div class="mt-1 is-flex"> <div class="mt-1 is-flex">
@ -128,6 +136,14 @@
</a> </a>
</div> </div>
{% endif %} {% endif %}
{% if author.isfdb %}
<div>
<a itemprop="sameAs" href="https://www.isfdb.org/cgi-bin/ea.cgi?{{ author.isfdb }}" target="_blank" rel="nofollow noopener noreferrer">
{% trans "View ISFDB entry" %}
</a>
</div>
{% endif %}
</div> </div>
</section> </section>
{% endif %} {% endif %}

View file

@ -101,6 +101,13 @@
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %} {% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
</div> </div>
<div class="field">
<label class="label" for="id_isfdb">{% trans "ISFDB:" %}</label>
{{ form.isfdb }}
{% include 'snippets/form_errors.html' with errors_list=form.isfdb.errors id="desc_isfdb" %}
</div>
<div class="field"> <div class="field">
<label class="label" for="id_isni">{% trans "ISNI:" %}</label> <label class="label" for="id_isni">{% trans "ISNI:" %}</label>
{{ form.isni }} {{ form.isni }}

View file

@ -25,7 +25,7 @@
<div class="block" itemscope itemtype="https://schema.org/Book"> <div class="block" itemscope itemtype="https://schema.org/Book">
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<h1 class="title" itemprop="name"> <h1 class="title" itemprop="name" dir="auto">
{{ book.title }} {{ book.title }}
</h1> </h1>
@ -37,7 +37,7 @@
content="{{ book.subtitle | escape }}" content="{{ book.subtitle | escape }}"
> >
<span class="has-text-weight-bold"> <span class="has-text-weight-bold" dir="auto">
{{ book.subtitle }} {{ book.subtitle }}
</span> </span>
{% endif %} {% endif %}
@ -52,7 +52,7 @@
{% endif %} {% endif %}
{% if book.authors.exists %} {% if book.authors.exists %}
<div class="subtitle"> <div class="subtitle" dir="auto">
{% trans "by" %} {% include 'snippets/authors.html' with book=book %} {% trans "by" %} {% include 'snippets/authors.html' with book=book %}
</div> </div>
{% endif %} {% endif %}
@ -158,6 +158,13 @@
{% endif %} {% endif %}
</p> </p>
{% endif %} {% endif %}
{% if book.isfdb %}
<p>
<a href="{{ book.isfdb_link }}" target="_blank" rel="nofollow noopener noreferrer">
{% trans "View on ISFDB" %}
</a>
</p>
{% endif %}
</section> </section>
</div> </div>

View file

@ -1,7 +1,7 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% if book.isbn_13 or book.oclc_number or book.asin %} {% if book.isbn_13 or book.oclc_number or book.asin or book.aasin or book.isfdb %}
<dl> <dl>
{% if book.isbn_13 %} {% if book.isbn_13 %}
<div class="is-flex"> <div class="is-flex">
@ -23,6 +23,27 @@
<dd>{{ book.asin }}</dd> <dd>{{ book.asin }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if book.aasin %}
<div class="is-flex">
<dt class="mr-1">{% trans "Audible ASIN:" %}</dt>
<dd>{{ book.aasin }}</dd>
</div>
{% endif %}
{% if book.isfdb %}
<div class="is-flex">
<dt class="mr-1">{% trans "ISFDB ID:" %}</dt>
<dd>{{ book.isfdb }}</dd>
</div>
{% endif %}
{% if book.goodreads_key %}
<div class="is-flex">
<dt class="mr-1">{% trans "Goodreads:" %}</dt>
<dd>{{ book.goodreads_key }}</dd>
</div>
{% endif %}
</dl> </dl>
{% endif %} {% endif %}
{% endspaceless %} {% endspaceless %}

View file

@ -65,17 +65,17 @@
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}"> <input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
<div class="column is-half"> <div class="column is-half">
{% for author in author_matches %} {% for author in author_matches %}
<fieldset> <fieldset class="block">
<legend class="title is-5 mb-1"> <legend class="title is-5 mb-1">
{% blocktrans with name=author.name %}Is "{{ name }}" one of these authors?{% endblocktrans %} {% blocktrans with name=author.name %}Is "{{ name }}" one of these authors?{% endblocktrans %}
</legend> </legend>
{% with forloop.counter0 as counter %} {% with forloop.counter0 as counter %}
{% for match in author.matches %} {% for match in author.matches %}
<label class="label"> <label class="label mb-0">
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required> <input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
{{ match.name }} {{ match.name }}
</label> </label>
<p class="help ml-5 mb-2"> <p class="help ml-5 mb-0 mt-0">
{% with book_title=match.book_set.first.title alt_title=match.bio %} {% with book_title=match.book_set.first.title alt_title=match.bio %}
{% if book_title %} {% if book_title %}
<a href="{{ match.local_path }}" target="_blank" rel="nofollow noopener noreferrer">{% blocktrans trimmed %} <a href="{{ match.local_path }}" target="_blank" rel="nofollow noopener noreferrer">{% blocktrans trimmed %}
@ -98,6 +98,9 @@
</label> </label>
{% endwith %} {% endwith %}
</fieldset> </fieldset>
{% if not forloop.last %}
<hr aria-hidden="true">
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}

View file

@ -327,6 +327,15 @@
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %} {% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
</div> </div>
<div class="field">
<label class="label" for="id_goodreads_key">
{% trans "Goodreads key:" %}
</label>
{{ form.goodreads_key }}
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
</div>
<div class="field"> <div class="field">
<label class="label" for="id_oclc_number"> <label class="label" for="id_oclc_number">
{% trans "OCLC Number:" %} {% trans "OCLC Number:" %}
@ -344,6 +353,24 @@
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %} {% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
</div> </div>
<div class="field">
<label class="label" for="id_aasin">
{% trans "Audible ASIN:" %}
</label>
{{ form.aasin }}
{% include 'snippets/form_errors.html' with errors_list=form.AASIN.errors id="desc_AASIN" %}
</div>
<div class="field">
<label class="label" for="id_isfdb">
{% trans "ISFDB ID:" %}
</label>
{{ form.isfdb }}
{% include 'snippets/form_errors.html' with errors_list=form.isfdb.errors id="desc_isfdb" %}
</div>
</div> </div>
</section> </section>
</div> </div>

View file

@ -0,0 +1,12 @@
{% extends 'email/html_layout.html' %}
{% load i18n %}
{% block content %}
<p>
{% blocktrans trimmed %}
This is a test email.
{% endblocktrans %}
</p>
{% endblock %}

View file

@ -0,0 +1,4 @@
{% load i18n %}
{% blocktrans trimmed %}
Test email
{% endblocktrans %}

View file

@ -0,0 +1,9 @@
{% extends 'email/text_layout.html' %}
{% load i18n %}
{% block content %}
{% blocktrans trimmed %}
This is a test email.
{% endblocktrans %}
{% endblock %}

View file

@ -13,6 +13,7 @@
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" /> <link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
<link rel="apple-touch-icon" href="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}">
{% if preview_images_enabled is True %} {% if preview_images_enabled is True %}
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">

View file

@ -0,0 +1,96 @@
{% extends 'settings/layout.html' %}
{% load humanize %}
{% load i18n %}
{% load celery_tags %}
{% block title %}{% trans "Email Configuration" %}{% endblock %}
{% block header %}{% trans "Email Configuration" %}{% endblock %}
{% block panel %}
{% if error %}
<div class="notification is-danger is-light">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "Error sending test email:" %}
{{ error }}
</span>
</div>
{% elif success %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% trans "Successfully sent test email." %}
</span>
</div>
{% endif %}
<section class="block content">
<dl>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Email sender:" %}
</dt>
<dd>
{{ email_sender }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Email backend:" %}
</dt>
<dd>
<code>{{ email_backend }}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Host:" %}
</dt>
<dd>
<code>{{ email_host }}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Host user:" %}
</dt>
<dd>
<code>{% firstof email_host_user "-" %}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Port:" %}
</dt>
<dd>
<code>{{ email_port }}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Use TLS:" %}
</dt>
<dd>
{{ email_use_tls|yesno }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Use SSL:" %}
</dt>
<dd>
{{ email_use_ssl|yesno }}
</dd>
</dl>
</section>
<section class="block content box">
<p>
{% blocktrans trimmed with email=request.user.email %}
Send test email to {{ email }}
{% endblocktrans %}
</p>
<form action="{% url 'settings-email-config' %}" method="post">
{% csrf_token %}
<button type="submit" class="button is-success">
{% trans "Send test email" %}
</button>
</form>
</section>
{% endblock %}

View file

@ -81,12 +81,14 @@
{% url 'settings-imports' as url %} {% url 'settings-imports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Imports" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Imports" %}</a>
</li> </li>
</ul>
<ul class="menu-list">
<li> <li>
{% url 'settings-celery' as url %} {% url 'settings-celery' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Celery status" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Celery status" %}</a>
</li> </li>
<li>
{% url 'settings-email-config' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Configuration" %}</a>
</li>
</ul> </ul>
{% endif %} {% endif %}
{% if perms.bookwyrm.edit_instance_settings %} {% if perms.bookwyrm.edit_instance_settings %}

View file

@ -66,6 +66,10 @@
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}> <li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Activity" %}</a> <a href="{{ url }}">{% trans "Activity" %}</a>
</li> </li>
{% url 'user-reviews-comments' user|username as url %}
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Reviews and Comments" %}</a>
</li>
{% if is_self or user.goal.exists %} {% if is_self or user.goal.exists %}
{% now 'Y' as year %} {% now 'Y' as year %}
{% url 'user-goal' user|username year as url %} {% url 'user-goal' user|username year as url %}

View file

@ -0,0 +1,30 @@
{% extends 'user/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}{{ user.display_name }}{% endblock %}
{% block header %}
<div class="columns is-mobile">
<div class="column">
<h1 class="title">{% trans "Reviews and Comments" %}</h1>
</div>
</div>
{% endblock %}
{% block panel %}
<div>
{% for activity in activities %}
<div class="block" id="feed_{{ activity.id }}">
{% include 'snippets/status/status.html' with status=activity %}
</div>
{% endfor %}
{% if not activities %}
<div class="block">
<p>{% trans "No reviews or comments yet!" %}</p>
</div>
{% endif %}
{% include 'snippets/pagination.html' with page=activities path=path %}
</div>
{% endblock %}

View file

@ -21,6 +21,7 @@
"openlibrary_key": "OL29486417M", "openlibrary_key": "OL29486417M",
"librarything_key": null, "librarything_key": null,
"goodreads_key": null, "goodreads_key": null,
"isfdb": null,
"attachment": [ "attachment": [
{ {
"url": "https://bookwyrm.social/images/covers/50202953._SX318_.jpg", "url": "https://bookwyrm.social/images/covers/50202953._SX318_.jpg",

View file

@ -194,7 +194,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue( self.assertTrue(
re.match( re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg", r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg",
activity["attachment"][0]["url"], activity["attachment"][0]["url"],
) )
) )

View file

@ -0,0 +1,46 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.management.commands import initdb
from bookwyrm.tests.validate_html import validate_html
class EmailConfigViews(TestCase):
"""every response to a get request, html or json"""
# pylint: disable=invalid-name
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
initdb.init_groups()
initdb.init_permissions()
group = Group.objects.get(name="admin")
self.local_user.groups.set([group])
models.SiteSettings.objects.create()
def test_email_config_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.EmailConfig.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -63,7 +63,7 @@ class ExportViews(TestCase):
# pylint: disable=line-too-long # pylint: disable=line-too-long
self.assertEqual( self.assertEqual(
result[0], result[0],
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n", b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n",
) )
expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,123456789X,9781234567890,,,,,\r\n" expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,,,123456789X,9781234567890,,,,,\r\n"
self.assertEqual(result[1].decode("utf-8"), expected) self.assertEqual(result[1].decode("utf-8"), expected)

View file

@ -233,3 +233,19 @@ class UserViews(TestCase):
result = views.user_redirect(request, "mouse") result = views.user_redirect(request, "mouse")
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
def test_reviews_comments_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.UserReviewsComments.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
request.user = self.anonymous_user
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -129,7 +129,7 @@ urlpatterns = [
), ),
re_path( re_path(
r"^settings/email-preview/?$", r"^settings/email-preview/?$",
views.admin.site.email_preview, views.admin.email_config.email_preview,
name="settings-email-preview", name="settings-email-preview",
), ),
re_path( re_path(
@ -324,6 +324,11 @@ urlpatterns = [
re_path( re_path(
r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery" r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery"
), ),
re_path(
r"^settings/email-config/?$",
views.EmailConfig.as_view(),
name="settings-email-config",
),
# landing pages # landing pages
re_path(r"^about/?$", views.about, name="about"), re_path(r"^about/?$", views.about, name="about"),
re_path(r"^privacy/?$", views.privacy, name="privacy"), re_path(r"^privacy/?$", views.privacy, name="privacy"),
@ -420,6 +425,11 @@ urlpatterns = [
name="user-relationships", name="user-relationships",
), ),
re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"), re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"),
re_path(
rf"{USER_PATH}/reviews-comments",
views.UserReviewsComments.as_view(),
name="user-reviews-comments",
),
# groups # groups
re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"), re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"),
re_path( re_path(

View file

@ -10,6 +10,7 @@ from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist from .admin.federation import AddFederatedServer, ImportServerBlocklist
from .admin.federation import block_server, unblock_server, refresh_server from .admin.federation import block_server, unblock_server, refresh_server
from .admin.email_blocklist import EmailBlocklist from .admin.email_blocklist import EmailBlocklist
from .admin.email_config import EmailConfig
from .admin.imports import ImportList, disable_imports, enable_imports from .admin.imports import ImportList, disable_imports, enable_imports
from .admin.ip_blocklist import IPBlocklist from .admin.ip_blocklist import IPBlocklist
from .admin.invite import ManageInvites, Invite, InviteRequest from .admin.invite import ManageInvites, Invite, InviteRequest
@ -137,7 +138,13 @@ from .setup import InstanceConfig, CreateAdmin
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress from .status import CreateStatus, EditStatus, DeleteStatus, update_progress
from .status import edit_readthrough from .status import edit_readthrough
from .updates import get_notification_count, get_unread_status_string from .updates import get_notification_count, get_unread_status_string
from .user import User, hide_suggestions, user_redirect, toggle_guided_tour from .user import (
User,
UserReviewsComments,
hide_suggestions,
user_redirect,
toggle_guided_tour,
)
from .relationships import Relationships from .relationships import Relationships
from .wellknown import * from .wellknown import *
from .annual_summary import ( from .annual_summary import (

View file

@ -0,0 +1,65 @@
""" is your email running? """
from django.contrib.auth.decorators import login_required, permission_required
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import emailing
from bookwyrm import settings
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
class EmailConfig(View):
"""View and test your emailing setup"""
def get(self, request):
"""View email config"""
data = view_data()
# TODO: show email previews
return TemplateResponse(request, "settings/email_config.html", data)
def post(self, request):
"""Send test email"""
data = view_data()
try:
emailing.test_email(request.user)
data["success"] = True
except Exception as err: # pylint: disable=broad-except
data["error"] = err
return TemplateResponse(request, "settings/email_config.html", data)
def view_data():
"""helper to get data for view"""
return {
"email_backend": settings.EMAIL_BACKEND,
"email_host": settings.EMAIL_HOST,
"email_port": settings.EMAIL_PORT,
"Email_host_user": settings.EMAIL_HOST_USER,
"email_use_tls": settings.EMAIL_USE_TLS,
"email_use_ssl": settings.EMAIL_USE_SSL,
"email_sender": settings.EMAIL_SENDER,
}
@login_required
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
def email_preview(request):
"""for development, renders and example email template"""
template = request.GET.get("email")
data = emailing.email_data()
data["subject_path"] = f"email/{template}/subject.html"
data["html_content_path"] = f"email/{template}/html_content.html"
data["text_content_path"] = f"email/{template}/text_content.html"
data["reset_link"] = "https://example.com/link"
data["invite_link"] = "https://example.com/link"
data["confirmation_link"] = "https://example.com/link"
data["confirmation_code"] = "AKJHKDGKJSDFG"
data["reporter"] = "ConcernedUser"
data["reportee"] = "UserName"
data["report_link"] = "https://example.com/link"
return TemplateResponse(request, "email/preview.html", data)

View file

@ -87,22 +87,3 @@ class Registration(View):
data = {"form": forms.RegistrationForm(instance=site), "success": True} data = {"form": forms.RegistrationForm(instance=site), "success": True}
return TemplateResponse(request, "settings/registration.html", data) return TemplateResponse(request, "settings/registration.html", data)
@login_required
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
def email_preview(request):
"""for development, renders and example email template"""
template = request.GET.get("email")
data = emailing.email_data()
data["subject_path"] = f"email/{template}/subject.html"
data["html_content_path"] = f"email/{template}/html_content.html"
data["text_content_path"] = f"email/{template}/text_content.html"
data["reset_link"] = "https://example.com/link"
data["invite_link"] = "https://example.com/link"
data["confirmation_link"] = "https://example.com/link"
data["confirmation_code"] = "AKJHKDGKJSDFG"
data["reporter"] = "ConcernedUser"
data["reportee"] = "UserName"
data["report_link"] = "https://example.com/link"
return TemplateResponse(request, "email/preview.html", data)

View file

@ -49,6 +49,8 @@ class Editions(View):
"isbn_13", "isbn_13",
"oclc_number", "oclc_number",
"asin", "asin",
"aasin",
"isfdb",
] ]
search_filter_entries = [ search_filter_entries = [
{f"{f}__icontains": query} for f in searchable_fields {f"{f}__icontains": query} for f in searchable_fields

View file

@ -1,6 +1,7 @@
""" The user profile """ """ The user profile """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q
from django.http import Http404 from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -100,6 +101,49 @@ class User(View):
return TemplateResponse(request, "user/user.html", data) return TemplateResponse(request, "user/user.html", data)
class UserReviewsComments(View):
"""user's activity filtered by reviews and comments"""
def get(self, request, username):
"""user's activity filtered by reviews and comments"""
user = get_user_from_username(request.user, username)
is_self = request.user.id == user.id
activities = (
models.Status.privacy_filter(
request.user,
)
.filter(
Q(review__isnull=False) | Q(comment__isnull=False),
user=user,
)
.exclude(
privacy="direct",
)
.select_related(
"user",
"reply_parent",
"review__book",
"comment__book",
"quotation__book",
)
.prefetch_related(
"mention_books",
"mention_users",
"attachments",
)
)
paginated = Paginator(activities, PAGE_LENGTH)
data = {
"user": user,
"is_self": is_self,
"activities": paginated.get_page(request.GET.get("page", 1)),
}
return TemplateResponse(request, "user/reviews_comments.html", data)
@require_POST @require_POST
@login_required @login_required
def hide_suggestions(request): def hide_suggestions(request):

6
bw-dev
View file

@ -174,6 +174,10 @@ case "$CMD" in
prod_error prod_error
docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
;; ;;
eslint)
prod_error
docker-compose run --rm dev-tools npx eslint bookwyrm/static --ext .js
;;
stylelint) stylelint)
prod_error prod_error
docker-compose run --rm dev-tools npx stylelint \ docker-compose run --rm dev-tools npx stylelint \
@ -185,6 +189,7 @@ case "$CMD" in
runweb pylint bookwyrm/ runweb pylint bookwyrm/
docker-compose run --rm dev-tools black celerywyrm bookwyrm docker-compose run --rm dev-tools black celerywyrm bookwyrm
docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
docker-compose run --rm dev-tools npx eslint bookwyrm/static --ext .js
docker-compose run --rm dev-tools npx stylelint \ docker-compose run --rm dev-tools npx stylelint \
bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \ bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \
--config dev-tools/.stylelintrc.js --config dev-tools/.stylelintrc.js
@ -283,6 +288,7 @@ case "$CMD" in
echo " clean" echo " clean"
echo " black" echo " black"
echo " prettier" echo " prettier"
echo " eslint"
echo " stylelint" echo " stylelint"
echo " formatters" echo " formatters"
echo " collectstatic_watch" echo " collectstatic_watch"

View file

@ -22,6 +22,7 @@ build \
clean \ clean \
black \ black \
prettier \ prettier \
eslint \
stylelint \ stylelint \
formatters \ formatters \
collectstatic_watch \ collectstatic_watch \
@ -59,6 +60,7 @@ __bw_complete "$commands" "build" "build the containers"
__bw_complete "$commands" "clean" "bring the cluster down and remove all containers" __bw_complete "$commands" "clean" "bring the cluster down and remove all containers"
__bw_complete "$commands" "black" "run Python code formatting tool" __bw_complete "$commands" "black" "run Python code formatting tool"
__bw_complete "$commands" "prettier" "run JavaScript code formatting tool" __bw_complete "$commands" "prettier" "run JavaScript code formatting tool"
__bw_complete "$commands" "eslint" "run JavaScript linting tool"
__bw_complete "$commands" "stylelint" "run SCSS linting tool" __bw_complete "$commands" "stylelint" "run SCSS linting tool"
__bw_complete "$commands" "formatters" "run multiple formatter tools" __bw_complete "$commands" "formatters" "run multiple formatter tools"
__bw_complete "$commands" "populate_streams" "populate the main streams" __bw_complete "$commands" "populate_streams" "populate the main streams"

View file

@ -19,6 +19,7 @@ build
clean clean
black black
prettier prettier
eslint
stylelint stylelint
formatters formatters
collectstatic_watch collectstatic_watch

View file

@ -21,6 +21,7 @@ build
clean clean
black black
prettier prettier
eslint
stylelint stylelint
formatters formatters
collectstatic_watch collectstatic_watch

View file

@ -86,10 +86,8 @@ services:
restart: on-failure restart: on-failure
flower: flower:
build: . build: .
command: celery -A celerywyrm flower --basic_auth=${FLOWER_USER}:${FLOWER_PASSWORD} command: celery -A celerywyrm flower --basic_auth=${FLOWER_USER}:${FLOWER_PASSWORD} --url_prefix=flower
env_file: .env env_file: .env
ports:
- ${FLOWER_PORT}:${FLOWER_PORT}
volumes: volumes:
- .:/app - .:/app
networks: networks:

View file

@ -5,29 +5,75 @@ upstream web {
} }
server { server {
access_log /var/log/nginx/access.log cache_log;
listen 80; listen 80;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
#include /etc/nginx/mime.types;
#default_type application/octet-stream;
gzip on;
gzip_disable "msie6";
proxy_read_timeout 1800s;
chunked_transfer_encoding on;
# store responses to anonymous users for up to 1 minute
proxy_cache bookwyrm_cache;
proxy_cache_valid any 1m;
add_header X-Cache-Status $upstream_cache_status;
# ignore the set cookie header when deciding to
# store a response in the cache
proxy_ignore_headers Cache-Control Set-Cookie Expires;
# PUT requests always bypass the cache
# logged in sessions also do not populate the cache
# to avoid serving personal data to anonymous users
proxy_cache_methods GET HEAD;
proxy_no_cache $cookie_sessionid;
proxy_cache_bypass $cookie_sessionid;
# tell the web container the address of the outside client
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
# rate limit the login or password reset pages
location ~ ^/(login[^-/]|password-reset|resend-link|2fa-check) { location ~ ^/(login[^-/]|password-reset|resend-link|2fa-check) {
limit_req zone=loginlimit; limit_req zone=loginlimit;
proxy_pass http://web; proxy_pass http://web;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
} }
# do not log periodic polling requests from logged in users
location /api/updates/ {
access_log off;
proxy_pass http://web;
}
# forward any cache misses or bypass to the web container
location / { location / {
proxy_pass http://web; proxy_pass http://web;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
} }
location /images/ { # directly serve images and static files from the
alias /app/images/; # bookwyrm filesystem using sendfile.
# make the logs quieter by not reporting these requests
location ~ ^/(images|static)/ {
root /app;
try_files $uri =404;
add_header X-Cache-Status STATIC;
access_log off;
} }
location /static/ { # monitor the celery queues with flower, no caching enabled
alias /app/static/; location /flower/ {
proxy_pass http://flower:8888;
proxy_cache_bypass 1;
} }
} }

View file

@ -1,2 +1,22 @@
client_max_body_size 10m; client_max_body_size 10m;
limit_req_zone $binary_remote_addr zone=loginlimit:10m rate=1r/s; limit_req_zone $binary_remote_addr zone=loginlimit:10m rate=1r/s;
# include the cache status in the log message
log_format cache_log '$upstream_cache_status - '
'$remote_addr [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$upstream_response_time $request_time';
# Create a cache for responses from the web app
proxy_cache_path
/var/cache/nginx/bookwyrm_cache
keys_zone=bookwyrm_cache:20m
loader_threshold=400
loader_files=400
max_size=400m;
# use the accept header as part of the cache key
# since activitypub endpoints have both HTML and JSON
# on the same URI.
proxy_cache_key $scheme$proxy_host$uri$is_args$args$http_accept;