Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2022-03-24 09:46:32 -07:00
commit 588d2da2c6
29 changed files with 258 additions and 158 deletions

View file

@ -4,6 +4,7 @@ from django import forms
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
from .widgets import ArrayWidget, SelectDateWidget, Select
# pylint: disable=missing-class-docstring
@ -14,14 +15,6 @@ class CoverForm(CustomForm):
help_texts = {f: None for f in fields}
class ArrayWidget(forms.widgets.TextInput):
# pylint: disable=unused-argument
# pylint: disable=no-self-use
def value_from_datadict(self, data, files, name):
"""get all values for this name"""
return [i for i in data.getlist(name) if i]
class EditionForm(CustomForm):
class Meta:
model = models.Edition
@ -56,16 +49,16 @@ class EditionForm(CustomForm):
"publishers": forms.TextInput(
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
),
"first_published_date": forms.SelectDateWidget(
"first_published_date": SelectDateWidget(
attrs={"aria-describedby": "desc_first_published_date"}
),
"published_date": forms.SelectDateWidget(
"published_date": SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"physical_format": forms.Select(
"physical_format": Select(
attrs={"aria-describedby": "desc_physical_format"}
),
"physical_format_detail": forms.TextInput(

70
bookwyrm/forms/widgets.py Normal file
View file

@ -0,0 +1,70 @@
""" using django model forms """
from django import forms
class ArrayWidget(forms.widgets.TextInput):
"""Inputs for postgres array fields"""
# pylint: disable=unused-argument
# pylint: disable=no-self-use
def value_from_datadict(self, data, files, name):
"""get all values for this name"""
return [i for i in data.getlist(name) if i]
class Select(forms.Select):
"""custom template for select widget"""
template_name = "widgets/select.html"
class SelectDateWidget(forms.SelectDateWidget):
"""
A widget that splits date input into two <select> boxes and a numerical year.
"""
template_name = "widgets/addon_multiwidget.html"
select_widget = Select
def get_context(self, name, value, attrs):
"""sets individual widgets"""
context = super().get_context(name, value, attrs)
date_context = {}
year_name = self.year_field % name
date_context["year"] = forms.NumberInput().get_context(
name=year_name,
value=context["widget"]["value"]["year"],
attrs={
**context["widget"]["attrs"],
"id": f"id_{year_name}",
"class": "input",
},
)
month_choices = list(self.months.items())
if not self.is_required:
month_choices.insert(0, self.month_none_value)
month_name = self.month_field % name
date_context["month"] = self.select_widget(
attrs, choices=month_choices
).get_context(
name=month_name,
value=context["widget"]["value"]["month"],
attrs={**context["widget"]["attrs"], "id": f"id_{month_name}"},
)
day_choices = [(i, i) for i in range(1, 32)]
if not self.is_required:
day_choices.insert(0, self.day_none_value)
day_name = self.day_field % name
date_context["day"] = self.select_widget(
attrs,
choices=day_choices,
).get_context(
name=day_name,
value=context["widget"]["value"]["day"],
attrs={**context["widget"]["attrs"], "id": f"id_{day_name}"},
)
subwidgets = []
for field in self._parse_date_fmt():
subwidgets.append(date_context[field]["widget"])
context["widget"]["subwidgets"] = subwidgets
return context

View file

@ -129,14 +129,6 @@ button:focus-visible .button-invisible-overlay {
}
/** Tooltips
******************************************************************************/
.tooltip {
width: 100%;
}
/** States
******************************************************************************/

View file

@ -155,8 +155,7 @@
<label class="label" for="id_first_published_date">
{% trans "First published date:" %}
</label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
{{ form.first_published_date }}
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
</div>
@ -164,7 +163,7 @@
<label class="label" for="id_published_date">
{% trans "Published date:" %}
</label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
{{ form.published_date }}
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
</div>
@ -259,9 +258,7 @@
<label class="label" for="id_physical_format">
{% trans "Format:" %}
</label>
<div class="select">
{{ form.physical_format }}
</div>
{{ form.physical_format }}
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
</div>

View file

@ -1,11 +0,0 @@
{% load i18n %}
{% trans "Help" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text class="ml-3 is-rounded is-small has-background-body p-0 pb-1" icon="question-circle is-size-6" controls_text=controls_text controls_uid=controls_uid %}
<aside class="tooltip notification is-hidden transition-y is-pulled-left mb-2" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
{% trans "Close" as button_text %}
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
{% block tooltip_content %}{% endblock %}
</aside>

View file

@ -29,9 +29,16 @@
</section>
<section class="block">
{% trans "Can't find your code?" as button_text %}
{% include "snippets/toggle/open_button.html" with text=button_text controls_text="resend_form" focus="resend_form_header" %}
{% include "confirm_email/resend_form.html" with controls_text="resend_form" %}
<form name="fallback" method="GET" action="{% url 'resend-link' %}" autocomplete="off">
<button
type="submit"
class="button"
data-modal-open="resend_form"
>
{% trans "Can't find your code?" %}
</button>
</form>
{% include "confirm_email/resend_modal.html" with id="resend_form" %}
</section>
</div>
</div>

View file

@ -0,0 +1,10 @@
{% extends 'landing/layout.html' %}
{% load i18n %}
{% block title %}
{% trans "Resend confirmation link" %}
{% endblock %}
{% block content %}
{% include "confirm_email/resend_modal.html" with active=True static=True id="resend-modal" %}
{% endblock %}

View file

@ -1,20 +0,0 @@
{% extends "components/inline_form.html" %}
{% load i18n %}
{% block header %}
{% trans "Resend confirmation link" %}
{% endblock %}
{% block form %}
<form name="resend" method="post" action="{% url 'resend-link' %}">
{% csrf_token %}
<div class="field">
<label class="label" for="email">{% trans "Email address:" %}</label>
<div class="control">
<input type="text" name="email" class="input" required id="email">
</div>
</div>
<div class="control">
<button class="button is-link">{% trans "Resend link" %}</button>
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,44 @@
{% extends "components/modal.html" %}
{% load i18n %}
{% block modal-title %}
{% trans "Resend confirmation link" %}
{% endblock %}
{% block modal-form-open %}
<form name="resend" method="post" action="{% url 'resend-link' %}">
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<div class="field">
<label class="label" for="email">{% trans "Email address:" %}</label>
<div class="control">
<input
type="email"
name="email"
class="input"
id="email"
aria-described-by="id_email_errors"
required
>
{% if error %}
<div id="id_email_errors">
<p class="help is-danger">
{% trans "No user matching this email address found." %}
</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block modal-footer %}
<div class="control">
<button class="button is-link">{% trans "Resend link" %}</button>
</div>
{% endblock %}
{% block modal-form-close %}
</form>
{% endblock %}

View file

@ -5,7 +5,19 @@
<section class="block">
<h2 class="title is-4">{% trans "Your Books" %}</h2>
{% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
<div class="content">
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
<div class="box has-background-link-light">
<p>{% trans "Do you have book data from another service like GoodReads?" %}</p>
<a href="{% url 'import' %}">
<span class="icon icon-list" aria-hidden="true"></span>
{% trans "Import your reading history" %}
</a>
</div>
</div>
{% else %}
{% with active_book=request.GET.book %}
<div class="tab-group">

View file

@ -14,28 +14,32 @@
<div class="column is-half">
<div class="field">
<label class="label is-pulled-left" for="source">
<label class="label" for="source">
{% trans "Data source:" %}
</label>
{% include 'import/tooltip.html' with controls_text="goodreads-tooltip" %}
<div class="select">
<select name="source" id="source" aria-describedby="desc_source">
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
Goodreads (CSV)
</option>
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
Storygraph (CSV)
</option>
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
LibraryThing (TSV)
</option>
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
OpenLibrary (CSV)
</option>
</select>
</div>
<p class="help" id="desc_source">
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
</p>
</div>
<div class="select block">
<select name="source" id="source">
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
Goodreads (CSV)
</option>
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
Storygraph (CSV)
</option>
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
LibraryThing (TSV)
</option>
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
OpenLibrary (CSV)
</option>
</select>
</div>
<div class="field">
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
{{ import_form.csv_file }}
@ -63,7 +67,7 @@
<div class="content block">
<h2 class="title">{% trans "Recent Imports" %}</h2>
{% if not jobs %}
<p>{% trans "No recent imports" %}</p>
<p><em>{% trans "No recent imports" %}</em></p>
{% endif %}
<ul>
{% for job in jobs %}

View file

@ -1,8 +0,0 @@
{% extends 'components/tooltip.html' %}
{% load i18n %}
{% block tooltip_content %}
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
{% endblock %}

View file

@ -14,7 +14,7 @@
{% block panel %}
<div class="block table-container">
<table class="table is-striped">
<table class="table is-striped is-fullwidth">
<tr>
<th>
{% url 'settings-announcements' as url %}

View file

@ -154,7 +154,7 @@
</summary>
<div class="table-container">
<table class="table is-striped">
<table class="table is-striped is-fullwidth">
<tr>
<th>
<label for="id_string_match">{% trans "String match" %}</label>

View file

@ -10,26 +10,26 @@
{% block panel %}
<div class="columns block has-text-centered is-mobile is-multiline">
<div class="column is-3-desktop is-6-mobile">
<div class="notification">
<div class="column is-3-desktop is-6-mobile is-flex">
<div class="notification is-flex-grow-1">
<h3>{% trans "Total users" %}</h3>
<p class="title is-5">{{ users|intcomma }}</p>
</div>
</div>
<div class="column is-3-desktop is-6-mobile">
<div class="notification">
<div class="column is-3-desktop is-6-mobil is-flexe">
<div class="notification is-flex-grow-1">
<h3>{% trans "Active this month" %}</h3>
<p class="title is-5">{{ active_users|intcomma }}</p>
</div>
</div>
<div class="column is-3-desktop is-6-mobile">
<div class="notification">
<div class="column is-3-desktop is-6-mobile is-flex">
<div class="notification is-flex-grow-1">
<h3>{% trans "Statuses" %}</h3>
<p class="title is-5">{{ statuses|intcomma }}</p>
</div>
</div>
<div class="column is-3-desktop is-6-mobile">
<div class="notification">
<div class="column is-3-desktop is-6-mobile is-flex">
<div class="notification is-flex-grow-1">
<h3>{% trans "Works" %}</h3>
<p class="title is-5">{{ works|intcomma }}</p>
</div>
@ -38,8 +38,8 @@
<div class="columns block is-multiline">
{% if reports %}
<div class="column">
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
<div class="column is-flex">
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block is-flex-grow-1">
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
{{ display_count }} open report
{% plural %}
@ -50,8 +50,8 @@
{% endif %}
{% if pending_domains %}
<div class="column">
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
<div class="column is-flex">
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block is-flex-grow-1">
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
{{ display_count }} domain needs review
{% plural %}
@ -62,8 +62,8 @@
{% endif %}
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
<div class="column">
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success">
<div class="column is-flex">
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-flex-grow-1">
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
{{ display_count }} invite request
{% plural %}
@ -74,8 +74,8 @@
{% endif %}
{% if current_version %}
<div class="column">
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning" target="_blank">
<div class="column is-flex">
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning is-flex-grow-1" target="_blank">
{% blocktrans trimmed with current=current_version available=available_version %}
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
{% endblocktrans %}

View file

@ -25,7 +25,7 @@
</ul>
</div>
<table class="table is-striped">
<table class="table is-striped is-fullwidth">
<tr>
{% url 'settings-federation' as url %}
<th>

View file

@ -21,6 +21,7 @@
<div class="field">
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24" aria-describedby="desc_address">
<p class="help">{% trans "You can block IP ranges using CIDR syntax." %}</p>
</div>
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}

View file

@ -1,8 +0,0 @@
{% extends 'components/tooltip.html' %}
{% load i18n %}
{% block tooltip_content %}
{% trans "You can block IP ranges using CIDR syntax." %}
{% endblock %}

View file

@ -93,7 +93,7 @@
</ul>
{% endif %}
</nav>
<div class="column">
<div class="column is-clipped">
{% block panel %}{% endblock %}
</div>
</div>

View file

@ -139,6 +139,13 @@
{% trans "Allow registration" %}
</label>
</div>
<div class="field">
<label class="label mb-0" for="id_require_confirm_email">
{{ site_form.require_confirm_email }}
{% trans "Require users to confirm email address" %}
</label>
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
</div>
<div class="field">
<label class="label" for="id_allow_invite_requests">
{{ site_form.allow_invite_requests }}
@ -157,13 +164,6 @@
{{ site_form.invite_question_text }}
</label>
</div>
<div class="field">
<label class="label mb-0" for="id_require_confirm_email">
{{ site_form.require_confirm_email }}
{% trans "Require users to confirm email address" %}
</label>
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
</div>
<div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ site_form.registration_closed_text }}
@ -171,7 +171,7 @@
<div class="field">
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{{ site_form.invite_request_text }}
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
</div>
</div>

View file

@ -88,7 +88,7 @@
<section class="block content">
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
<div class="table-container">
<table class="table is-striped">
<table class="table is-striped is-fullwidth">
<tr>
<th>
{% trans "Theme name" %}

View file

@ -33,7 +33,7 @@
</div>
<div class="table-container block">
<table class="table is-striped">
<table class="table is-striped is-fullwidth">
<tr>
{% url 'settings-users' as url %}
<th>

View file

@ -0,0 +1,9 @@
{% spaceless %}
<div class="field has-addons">
{% for widget in widget.subwidgets %}
<div class="control{% if forloop.last %} is-expanded{% endif %}">
{% include widget.template_name %}
</div>
{% endfor %}
</div>
{% endspaceless %}

View file

@ -0,0 +1,10 @@
<div class="select">
<select
name="{{ widget.name }}"
{% include "django/forms/widgets/attrs.html" %}
>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</select>
</div>

View file

@ -360,10 +360,17 @@ class RegisterViews(TestCase):
result = view(request)
validate_html(result.render())
def test_resend_link(self, *_):
def test_resend_link_get(self, *_):
"""try again"""
request = self.factory.get("")
request.user = self.anonymous_user
result = views.ResendConfirmEmail.as_view()(request)
validate_html(result.render())
def test_resend_link_post(self, *_):
"""try again"""
request = self.factory.post("", {"email": "mouse@mouse.com"})
request.user = self.anonymous_user
with patch("bookwyrm.emailing.send_email.delay") as mock:
views.resend_link(request)
views.ResendConfirmEmail.as_view()(request)
self.assertEqual(mock.call_count, 1)

View file

@ -71,7 +71,7 @@ urlpatterns = [
views.ConfirmEmailCode.as_view(),
name="confirm-email-code",
),
re_path(r"^resend-link/?$", views.resend_link, name="resend-link"),
re_path(r"^resend-link/?$", views.ResendConfirmEmail.as_view(), name="resend-link"),
re_path(r"^logout/?$", views.Logout.as_view(), name="logout"),
re_path(
r"^password-reset/?$",

View file

@ -52,7 +52,8 @@ from .books.links import BookFileLinks, AddFileLink, delete_link
from .landing.about import about, privacy, conduct
from .landing.landing import Home, Landing
from .landing.login import Login, Logout
from .landing.register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
from .landing.register import Register
from .landing.register import ConfirmEmail, ConfirmEmailCode, ResendConfirmEmail
from .landing.password import PasswordResetRequest, PasswordReset
# shelves

View file

@ -1,13 +1,11 @@
""" the good stuff! the books! """
from re import sub, findall
from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector
from django.db import transaction
from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator
from django.views.decorators.http import require_POST
from django.views import View
@ -52,7 +50,6 @@ class EditBook(View):
# either of the above cases requires additional confirmation
if data.get("add_author"):
data = copy_form(data)
return TemplateResponse(request, "book/edit/edit_book.html", data)
remove_authors = request.POST.getlist("remove_authors")
@ -118,7 +115,6 @@ class CreateBook(View):
# go to confirm mode
if not parent_work_id or data.get("add_author"):
data = copy_form(data)
return TemplateResponse(request, "book/edit/edit_book.html", data)
with transaction.atomic():
@ -139,21 +135,6 @@ class CreateBook(View):
return redirect(f"/book/{book.id}")
def copy_form(data):
"""helper to re-create the date fields in the form"""
formcopy = data["form"].data.copy()
try:
formcopy["first_published_date"] = dateparse(formcopy["first_published_date"])
except (MultiValueDictKeyError, ValueError):
pass
try:
formcopy["published_date"] = dateparse(formcopy["published_date"])
except (MultiValueDictKeyError, ValueError):
pass
data["form"].data = formcopy
return data
def add_authors(request, data):
"""helper for adding authors"""
add_author = [author for author in request.POST.getlist("add_author") if author]

View file

@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters
from bookwyrm import emailing, forms, models
@ -129,12 +128,22 @@ class ConfirmEmail(View):
return ConfirmEmailCode().get(request, code)
@require_POST
def resend_link(request):
"""resend confirmation link"""
email = request.POST.get("email")
user = get_object_or_404(models.User, email=email)
emailing.email_confirmation_email(user)
return TemplateResponse(
request, "confirm_email/confirm_email.html", {"valid": True}
)
class ResendConfirmEmail(View):
"""you probably didn't get the email because celery is slow but you can try this"""
def get(self, request, error=False):
"""resend link landing page"""
return TemplateResponse(request, "confirm_email/resend.html", {"error": error})
def post(self, request):
"""resend confirmation link"""
email = request.POST.get("email")
try:
user = models.User.objects.get(email=email)
except models.User.DoesNotExist:
return self.get(request, error=True)
emailing.email_confirmation_email(user)
return TemplateResponse(
request, "confirm_email/confirm_email.html", {"valid": True}
)