diff --git a/README.md b/README.md index 6487137c2..10f77c042 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ Social reading and reviewing, decentralized with ActivityPub ## Joining BookWyrm BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://github.com/mouse-reeve/bookwyrm/blob/main/instances.md) list. -You can request an invite to https://bookwyrm.social by [email](mailto:mousereeve@riseup.net), [Mastodon direct message](https://friend.camp/@tripofmice), or [Twitter direct message](https://twitter.com/tripofmice). - - ## Contributing There are many ways you can contribute to this project, regardless of your level of technical expertise. diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index c7536876d..47ac59df5 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -1,27 +1,64 @@ """ send emails """ -from django.core.mail import send_mail +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template from bookwyrm import models from bookwyrm.tasks import app +from bookwyrm.settings import DOMAIN + + +def email_data(): + """ fields every email needs """ + site = models.SiteSettings.objects.get() + if site.logo_small: + logo_path = "/images/{}".format(site.logo_small.url) + else: + logo_path = "/static/images/logo-small.png" + + return { + "site_name": site.name, + "logo": logo_path, + "domain": DOMAIN, + "user": None, + } + + +def invite_email(invite_request): + """ send out an invite code """ + data = email_data() + data["invite_link"] = invite_request.invite.link + send_email.delay(invite_request.email, *format_email("invite", data)) def password_reset_email(reset_code): """ generate a password reset email """ - site = models.SiteSettings.get() - send_email.delay( - reset_code.user.email, - "Reset your password on %s" % site.name, - "Your password reset link: %s" % reset_code.link, + data = email_data() + data["reset_link"] = reset_code.link + data["user"] = reset_code.user.display_name + send_email.delay(reset_code.user.email, *format_email("password_reset", data)) + + +def format_email(email_name, data): + """ render the email templates """ + subject = ( + get_template("email/{}/subject.html".format(email_name)).render(data).strip() ) + html_content = ( + get_template("email/{}/html_content.html".format(email_name)) + .render(data) + .strip() + ) + text_content = ( + get_template("email/{}/text_content.html".format(email_name)) + .render(data) + .strip() + ) + return (subject, html_content, text_content) @app.task -def send_email(recipient, subject, message): +def send_email(recipient, subject, html_content, text_content): """ use a task to send the email """ - send_mail( - subject, - message, - None, # sender will be the config default - [recipient], - fail_silently=False, - ) + email = EmailMultiAlternatives(subject, text_content, None, [recipient]) + email.attach_alternative(html_content, "text/html") + email.send() diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index d723ebdbf..d330211c1 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -3,6 +3,7 @@ import datetime from collections import defaultdict from django import forms +from django.core.exceptions import ValidationError from django.forms import ModelForm, PasswordInput, widgets from django.forms.widgets import Textarea from django.utils import timezone @@ -202,6 +203,19 @@ class ExpiryWidget(widgets.Select): return timezone.now() + interval +class InviteRequestForm(CustomForm): + def clean(self): + """ make sure the email isn't in use by a registered user """ + cleaned_data = super().clean() + email = cleaned_data.get("email") + if email and models.User.objects.filter(email=email).exists(): + self.add_error("email", _("A user with this email already exists.")) + + class Meta: + model = models.InviteRequest + fields = ["email"] + + class CreateInviteForm(CustomForm): class Meta: model = models.SiteInvite diff --git a/bookwyrm/migrations/0056_auto_20210321_0303.py b/bookwyrm/migrations/0056_auto_20210321_0303.py new file mode 100644 index 000000000..aa475e033 --- /dev/null +++ b/bookwyrm/migrations/0056_auto_20210321_0303.py @@ -0,0 +1,59 @@ +# Generated by Django 3.1.6 on 2021-03-21 03:03 + +import bookwyrm.models.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0055_auto_20210321_0101"), + ] + + operations = [ + migrations.AddField( + model_name="sitesettings", + name="allow_invite_requests", + field=models.BooleanField(default=True), + ), + migrations.CreateModel( + name="InviteRequest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("email", models.EmailField(max_length=255, unique=True)), + ("invite_sent", models.BooleanField(default=False)), + ("ignored", models.BooleanField(default=False)), + ( + "invite", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="bookwyrm.siteinvite", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 326a673e1..35e32c2cf 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -26,7 +26,7 @@ from .federated_server import FederatedServer from .import_job import ImportJob, ImportItem -from .site import SiteSettings, SiteInvite, PasswordReset +from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) activity_models = { diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 7fde6781e..2c8d25539 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -3,10 +3,11 @@ import base64 import datetime from Crypto import Random -from django.db import models +from django.db import models, IntegrityError from django.utils import timezone from bookwyrm.settings import DOMAIN +from .base_model import BookWyrmModel from .user import User @@ -24,6 +25,7 @@ class SiteSettings(models.Model): code_of_conduct = models.TextField(default="Add a code of conduct here.") privacy_policy = models.TextField(default="Add a privacy policy here.") allow_registration = models.BooleanField(default=True) + allow_invite_requests = models.BooleanField(default=True) logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) favicon = models.ImageField(upload_to="logos/", null=True, blank=True) @@ -69,6 +71,23 @@ class SiteInvite(models.Model): return "https://{}/invite/{}".format(DOMAIN, self.code) +class InviteRequest(BookWyrmModel): + """ prospective users can request an invite """ + + email = models.EmailField(max_length=255, unique=True) + invite = models.ForeignKey( + SiteInvite, on_delete=models.SET_NULL, null=True, blank=True + ) + invite_sent = models.BooleanField(default=False) + ignored = models.BooleanField(default=False) + + def save(self, *args, **kwargs): + """ don't create a request for a registered email """ + if User.objects.filter(email=self.email).exists(): + raise IntegrityError() + super().save(*args, **kwargs) + + def get_passowrd_reset_expiry(): """ give people a limited time to use the link """ now = timezone.now() diff --git a/bookwyrm/static/images/logo-small.png b/bookwyrm/static/images/logo-small.png index 10ea7a38f..72f49ef78 100644 Binary files a/bookwyrm/static/images/logo-small.png and b/bookwyrm/static/images/logo-small.png differ diff --git a/bookwyrm/templates/discover/landing_layout.html b/bookwyrm/templates/discover/landing_layout.html index 5cfa1fd39..8e507531e 100644 --- a/bookwyrm/templates/discover/landing_layout.html +++ b/bookwyrm/templates/discover/landing_layout.html @@ -45,9 +45,33 @@
{% include 'snippets/register_form.html' %}
+ {% else %} +

{% trans "This instance is closed" %}

{{ site.registration_closed_text | safe}}

+ + {% if site.allow_invite_requests %} + {% if request_received %} +

+ {% trans "Thank you! Your request has been received." %} +

+ {% else %} +

{% trans "Request an Invitation" %}

+
+ {% csrf_token %} +
+ + + {% for error in request_form.email.errors %} +

{{ error | escape }}

+ {% endfor %} +
+ +
+ {% endif %} + {% endif %} + {% endif %} {% else %} diff --git a/bookwyrm/templates/email/html_layout.html b/bookwyrm/templates/email/html_layout.html new file mode 100644 index 000000000..02527ff52 --- /dev/null +++ b/bookwyrm/templates/email/html_layout.html @@ -0,0 +1,26 @@ +{% load i18n %} +
+
+
+ logo +
+
+ {{ site_name }}
+ {{ domain }}
+
+
+ +
+

+ {% if user %}{{ user }},{% else %}{% trans "Hi there," %}{% endif %} +

+ {% block content %}{% endblock %} +
+ +
+

{% blocktrans %}BookWyrm hosted on {{ site_name }}{% endblocktrans %}

+ {% if user %} +

{% trans "Email preference" %}

+ {% endif %} +
+
diff --git a/bookwyrm/templates/email/invite/html_content.html b/bookwyrm/templates/email/invite/html_content.html new file mode 100644 index 000000000..358e23dc1 --- /dev/null +++ b/bookwyrm/templates/email/invite/html_content.html @@ -0,0 +1,17 @@ +{% extends 'email/html_layout.html' %} +{% load i18n %} + +{% block content %} +

+ {% blocktrans %}You're invited to join {{ site_name }}!{% endblocktrans %} +

+ +{% trans "Join Now" as text %} +{% include 'email/snippets/action.html' with path=invite_link text=text %} + +

+ {% url 'code-of-conduct' as coc_path %} + {% url 'about' as about_path %} + {% blocktrans %}Learn more about this instance.{% endblocktrans %} +

+{% endblock %} diff --git a/bookwyrm/templates/email/invite/subject.html b/bookwyrm/templates/email/invite/subject.html new file mode 100644 index 000000000..efb8be5ae --- /dev/null +++ b/bookwyrm/templates/email/invite/subject.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}You're invited to join {{ site_name }}!{% endblocktrans %} diff --git a/bookwyrm/templates/email/invite/text_content.html b/bookwyrm/templates/email/invite/text_content.html new file mode 100644 index 000000000..c3fcdc04e --- /dev/null +++ b/bookwyrm/templates/email/invite/text_content.html @@ -0,0 +1,10 @@ +{% extends 'email/text_layout.html' %} +{% load i18n %} +{% block content %} +{% blocktrans %}You're invited to join {{ site_name }}! Click the link below to create an account.{% endblocktrans %} + +{{ invite_link }} + +{% trans "Learn more about this instance:" %} https://{{ domain }}{% url 'about' %} + +{% endblock %} diff --git a/bookwyrm/templates/email/password_reset/html_content.html b/bookwyrm/templates/email/password_reset/html_content.html new file mode 100644 index 000000000..eef0e5e59 --- /dev/null +++ b/bookwyrm/templates/email/password_reset/html_content.html @@ -0,0 +1,15 @@ +{% extends 'email/html_layout.html' %} +{% load i18n %} + +{% block content %} +

+ {% blocktrans %}You requested to reset your {{ site_name }} password. Click the link below to set a new password and log in to your account.{% endblocktrans %} +

+ +{% trans "Reset Password" as text %} +{% include 'email/snippets/action.html' with path=reset_link text=text %} + +

+ {% trans "If you didn't request to reset your password, you can ignore this email." %} +

+{% endblock %} diff --git a/bookwyrm/templates/email/password_reset/subject.html b/bookwyrm/templates/email/password_reset/subject.html new file mode 100644 index 000000000..886801402 --- /dev/null +++ b/bookwyrm/templates/email/password_reset/subject.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Reset your {{ site_name }} password{% endblocktrans %} diff --git a/bookwyrm/templates/email/password_reset/text_content.html b/bookwyrm/templates/email/password_reset/text_content.html new file mode 100644 index 000000000..b5cf754ef --- /dev/null +++ b/bookwyrm/templates/email/password_reset/text_content.html @@ -0,0 +1,9 @@ +{% extends 'email/text_layout.html' %} +{% load i18n %} +{% block content %} +{% blocktrans %}You requested to reset your {{ site_name }} password. Click the link below to set a new password and log in to your account.{% endblocktrans %} + +{{ reset_link }} + +{% trans "If you didn't request to reset your password, you can ignore this email." %} +{% endblock %} diff --git a/bookwyrm/templates/email/preview.html b/bookwyrm/templates/email/preview.html new file mode 100644 index 000000000..66d856c08 --- /dev/null +++ b/bookwyrm/templates/email/preview.html @@ -0,0 +1,19 @@ + + +
+ Subject: {% include subject_path %} +
+
+ + Html email: +
+ {% include html_content_path %} +
+
+ + Text email: +
+ {% include text_content_path %} +
+ + diff --git a/bookwyrm/templates/email/snippets/action.html b/bookwyrm/templates/email/snippets/action.html new file mode 100644 index 000000000..56feb9efd --- /dev/null +++ b/bookwyrm/templates/email/snippets/action.html @@ -0,0 +1,5 @@ +

+ + {{ text }} + +

diff --git a/bookwyrm/templates/email/text_layout.html b/bookwyrm/templates/email/text_layout.html new file mode 100644 index 000000000..cd0444f16 --- /dev/null +++ b/bookwyrm/templates/email/text_layout.html @@ -0,0 +1,3 @@ +{% load i18n %} +{% if user %}{{ user.display_name }},{% else %}{% trans "Hi there," %}{% endif %} +{% block content %}{% endblock %} diff --git a/bookwyrm/templates/password_reset_request.html b/bookwyrm/templates/password_reset_request.html index 97191afa4..5d877442f 100644 --- a/bookwyrm/templates/password_reset_request.html +++ b/bookwyrm/templates/password_reset_request.html @@ -8,7 +8,9 @@

{% trans "Reset Password" %}

- {% if message %}

{{ message }}

{% endif %} + + {% if message %}

{{ message }}

{% endif %} +

{% trans "A link to reset your password will be sent to your email address" %}

{% csrf_token %} @@ -16,6 +18,9 @@
+ {% if error %} +

{{ error }}

+ {% endif %}
diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html index a34fe6389..cfc7a92a8 100644 --- a/bookwyrm/templates/settings/admin_layout.html +++ b/bookwyrm/templates/settings/admin_layout.html @@ -15,8 +15,9 @@
+
+
{{ site_form.registration_closed_text }} diff --git a/bookwyrm/tests/test_emailing.py b/bookwyrm/tests/test_emailing.py new file mode 100644 index 000000000..5d7d4894b --- /dev/null +++ b/bookwyrm/tests/test_emailing.py @@ -0,0 +1,51 @@ +""" test creating emails """ +from unittest.mock import patch + +from django.test import TestCase +from django.test.client import RequestFactory +import responses + +from bookwyrm import emailing, models + + +@patch("bookwyrm.emailing.send_email.delay") +class Emailing(TestCase): + """ every response to a get request, html or json """ + + def setUp(self): + """ we need basic test data and mocks """ + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.mouse", + "password", + local=True, + localname="mouse", + ) + models.SiteSettings.objects.create() + + def test_invite_email(self, email_mock): + """ load the invite email """ + invite_request = models.InviteRequest.objects.create( + email="test@email.com", + invite=models.SiteInvite.objects.create(user=self.local_user), + ) + + emailing.invite_email(invite_request) + + self.assertEqual(email_mock.call_count, 1) + args = email_mock.call_args[0] + self.assertEqual(args[0], "test@email.com") + self.assertEqual(args[1], "You're invited to join BookWyrm!") + self.assertEqual(len(args), 4) + + def test_password_reset_email(self, email_mock): + """ load the password reset email """ + reset = models.PasswordReset.objects.create(user=self.local_user) + emailing.password_reset_email(reset) + + self.assertEqual(email_mock.call_count, 1) + args = email_mock.call_args[0] + self.assertEqual(args[0], "mouse@mouse.mouse") + self.assertEqual(args[1], "Reset your BookWyrm password") + self.assertEqual(len(args), 4) diff --git a/bookwyrm/tests/test_goodreads_import.py b/bookwyrm/tests/test_goodreads_import.py index 080ccd15b..a62cfdd28 100644 --- a/bookwyrm/tests/test_goodreads_import.py +++ b/bookwyrm/tests/test_goodreads_import.py @@ -9,7 +9,6 @@ import responses from bookwyrm import models, importer from bookwyrm.goodreads_import import GoodreadsImporter -from bookwyrm import importer from bookwyrm.settings import DOMAIN @@ -17,8 +16,8 @@ class GoodreadsImport(TestCase): """ importing from goodreads csv """ def setUp(self): - self.importer = GoodreadsImporter() """ use a test csv """ + self.importer = GoodreadsImporter() datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv") self.csv = open(datafile, "r", encoding=self.importer.encoding) self.user = models.User.objects.create_user( diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index cd2276c0d..7bfc8fe53 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -6,7 +6,7 @@ from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models +from bookwyrm import forms, models from bookwyrm import views @@ -50,3 +50,81 @@ class InviteViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_invite_request(self): + """ request to join a server """ + form = forms.InviteRequestForm() + form.data["email"] = "new@user.email" + + view = views.InviteRequest.as_view() + request = self.factory.post("", form.data) + + result = view(request) + result.render() + + req = models.InviteRequest.objects.get() + self.assertEqual(req.email, "new@user.email") + + def test_invite_request_email_taken(self): + """ request to join a server with an existing user email """ + form = forms.InviteRequestForm() + form.data["email"] = "mouse@mouse.mouse" + + view = views.InviteRequest.as_view() + request = self.factory.post("", form.data) + + result = view(request) + result.render() + + # no request created + self.assertFalse(models.InviteRequest.objects.exists()) + + def test_manage_invite_requests(self): + """ there are so many views, this just makes sure it LOADS """ + view = views.ManageInviteRequests.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + result = view(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + # now with data + models.InviteRequest.objects.create(email="fish@example.com") + result = view(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_manage_invite_requests_send(self): + """ send an invite """ + req = models.InviteRequest.objects.create(email="fish@example.com") + + view = views.ManageInviteRequests.as_view() + request = self.factory.post("", {"invite-request": req.id}) + request.user = self.local_user + request.user.is_superuser = True + + with patch("bookwyrm.emailing.send_email.delay") as mock: + view(request) + self.assertEqual(mock.call_count, 1) + req.refresh_from_db() + self.assertIsNotNone(req.invite) + + def test_ignore_invite_request(self): + """ don't invite that jerk """ + req = models.InviteRequest.objects.create(email="fish@example.com") + + view = views.ignore_invite_request + request = self.factory.post("", {"invite-request": req.id}) + request.user = self.local_user + request.user.is_superuser = True + + view(request) + req.refresh_from_db() + self.assertTrue(req.ignored) + + view(request) + req.refresh_from_db() + self.assertFalse(req.ignored) diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index f67f5538f..53a9bcbdc 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -42,7 +42,8 @@ class PasswordViews(TestCase): request = self.factory.post("", {"email": "aa@bb.ccc"}) view = views.PasswordResetRequest.as_view() resp = view(request) - self.assertEqual(resp.status_code, 302) + self.assertEqual(resp.status_code, 200) + resp.render() request = self.factory.post("", {"email": "mouse@mouse.com"}) with patch("bookwyrm.emailing.send_email.delay"): diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index c24b83f70..4aced6fe6 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -48,12 +48,30 @@ urlpatterns = [ ), # admin re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"), + re_path( + r"^settings/email-preview", + views.site.email_preview, + name="settings-email-preview", + ), re_path( r"^settings/federation", views.Federation.as_view(), name="settings-federation" ), re_path( r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" ), + re_path( + r"^settings/requests/?$", + views.ManageInviteRequests.as_view(), + name="settings-invite-requests", + ), + re_path( + r"^settings/requests/ignore?$", + views.ignore_invite_request, + name="settings-invite-requests-ignore", + ), + re_path( + r"^invite-request/?$", views.InviteRequest.as_view(), name="invite-request" + ), re_path(r"^invite/(?P[A-Za-z0-9]+)/?$", views.Invite.as_view()), # moderation re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"), @@ -74,8 +92,8 @@ urlpatterns = [ ), re_path(r"^report/?$", views.make_report, name="report"), # landing pages - re_path(r"^about/?$", views.About.as_view()), - path("", views.Home.as_view()), + re_path(r"^about/?$", views.About.as_view(), name="about"), + path("", views.Home.as_view(), name="landing"), re_path(r"^discover/?$", views.Discover.as_view()), re_path(r"^notifications/?$", views.Notifications.as_view()), # feeds diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index e98cb24af..3402e65f7 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -13,7 +13,8 @@ from .goal import Goal, hide_goal from .import_data import Import, ImportStatus from .inbox import Inbox from .interaction import Favorite, Unfavorite, Boost, Unboost -from .invite import ManageInvites, Invite +from .invite import ManageInvites, Invite, InviteRequest +from .invite import ManageInviteRequests, ignore_invite_request from .isbn import Isbn from .landing import About, Home, Discover from .list import Lists, List, Curate, UserLists diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 20332cbda..c62856bab 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -2,7 +2,7 @@ import re from requests import HTTPError from django.core.exceptions import FieldError -from django.db.models import Q +from django.db.models import Max, Q from bookwyrm import activitypub, models from bookwyrm.connectors import ConnectorException, get_data @@ -216,3 +216,20 @@ def is_blocked(viewer, user): if viewer.is_authenticated and viewer in user.blocks.all(): return True return False + + +def get_discover_books(): + """ list of books for the discover page """ + return list( + set( + models.Edition.objects.filter( + review__published_date__isnull=False, + review__deleted=False, + review__user__local=True, + review__privacy__in=["public", "unlisted"], + ) + .exclude(cover__exact="") + .annotate(Max("review__published_date")) + .order_by("-review__published_date__max")[:6] + ) + ) diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index 1f1ccaf11..e4b98cba2 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -6,9 +6,11 @@ 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 bookwyrm import forms, models +from bookwyrm import emailing, forms, models from bookwyrm.settings import PAGE_LENGTH +from . import helpers # pylint: disable= no-self-use @@ -77,3 +79,74 @@ class Invite(View): return TemplateResponse(request, "invite.html", data) # post handling is in views.authentication.Register + + +class ManageInviteRequests(View): + """ grant invites like the benevolent lord you are """ + + def get(self, request): + """ view a list of requests """ + ignored = request.GET.get("ignored", False) + try: + page = int(request.GET.get("page", 1)) + except ValueError: + page = 1 + + paginated = Paginator( + models.InviteRequest.objects.filter(ignored=ignored).order_by( + "-created_date" + ), + PAGE_LENGTH, + ) + + data = { + "ignored": ignored, + "requests": paginated.page(page), + } + return TemplateResponse(request, "settings/manage_invite_requests.html", data) + + def post(self, request): + """ send out an invite """ + invite_request = get_object_or_404( + models.InviteRequest, id=request.POST.get("invite-request") + ) + # allows re-sending invites + invite_request.invite, _ = models.SiteInvite.objects.get_or_create( + use_limit=1, + user=request.user, + ) + + invite_request.save() + emailing.invite_email(invite_request) + return redirect("settings-invite-requests") + + +class InviteRequest(View): + """ prospective users sign up here """ + + def post(self, request): + """ create a request """ + form = forms.InviteRequestForm(request.POST) + received = False + if form.is_valid(): + received = True + form.save() + + data = { + "request_form": form, + "request_received": received, + "books": helpers.get_discover_books(), + } + return TemplateResponse(request, "discover/discover.html", data) + + +@require_POST +def ignore_invite_request(request): + """ hide an invite request """ + invite_request = get_object_or_404( + models.InviteRequest, id=request.POST.get("invite-request") + ) + + invite_request.ignored = not invite_request.ignored + invite_request.save() + return redirect("settings-invite-requests") diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py index 2c4a51478..86f7d3e9f 100644 --- a/bookwyrm/views/landing.py +++ b/bookwyrm/views/landing.py @@ -1,10 +1,10 @@ """ non-interactive pages """ -from django.db.models import Max from django.template.response import TemplateResponse from django.views import View from bookwyrm import forms, models from .feed import Feed +from . import helpers # pylint: disable= no-self-use @@ -33,20 +33,9 @@ class Discover(View): def get(self, request): """ tiled book activity page """ - books = ( - models.Edition.objects.filter( - review__published_date__isnull=False, - review__deleted=False, - review__user__local=True, - review__privacy__in=["public", "unlisted"], - ) - .exclude(cover__exact="") - .annotate(Max("review__published_date")) - .order_by("-review__published_date__max")[:6] - ) - data = { "register_form": forms.RegisterForm(), - "books": list(set(books)), + "request_form": forms.InviteRequestForm(), + "books": helpers.get_discover_books(), } return TemplateResponse(request, "discover/discover.html", data) diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index e853d16bf..2926b9d76 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -5,6 +5,7 @@ from django.core.exceptions import PermissionDenied from django.shortcuts import redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator +from django.utils.translation import gettext as _ from django.views import View from bookwyrm import models @@ -28,7 +29,8 @@ class PasswordResetRequest(View): try: user = models.User.objects.get(email=email) except models.User.DoesNotExist: - return redirect("/password-reset") + data = {"error": _("No user with that email address was found.")} + return TemplateResponse(request, "password_reset_request.html", data) # remove any existing password reset cods for this user models.PasswordReset.objects.filter(user=user).all().delete() @@ -36,7 +38,7 @@ class PasswordResetRequest(View): # create a new reset code code = models.PasswordReset.objects.create(user=user) password_reset_email(code) - data = {"message": "Password reset link sent to %s" % email} + data = {"message": _("A password reset link sent to %s" % email)} return TemplateResponse(request, "password_reset_request.html", data) diff --git a/bookwyrm/views/site.py b/bookwyrm/views/site.py index ce64e6e0f..e58976607 100644 --- a/bookwyrm/views/site.py +++ b/bookwyrm/views/site.py @@ -5,7 +5,7 @@ from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View -from bookwyrm import forms, models +from bookwyrm import emailing, forms, models # pylint: disable= no-self-use @@ -33,3 +33,17 @@ class Site(View): form.save() return redirect("settings-site") + + +@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"] = "email/{}/subject.html".format(template) + data["html_content_path"] = "email/{}/html_content.html".format(template) + data["text_content_path"] = "email/{}/text_content.html".format(template) + data["reset_link"] = "https://example.com/link" + data["invite_link"] = "https://example.com/link" + return TemplateResponse(request, "email/preview.html", data)