mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-15 12:46:38 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
bb8cac021b
34 changed files with 661 additions and 48 deletions
|
@ -16,9 +16,6 @@ Social reading and reviewing, decentralized with ActivityPub
|
||||||
## Joining BookWyrm
|
## 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.
|
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
|
## Contributing
|
||||||
There are many ways you can contribute to this project, regardless of your level of technical expertise.
|
There are many ways you can contribute to this project, regardless of your level of technical expertise.
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,64 @@
|
||||||
""" send emails """
|
""" 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 import models
|
||||||
from bookwyrm.tasks import app
|
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):
|
def password_reset_email(reset_code):
|
||||||
""" generate a password reset email """
|
""" generate a password reset email """
|
||||||
site = models.SiteSettings.get()
|
data = email_data()
|
||||||
send_email.delay(
|
data["reset_link"] = reset_code.link
|
||||||
reset_code.user.email,
|
data["user"] = reset_code.user.display_name
|
||||||
"Reset your password on %s" % site.name,
|
send_email.delay(reset_code.user.email, *format_email("password_reset", data))
|
||||||
"Your password reset link: %s" % reset_code.link,
|
|
||||||
|
|
||||||
|
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
|
@app.task
|
||||||
def send_email(recipient, subject, message):
|
def send_email(recipient, subject, html_content, text_content):
|
||||||
""" use a task to send the email """
|
""" use a task to send the email """
|
||||||
send_mail(
|
email = EmailMultiAlternatives(subject, text_content, None, [recipient])
|
||||||
subject,
|
email.attach_alternative(html_content, "text/html")
|
||||||
message,
|
email.send()
|
||||||
None, # sender will be the config default
|
|
||||||
[recipient],
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import ModelForm, PasswordInput, widgets
|
from django.forms import ModelForm, PasswordInput, widgets
|
||||||
from django.forms.widgets import Textarea
|
from django.forms.widgets import Textarea
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -202,6 +203,19 @@ class ExpiryWidget(widgets.Select):
|
||||||
return timezone.now() + interval
|
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 CreateInviteForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.SiteInvite
|
model = models.SiteInvite
|
||||||
|
|
59
bookwyrm/migrations/0056_auto_20210321_0303.py
Normal file
59
bookwyrm/migrations/0056_auto_20210321_0303.py
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -26,7 +26,7 @@ from .federated_server import FederatedServer
|
||||||
|
|
||||||
from .import_job import ImportJob, ImportItem
|
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)
|
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
||||||
activity_models = {
|
activity_models = {
|
||||||
|
|
|
@ -3,10 +3,11 @@ import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
from django.db import models
|
from django.db import models, IntegrityError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
from .base_model import BookWyrmModel
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ class SiteSettings(models.Model):
|
||||||
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
||||||
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
||||||
allow_registration = models.BooleanField(default=True)
|
allow_registration = models.BooleanField(default=True)
|
||||||
|
allow_invite_requests = models.BooleanField(default=True)
|
||||||
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
logo_small = 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)
|
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)
|
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():
|
def get_passowrd_reset_expiry():
|
||||||
""" give people a limited time to use the link """
|
""" give people a limited time to use the link """
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.1 KiB |
|
@ -45,9 +45,33 @@
|
||||||
<form name="register" method="post" action="/register">
|
<form name="register" method="post" action="/register">
|
||||||
{% include 'snippets/register_form.html' %}
|
{% include 'snippets/register_form.html' %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<h2 class="title">{% trans "This instance is closed" %}</h2>
|
<h2 class="title">{% trans "This instance is closed" %}</h2>
|
||||||
<p>{{ site.registration_closed_text | safe}}</p>
|
<p>{{ site.registration_closed_text | safe}}</p>
|
||||||
|
|
||||||
|
{% if site.allow_invite_requests %}
|
||||||
|
{% if request_received %}
|
||||||
|
<p>
|
||||||
|
{% trans "Thank you! Your request has been received." %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<h3>{% trans "Request an Invitation" %}</h3>
|
||||||
|
<form name="invite-request" action="{% url 'invite-request' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="block">
|
||||||
|
<label for="id_request_email" class="label">{% trans "Email address:" %}</label>
|
||||||
|
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
|
||||||
|
{% for error in request_form.email.errors %}
|
||||||
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
26
bookwyrm/templates/email/html_layout.html
Normal file
26
bookwyrm/templates/email/html_layout.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<div style="font-family: BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif; border-radius: 6px; background-color: #efefef; max-width: 632px;">
|
||||||
|
<div style="padding: 1rem; overflow: auto;">
|
||||||
|
<div style="float: left; margin-right: 1rem;">
|
||||||
|
<a style="color: #3273dc;" href="https://{{ domain }}" style="text-decoration: none;"><img src="https://{{ domain }}/{{ logo }}" alt="logo"></a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a style="color: black; text-decoration: none" href="https://{{ domain }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br>
|
||||||
|
{{ domain }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 1rem; background-color: white;">
|
||||||
|
<p>
|
||||||
|
{% if user %}{{ user }},{% else %}{% trans "Hi there," %}{% endif %}
|
||||||
|
</p>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 1rem; font-size: 0.8rem;">
|
||||||
|
<p style="margin: 0; color: #333;">{% blocktrans %}BookWyrm hosted on <a style="color: #3273dc;" href="https://{{ domain }}">{{ site_name }}</a>{% endblocktrans %}</p>
|
||||||
|
{% if user %}
|
||||||
|
<p style="margin: 0; color: #333;"><a style="color: #3273dc;" href="https://{{ domain }}{% url 'prefs-profile' %}">{% trans "Email preference" %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
17
bookwyrm/templates/email/invite/html_content.html
Normal file
17
bookwyrm/templates/email/invite/html_content.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends 'email/html_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}You're invited to join {{ site_name }}!{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% trans "Join Now" as text %}
|
||||||
|
{% include 'email/snippets/action.html' with path=invite_link text=text %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% url 'code-of-conduct' as coc_path %}
|
||||||
|
{% url 'about' as about_path %}
|
||||||
|
{% blocktrans %}Learn more <a href="https://{{ domain }}{{ about_path }}">about this instance</a>.{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
2
bookwyrm/templates/email/invite/subject.html
Normal file
2
bookwyrm/templates/email/invite/subject.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans %}You're invited to join {{ site_name }}!{% endblocktrans %}
|
10
bookwyrm/templates/email/invite/text_content.html
Normal file
10
bookwyrm/templates/email/invite/text_content.html
Normal file
|
@ -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 %}
|
15
bookwyrm/templates/email/password_reset/html_content.html
Normal file
15
bookwyrm/templates/email/password_reset/html_content.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'email/html_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
{% 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 %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% trans "Reset Password" as text %}
|
||||||
|
{% include 'email/snippets/action.html' with path=reset_link text=text %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans "If you didn't request to reset your password, you can ignore this email." %}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
2
bookwyrm/templates/email/password_reset/subject.html
Normal file
2
bookwyrm/templates/email/password_reset/subject.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans %}Reset your {{ site_name }} password{% endblocktrans %}
|
|
@ -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 %}
|
19
bookwyrm/templates/email/preview.html
Normal file
19
bookwyrm/templates/email/preview.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<strong>Subject:</strong> {% include subject_path %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<strong>Html email:</strong>
|
||||||
|
<div style="border: 1px solid #c0c0c0; margin: 2em; padding: 1em;">
|
||||||
|
{% include html_content_path %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<strong>Text email:</strong>
|
||||||
|
<div style="border: 1px solid #c0c0c0; margin: 2em; padding: 1em; white-space: pre;">
|
||||||
|
{% include text_content_path %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
5
bookwyrm/templates/email/snippets/action.html
Normal file
5
bookwyrm/templates/email/snippets/action.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<p style="text-align: center; margin: 2rem;">
|
||||||
|
<a href="{{ path }}" style="background-color: #3273dc; color: #fff; border-radius: 4px; padding: 0.5rem 1rem; text-decoration: none;">
|
||||||
|
{{ text }}
|
||||||
|
</a>
|
||||||
|
</p>
|
3
bookwyrm/templates/email/text_layout.html
Normal file
3
bookwyrm/templates/email/text_layout.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% if user %}{{ user.display_name }},{% else %}{% trans "Hi there," %}{% endif %}
|
||||||
|
{% block content %}{% endblock %}
|
|
@ -8,7 +8,9 @@
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h1 class="title">{% trans "Reset Password" %}</h1>
|
<h1 class="title">{% trans "Reset Password" %}</h1>
|
||||||
{% if message %}<p>{{ message }}</p>{% endif %}
|
|
||||||
|
{% if message %}<p class="notification is-primary">{{ message }}</p>{% endif %}
|
||||||
|
|
||||||
<p>{% trans "A link to reset your password will be sent to your email address" %}</p>
|
<p>{% trans "A link to reset your password will be sent to your email address" %}</p>
|
||||||
<form name="password-reset" method="post" action="/password-reset">
|
<form name="password-reset" method="post" action="/password-reset">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -16,6 +18,9 @@
|
||||||
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
|
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="email" name="email" maxlength="254" class="input" id="id_email_register">
|
<input type="email" name="email" maxlength="254" class="input" id="id_email_register">
|
||||||
|
{% if error %}
|
||||||
|
<p class="help is-danger">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
|
|
|
@ -15,8 +15,9 @@
|
||||||
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
|
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
{% url 'settings-invites' as url %}
|
{% url 'settings-invite-requests' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
{% url 'settings-invites' as alt_url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% url 'settings-reports' as url %}
|
{% url 'settings-reports' as url %}
|
||||||
|
|
88
bookwyrm/templates/settings/manage_invite_requests.html
Normal file
88
bookwyrm/templates/settings/manage_invite_requests.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{% extends 'settings/admin_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% block header %}{% trans "Invite Requests" %}{% endblock %}
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
{% url 'settings-invite-requests' as url %}
|
||||||
|
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||||
|
<a href="{{ url }}">{% trans "Invite Requests" %}</a>
|
||||||
|
</li>
|
||||||
|
{% url 'settings-invites' as url %}
|
||||||
|
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||||
|
<a href="{{ url }}">{% trans "Invites" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="title is-4">
|
||||||
|
{% if ignored %}
|
||||||
|
{% trans "Ignored Invite Requests" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Invite Requests" %}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<table class="table is-striped">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Date" %}</th>
|
||||||
|
<th>{% trans "Email" %}</th>
|
||||||
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th>{% trans "Action" %}</th>
|
||||||
|
</tr>
|
||||||
|
{% if not requests %}
|
||||||
|
<tr><td colspan="4">{% trans "No requests" %}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
{% for req in requests %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ req.created_date | naturaltime }}</td>
|
||||||
|
<td>{{ req.email }}</td>
|
||||||
|
<td>
|
||||||
|
{% if req.invite.times_used %}
|
||||||
|
{% trans "Accepted" %}
|
||||||
|
{% elif req.invite %}
|
||||||
|
{% trans "Sent" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Requested" %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="field is-grouped">
|
||||||
|
<form name="send-invite" method="post" action="{% url 'settings-invite-requests' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="invite-request" value="{{ req.id }}">
|
||||||
|
{% if not req.invite %}
|
||||||
|
<button type="submit" class="button is-link is-light is-small">{% trans "Send invite" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" class="button is-link is-light is-small">{% trans "Re-send invite" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if req.invite and not req.invite.times_used %}
|
||||||
|
{# <button class="button is-danger is-light is-small">{% trans "Revoke invite" %}</button> #}
|
||||||
|
{% else %}
|
||||||
|
<form name="ignore-request" method="post" action="{% url 'settings-invite-requests-ignore' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="invite-request" value="{{ req.id }}">
|
||||||
|
{% if not req.ignored %}
|
||||||
|
<button type="submit" class="button is-danger is-light is-small">{% trans "Ignore" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" class="button is-danger is-light is-small">{% trans "Un-gnore" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% include 'snippets/pagination.html' with page=requests path=request.path %}
|
||||||
|
|
||||||
|
{% if ignored %}
|
||||||
|
<p><a href="{% url 'settings-invite-requests' %}">{% trans "Back to pending requests" %}</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p><a href="{% url 'settings-invite-requests' %}?ignored=True">{% trans "View ignored requests" %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
|
@ -3,6 +3,20 @@
|
||||||
{% block header %}{% trans "Invites" %}{% endblock %}
|
{% block header %}{% trans "Invites" %}{% endblock %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
{% url 'settings-invite-requests' as url %}
|
||||||
|
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||||
|
<a href="{{ url }}">{% trans "Invite Requests" %}</a>
|
||||||
|
</li>
|
||||||
|
{% url 'settings-invites' as url %}
|
||||||
|
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||||
|
<a href="{{ url }}">{% trans "Invites" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Generate New Invite" %}</h2>
|
<h2 class="title is-4">{% trans "Generate New Invite" %}</h2>
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,10 @@
|
||||||
<label class="label" for="id_allow_registration">{% trans "Allow registration:" %}
|
<label class="label" for="id_allow_registration">{% trans "Allow registration:" %}
|
||||||
{{ site_form.allow_registration }}
|
{{ site_form.allow_registration }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<label class="label" for="id_allow_invite_requests">{% trans "Allow invite requests:" %}
|
||||||
|
{{ site_form.allow_invite_requests }}
|
||||||
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||||
{{ site_form.registration_closed_text }}
|
{{ site_form.registration_closed_text }}
|
||||||
|
|
51
bookwyrm/tests/test_emailing.py
Normal file
51
bookwyrm/tests/test_emailing.py
Normal file
|
@ -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)
|
|
@ -9,7 +9,6 @@ import responses
|
||||||
|
|
||||||
from bookwyrm import models, importer
|
from bookwyrm import models, importer
|
||||||
from bookwyrm.goodreads_import import GoodreadsImporter
|
from bookwyrm.goodreads_import import GoodreadsImporter
|
||||||
from bookwyrm import importer
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,8 +16,8 @@ class GoodreadsImport(TestCase):
|
||||||
""" importing from goodreads csv """
|
""" importing from goodreads csv """
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.importer = GoodreadsImporter()
|
|
||||||
""" use a test csv """
|
""" use a test csv """
|
||||||
|
self.importer = GoodreadsImporter()
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv")
|
datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv")
|
||||||
self.csv = open(datafile, "r", encoding=self.importer.encoding)
|
self.csv = open(datafile, "r", encoding=self.importer.encoding)
|
||||||
self.user = models.User.objects.create_user(
|
self.user = models.User.objects.create_user(
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm import views
|
from bookwyrm import views
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,3 +50,81 @@ class InviteViews(TestCase):
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
result.render()
|
result.render()
|
||||||
self.assertEqual(result.status_code, 200)
|
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)
|
||||||
|
|
|
@ -42,7 +42,8 @@ class PasswordViews(TestCase):
|
||||||
request = self.factory.post("", {"email": "aa@bb.ccc"})
|
request = self.factory.post("", {"email": "aa@bb.ccc"})
|
||||||
view = views.PasswordResetRequest.as_view()
|
view = views.PasswordResetRequest.as_view()
|
||||||
resp = view(request)
|
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"})
|
request = self.factory.post("", {"email": "mouse@mouse.com"})
|
||||||
with patch("bookwyrm.emailing.send_email.delay"):
|
with patch("bookwyrm.emailing.send_email.delay"):
|
||||||
|
|
|
@ -48,12 +48,30 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
# admin
|
# admin
|
||||||
re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"),
|
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(
|
re_path(
|
||||||
r"^settings/federation", views.Federation.as_view(), name="settings-federation"
|
r"^settings/federation", views.Federation.as_view(), name="settings-federation"
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
|
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<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
|
re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
|
||||||
# moderation
|
# moderation
|
||||||
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
|
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"),
|
re_path(r"^report/?$", views.make_report, name="report"),
|
||||||
# landing pages
|
# landing pages
|
||||||
re_path(r"^about/?$", views.About.as_view()),
|
re_path(r"^about/?$", views.About.as_view(), name="about"),
|
||||||
path("", views.Home.as_view()),
|
path("", views.Home.as_view(), name="landing"),
|
||||||
re_path(r"^discover/?$", views.Discover.as_view()),
|
re_path(r"^discover/?$", views.Discover.as_view()),
|
||||||
re_path(r"^notifications/?$", views.Notifications.as_view()),
|
re_path(r"^notifications/?$", views.Notifications.as_view()),
|
||||||
# feeds
|
# feeds
|
||||||
|
|
|
@ -13,7 +13,8 @@ from .goal import Goal, hide_goal
|
||||||
from .import_data import Import, ImportStatus
|
from .import_data import Import, ImportStatus
|
||||||
from .inbox import Inbox
|
from .inbox import Inbox
|
||||||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
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 .isbn import Isbn
|
||||||
from .landing import About, Home, Discover
|
from .landing import About, Home, Discover
|
||||||
from .list import Lists, List, Curate, UserLists
|
from .list import Lists, List, Curate, UserLists
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import re
|
import re
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
from django.core.exceptions import FieldError
|
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 import activitypub, models
|
||||||
from bookwyrm.connectors import ConnectorException, get_data
|
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():
|
if viewer.is_authenticated and viewer in user.blocks.all():
|
||||||
return True
|
return True
|
||||||
return False
|
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]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -6,9 +6,11 @@ from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
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 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 bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from . import helpers
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
|
@ -77,3 +79,74 @@ class Invite(View):
|
||||||
return TemplateResponse(request, "invite.html", data)
|
return TemplateResponse(request, "invite.html", data)
|
||||||
|
|
||||||
# post handling is in views.authentication.Register
|
# 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")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
""" non-interactive pages """
|
""" non-interactive pages """
|
||||||
from django.db.models import Max
|
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
from .feed import Feed
|
from .feed import Feed
|
||||||
|
from . import helpers
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
|
@ -33,20 +33,9 @@ class Discover(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
""" tiled book activity page """
|
""" 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 = {
|
data = {
|
||||||
"register_form": forms.RegisterForm(),
|
"register_form": forms.RegisterForm(),
|
||||||
"books": list(set(books)),
|
"request_form": forms.InviteRequestForm(),
|
||||||
|
"books": helpers.get_discover_books(),
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "discover/discover.html", data)
|
return TemplateResponse(request, "discover/discover.html", data)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
@ -28,7 +29,8 @@ class PasswordResetRequest(View):
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(email=email)
|
user = models.User.objects.get(email=email)
|
||||||
except models.User.DoesNotExist:
|
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
|
# remove any existing password reset cods for this user
|
||||||
models.PasswordReset.objects.filter(user=user).all().delete()
|
models.PasswordReset.objects.filter(user=user).all().delete()
|
||||||
|
@ -36,7 +38,7 @@ class PasswordResetRequest(View):
|
||||||
# create a new reset code
|
# create a new reset code
|
||||||
code = models.PasswordReset.objects.create(user=user)
|
code = models.PasswordReset.objects.create(user=user)
|
||||||
password_reset_email(code)
|
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)
|
return TemplateResponse(request, "password_reset_request.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.template.response import TemplateResponse
|
||||||
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 bookwyrm import forms, models
|
from bookwyrm import emailing, forms, models
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
|
@ -33,3 +33,17 @@ class Site(View):
|
||||||
form.save()
|
form.save()
|
||||||
|
|
||||||
return redirect("settings-site")
|
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)
|
||||||
|
|
Loading…
Reference in a new issue