flaskbb-plugin-inviteonly 1.0.1__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/PKG-INFO +2 -2
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/forms.py +2 -2
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/utils.py +20 -7
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/pyproject.toml +2 -2
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/conftest.py +5 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/test_inviteonly_views.py +64 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/.gitignore +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/LICENSE +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/README.md +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/__init__.py +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/migrations/202607301400_c3e7a1f9b8d2_add_inviteonly.py +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/migrations/__init__.py +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/models.py +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/py.typed +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/templates/inviteonly/_macros.html +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/templates/inviteonly/_navlink.html +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/templates/inviteonly/admin.html +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/templates/inviteonly/enter_code.html +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/templates/inviteonly/mine.html +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/templates/inviteonly/register.html +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/inviteonly/views.py +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/test_inviteonly_models.py +0 -0
- {flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/test_inviteonly_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flaskbb-plugin-inviteonly
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Invite-only registration for FlaskBB - let admins and quota-holding members invite new users even while registration is closed
|
|
5
5
|
Project-URL: Homepage, https://github.com/flaskbb/flaskbb-plugin-inviteonly
|
|
6
6
|
Project-URL: Repository, https://github.com/flaskbb/flaskbb-plugin-inviteonly
|
|
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
23
23
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
25
|
Requires-Python: >=3.12
|
|
26
|
-
Requires-Dist: flaskbb>=3.0.
|
|
26
|
+
Requires-Dist: flaskbb>=3.0.0rc4
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
Invite-only
|
|
@@ -13,7 +13,7 @@ from flask_babelplus import gettext as _
|
|
|
13
13
|
from flask_wtf import FlaskForm
|
|
14
14
|
from flaskbb.utils.forms import FlaskBBForm
|
|
15
15
|
from wtforms import BooleanField, IntegerField, StringField, SubmitField
|
|
16
|
-
from wtforms.validators import DataRequired, Length, NumberRange, Optional
|
|
16
|
+
from wtforms.validators import DataRequired, Email, Length, NumberRange, Optional
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class InviteCodeForm(FlaskBBForm):
|
|
@@ -27,7 +27,7 @@ class InviteCodeForm(FlaskBBForm):
|
|
|
27
27
|
class CreateInviteForm(FlaskBBForm):
|
|
28
28
|
email = StringField(
|
|
29
29
|
_("Email address (optional)"),
|
|
30
|
-
validators=[Optional(), Length(max=255)],
|
|
30
|
+
validators=[Optional(), Length(max=255), Email(message=_("Invalid email address."))],
|
|
31
31
|
)
|
|
32
32
|
submit = SubmitField(_("Create invitation"))
|
|
33
33
|
|
|
@@ -9,16 +9,20 @@ emailing invites.
|
|
|
9
9
|
:license: BSD License, see LICENSE for more details.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import logging
|
|
12
13
|
import secrets
|
|
13
14
|
|
|
14
15
|
from flask_allows2 import Permission
|
|
15
|
-
from flaskbb.email import send_email
|
|
16
|
+
from flaskbb.email import send_async_email, send_email
|
|
16
17
|
from flaskbb.settings import flaskbb_config
|
|
17
18
|
from flaskbb.user.models import User
|
|
18
19
|
from flaskbb.utils.requirements import IsAdmin
|
|
20
|
+
from kombu.exceptions import OperationalError
|
|
19
21
|
|
|
20
22
|
from .models import Invitation, InviteQuota
|
|
21
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
22
26
|
CODE_LENGTH = 10
|
|
23
27
|
|
|
24
28
|
# Setting defaults - also used as the SettingGroup's defaults in __init__.py,
|
|
@@ -103,15 +107,24 @@ def grant_quota(user: User, remaining: int | None) -> None:
|
|
|
103
107
|
|
|
104
108
|
|
|
105
109
|
def send_invite_email(email: str, link: str, invited_by: User) -> None:
|
|
106
|
-
|
|
107
|
-
subject
|
|
108
|
-
recipients
|
|
109
|
-
text_body
|
|
110
|
+
message = {
|
|
111
|
+
"subject": "You've been invited to join the forum",
|
|
112
|
+
"recipients": [email],
|
|
113
|
+
"text_body": (
|
|
110
114
|
f"{invited_by.username} has invited you to join the forum.\n\n"
|
|
111
115
|
f"Follow this link to register: {link}\n"
|
|
112
116
|
),
|
|
113
|
-
html_body
|
|
117
|
+
"html_body": (
|
|
114
118
|
f"<p>{invited_by.username} has invited you to join the forum.</p>"
|
|
115
119
|
f'<p><a href="{link}">{link}</a></p>'
|
|
116
120
|
),
|
|
117
|
-
|
|
121
|
+
}
|
|
122
|
+
# TODO: replace with flaskbb.email.delay_or_run once FlaskBB 3.0 is out
|
|
123
|
+
try:
|
|
124
|
+
send_async_email.delay(**message)
|
|
125
|
+
except OperationalError:
|
|
126
|
+
logger.warning("Could not queue the invitation mail, sending it in-process")
|
|
127
|
+
try:
|
|
128
|
+
send_email(**message)
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.exception("Could not send the invitation mail to %s", email)
|
|
@@ -7,6 +7,11 @@ from inviteonly.models import Invitation
|
|
|
7
7
|
from inviteonly.utils import generate_code
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
@pytest.fixture(scope="package")
|
|
11
|
+
def tested_plugins():
|
|
12
|
+
return ["inviteonly"]
|
|
13
|
+
|
|
14
|
+
|
|
10
15
|
@pytest.fixture
|
|
11
16
|
def invite(user):
|
|
12
17
|
invitation = Invitation(code=generate_code(), created_by_id=user.id)
|
{flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/test_inviteonly_views.py
RENAMED
|
@@ -2,9 +2,12 @@ from contextlib import contextmanager
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
from flask_login import login_user, logout_user
|
|
5
|
+
from flaskbb.email import send_async_email
|
|
6
|
+
from flaskbb.extensions import mail
|
|
5
7
|
from flaskbb.settings import flaskbb_config
|
|
6
8
|
from flaskbb.user.models import User
|
|
7
9
|
from flaskbb.utils.helpers import time_utcnow
|
|
10
|
+
from kombu.exceptions import OperationalError
|
|
8
11
|
from werkzeug.exceptions import NotFound
|
|
9
12
|
|
|
10
13
|
from inviteonly.models import Invitation
|
|
@@ -112,6 +115,23 @@ def test_create_invite_blocked_when_quota_exhausted(application, database, user)
|
|
|
112
115
|
assert Invitation.get_all(Invitation.created_by_id == user.id) == []
|
|
113
116
|
|
|
114
117
|
|
|
118
|
+
def test_create_invite_rejects_an_invalid_email(application, database, default_settings, user):
|
|
119
|
+
grant_quota(user, 1)
|
|
120
|
+
|
|
121
|
+
view = MyInvitations.as_view("mine")
|
|
122
|
+
with (
|
|
123
|
+
_csrf_disabled(application),
|
|
124
|
+
application.test_request_context(
|
|
125
|
+
method="POST", data={"email": "not-an-email"}, path="/invite/mine"
|
|
126
|
+
),
|
|
127
|
+
_logged_in(user),
|
|
128
|
+
):
|
|
129
|
+
resp = view()
|
|
130
|
+
|
|
131
|
+
assert "Invalid email address." in resp
|
|
132
|
+
assert Invitation.get_all(Invitation.created_by_id == user.id) == []
|
|
133
|
+
|
|
134
|
+
|
|
115
135
|
def test_revoke_invite_by_non_owner_non_admin_is_rejected(
|
|
116
136
|
application, database, invite, other_moderator_user
|
|
117
137
|
):
|
|
@@ -160,3 +180,47 @@ def test_admin_invitations_allows_admin(application, database, default_settings,
|
|
|
160
180
|
resp = view()
|
|
161
181
|
|
|
162
182
|
assert isinstance(resp, str)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _create_invite(application, user, email):
|
|
186
|
+
view = MyInvitations.as_view("mine")
|
|
187
|
+
with (
|
|
188
|
+
_csrf_disabled(application),
|
|
189
|
+
application.test_request_context(method="POST", data={"email": email}, path="/invite/mine"),
|
|
190
|
+
_logged_in(user),
|
|
191
|
+
):
|
|
192
|
+
return view()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_invite_mail_is_queued(application, database, default_settings, admin_user, mocker):
|
|
196
|
+
delay = mocker.patch.object(send_async_email, "delay")
|
|
197
|
+
|
|
198
|
+
resp = _create_invite(application, admin_user, "friend@example.org")
|
|
199
|
+
|
|
200
|
+
assert resp.status_code == 302
|
|
201
|
+
delay.assert_called_once()
|
|
202
|
+
assert delay.call_args.kwargs["recipients"] == ["friend@example.org"]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_invite_mail_is_sent_in_process_without_broker(
|
|
206
|
+
application, database, default_settings, admin_user, mocker
|
|
207
|
+
):
|
|
208
|
+
mocker.patch.object(send_async_email, "delay", side_effect=OperationalError)
|
|
209
|
+
|
|
210
|
+
with mail.record_messages() as outbox:
|
|
211
|
+
resp = _create_invite(application, admin_user, "friend@example.org")
|
|
212
|
+
|
|
213
|
+
assert resp.status_code == 302
|
|
214
|
+
assert [message.recipients for message in outbox] == [["friend@example.org"]]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_failing_invite_mail_keeps_the_invitation(
|
|
218
|
+
application, database, default_settings, admin_user, mocker
|
|
219
|
+
):
|
|
220
|
+
mocker.patch.object(send_async_email, "delay", side_effect=OperationalError)
|
|
221
|
+
mocker.patch.object(mail, "send", side_effect=ConnectionRefusedError)
|
|
222
|
+
|
|
223
|
+
resp = _create_invite(application, admin_user, "friend@example.org")
|
|
224
|
+
|
|
225
|
+
assert resp.status_code == 302
|
|
226
|
+
assert Invitation.get(Invitation.invited_email == "friend@example.org") is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/test_inviteonly_models.py
RENAMED
|
File without changes
|
{flaskbb_plugin_inviteonly-1.0.1 → flaskbb_plugin_inviteonly-1.0.2}/tests/test_inviteonly_utils.py
RENAMED
|
File without changes
|