From 6edacc44852cca0edef6b55bd2a4e3287e2e0f05 Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Sun, 20 Dec 2020 22:21:47 +0200 Subject: [PATCH] New page to allow user invites for tenant. Fixes #116 users will be invited via email. If they don't have accounts already such will be created automatically based on their addresses. --- tcms_tenants/forms.py | 24 +++++++ tcms_tenants/locale/en/LC_MESSAGES/django.po | 51 +++++++++++--- tcms_tenants/menu.py | 1 + .../tcms_tenants/email/invite_user.txt | 6 ++ .../templates/tcms_tenants/invite_users.html | 27 ++++++++ tcms_tenants/urls.py | 1 + tcms_tenants/utils.py | 69 ++++++++++++++++++- tcms_tenants/views.py | 39 ++++++++++- 8 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 tcms_tenants/templates/tcms_tenants/email/invite_user.txt create mode 100644 tcms_tenants/templates/tcms_tenants/invite_users.html diff --git a/tcms_tenants/forms.py b/tcms_tenants/forms.py index 130ed7b..7841188 100644 --- a/tcms_tenants/forms.py +++ b/tcms_tenants/forms.py @@ -37,3 +37,27 @@ class NewTenantForm(forms.Form): # pylint: disable=must-inherit-from-model-form widget=forms.HiddenInput) organization = forms.CharField(max_length=64, required=False, widget=forms.HiddenInput) + + +class InviteUsersForm(forms.Form): # pylint: disable=must-inherit-from-model-form + number_of_fields = 10 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for i in self.range: + self.fields["email_%d" % i] = forms.CharField(required=False) + + def clean(self): + emails = set() + + for i in self.range: + email = self.cleaned_data["email_%d" % i] + if email: + emails.add(email) + + self.cleaned_data["emails"] = emails + + @property + def range(self): + return range(self.number_of_fields) diff --git a/tcms_tenants/locale/en/LC_MESSAGES/django.po b/tcms_tenants/locale/en/LC_MESSAGES/django.po index 7b2eb29..2c9778a 100644 --- a/tcms_tenants/locale/en/LC_MESSAGES/django.po +++ b/tcms_tenants/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-12-09 20:56+0000\n" +"POT-Creation-Date: 2020-12-21 17:59+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,19 +18,19 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: tcms_tenants/admin.py:41 tcms_tenants/middleware.py:32 +#: tcms_tenants/admin.py:40 tcms_tenants/middleware.py:32 msgid "Unauthorized" msgstr "" -#: tcms_tenants/admin.py:71 +#: tcms_tenants/admin.py:70 msgid "Existing username, email or user ID" msgstr "" -#: tcms_tenants/admin.py:105 +#: tcms_tenants/admin.py:104 msgid "Username" msgstr "" -#: tcms_tenants/admin.py:110 +#: tcms_tenants/admin.py:109 msgid "Full name" msgstr "" @@ -47,6 +47,11 @@ msgid "Create" msgstr "" #: tcms_tenants/menu.py:14 +#: tcms_tenants/templates/tcms_tenants/invite_users.html:9 +msgid "Invite users" +msgstr "" + +#: tcms_tenants/menu.py:15 msgid "Authorized users" msgstr "" @@ -58,6 +63,17 @@ msgstr "" msgid "Tenant expires in less than 7 days" msgstr "" +#: tcms_tenants/templates/tcms_tenants/email/invite_user.txt:1 +#, python-format +msgid "" +"Dear tester,\n" +"%(invited_by)s has invited you to join their Kiwi TCMS tenant at\n" +"%(tenant_url)s\n" +"\n" +"In case you have never logged in before an account was created for you\n" +"automatically. You will have to reset your password before you can login!" +msgstr "" + #: tcms_tenants/templates/tcms_tenants/email/new.txt:1 #, python-format msgid "" @@ -67,6 +83,15 @@ msgid "" "If you have troubles please contact support!" msgstr "" +#: tcms_tenants/templates/tcms_tenants/invite_users.html:18 +msgid "Email" +msgstr "" + +#: tcms_tenants/templates/tcms_tenants/invite_users.html:24 +#: tcms_tenants/templates/tcms_tenants/new.html:49 +msgid "Save" +msgstr "" + #: tcms_tenants/templates/tcms_tenants/new.html:9 msgid "New tenant" msgstr "" @@ -87,14 +112,18 @@ msgstr "" msgid "Validation pattern" msgstr "" -#: tcms_tenants/templates/tcms_tenants/new.html:49 -msgid "Save" -msgstr "" - -#: tcms_tenants/utils.py:40 +#: tcms_tenants/utils.py:45 msgid "Schema name already in use" msgstr "" -#: tcms_tenants/utils.py:100 +#: tcms_tenants/utils.py:105 msgid "New Kiwi TCMS tenant created" msgstr "" + +#: tcms_tenants/utils.py:181 +msgid "Invitation to join to Kiwi TCMS" +msgstr "" + +#: tcms_tenants/views.py:92 +msgid "Only users who are authorized for this tenant can invite others" +msgstr "" diff --git a/tcms_tenants/menu.py b/tcms_tenants/menu.py index 785e32a..cce6d0a 100644 --- a/tcms_tenants/menu.py +++ b/tcms_tenants/menu.py @@ -11,6 +11,7 @@ (_('Tenant'), [ (_('Create'), reverse_lazy('tcms_tenants:create-tenant')), ('-', '-'), + (_('Invite users'), reverse_lazy('tcms_tenants:invite-users')), (_('Authorized users'), '/admin/tcms_tenants/tenant_authorized_users/'), ]), ] diff --git a/tcms_tenants/templates/tcms_tenants/email/invite_user.txt b/tcms_tenants/templates/tcms_tenants/email/invite_user.txt new file mode 100644 index 0000000..ca540d9 --- /dev/null +++ b/tcms_tenants/templates/tcms_tenants/email/invite_user.txt @@ -0,0 +1,6 @@ +{% load i18n %}{% blocktrans %}Dear tester, +{{ invited_by }} has invited you to join their Kiwi TCMS tenant at +{{ tenant_url }} + +In case you have never logged in before an account was created for you +automatically. You will have to reset your password before you can login!{% endblocktrans %} diff --git a/tcms_tenants/templates/tcms_tenants/invite_users.html b/tcms_tenants/templates/tcms_tenants/invite_users.html new file mode 100644 index 0000000..ff65cbc --- /dev/null +++ b/tcms_tenants/templates/tcms_tenants/invite_users.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block head %} + {{ form.media }} +{% endblock %} +{% block title %} + {% trans "Invite users" %} +{% endblock %} + + +{% block contents %} +
+
+ {% for i in form.range %} +
+ +
+ +
+
+ {% endfor %} + +
+
+{% endblock %} diff --git a/tcms_tenants/urls.py b/tcms_tenants/urls.py index 59dee1b..dcb7371 100644 --- a/tcms_tenants/urls.py +++ b/tcms_tenants/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ re_path(r'^create/$', views.NewTenantView.as_view(), name='create-tenant'), + re_path(r'^invite/$', views.InviteUsers.as_view(), name='invite-users'), re_path(r'^go/to/(?P\w+)/(?P.*)$', views.RedirectTo.as_view(), name='redirect-to'), ] diff --git a/tcms_tenants/utils.py b/tcms_tenants/utils.py index 6079e7c..b306f3b 100644 --- a/tcms_tenants/utils.py +++ b/tcms_tenants/utils.py @@ -1,8 +1,9 @@ -# Copyright (c) 2019 Alexander Todorov +# Copyright (c) 2019-2020 Alexander Todorov # Licensed under the GPL 3.0: https://www.gnu.org/licenses/gpl-3.0.txt import datetime +import uuid from django.conf import settings from django.contrib.sites.models import Site @@ -12,11 +13,15 @@ from django.contrib.contenttypes.models import ContentType from django_tenants.utils import schema_context, tenant_context +from tcms.kiwi_auth import forms as kiwi_auth_forms from tcms.core.utils.mailto import mailto from tcms_tenants.models import Domain, Tenant +UserModel = get_user_model() + + def can_access(user, tenant): # everybody can access the public schema if tenant.schema_name == 'public': @@ -115,7 +120,7 @@ class FakeRequest: # pylint: disable=too-few-public-methods,nested-class-found user = None def __init__(self, username): - self.user = get_user_model().objects.get(username=username) + self.user = UserModel.objects.get(username=username) data = { 'name': name, @@ -128,3 +133,63 @@ def __init__(self, username): request = FakeRequest(owner) return create_tenant(data, request) + + +# NOTE: defined here to avoid circular imports with forms.py +class RegistrationForm(kiwi_auth_forms.RegistrationForm): + """ + Override captcha field b/c Kiwi TCMS may be configured to + use reCAPTCHA and we don't want this to block automatic + creation of user accounts! + """ + captcha = None.__class__ + + +def create_user_account(email_address): + desired_username = username = email_address.split("@")[0] + password = uuid.uuid4().hex + + i = 1 + while UserModel.objects.filter(username=username).exists(): + username = "%s.%d" % (desired_username, i) + i += 1 + + form = RegistrationForm(data={ + "username": username, + "password1": password, + "password2": password, + "email": email_address, + }) + + user = form.save() + + # activate their account instead of sending them email with activation key + user.is_active = True + user.save() + + return user + + +def invite_users(request, email_addresses): + for email in email_addresses: + # note: users are on public_schema + user = UserModel.objects.filter(email=email).first() + + # email not found, need to create account for them + if not user: + user = create_user_account(email) + + # user already authorized for tenant + if request.tenant.authorized_users.filter(pk=user.pk).exists(): + continue + + request.tenant.authorized_users.add(user) + mailto( + template_name='tcms_tenants/email/invite_user.txt', + recipients=[user.email], + subject=str(_('Invitation to join to Kiwi TCMS')), + context={ + "invited_by": request.user.get_full_name() or request.user.username, + "tenant_url": tenant_url(request, request.tenant.schema_name), + } + ) diff --git a/tcms_tenants/views.py b/tcms_tenants/views.py index f5e50f6..590443c 100644 --- a/tcms_tenants/views.py +++ b/tcms_tenants/views.py @@ -3,15 +3,17 @@ # Licensed under the GPL 3.0: https://www.gnu.org/licenses/gpl-3.0.txt from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import permission_required from django.http import HttpResponseRedirect from django.views.generic.base import RedirectView from django.views.generic.edit import FormView from django.utils.decorators import method_decorator -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required +from django.utils.translation import gettext_lazy as _ from tcms_tenants import utils -from tcms_tenants.forms import NewTenantForm, VALIDATION_RE +from tcms_tenants.forms import InviteUsersForm, NewTenantForm, VALIDATION_RE @method_decorator(permission_required('tcms_tenants.add_tenant'), name='dispatch') @@ -66,3 +68,34 @@ def get_redirect_url(self, *args, **kwargs): tenant = kwargs['tenant'] path = kwargs['path'] return '%s/%s' % (utils.tenant_url(self.request, tenant), path) + + +@method_decorator(login_required, name='dispatch') # pylint: disable=missing-permission-required +class InviteUsers(FormView): + """ + Invite users to tenant via email. + + .. important:: + + Anyone who is authorized for this tenant can invite others + in the same way they can add them directly via the `Authorized users` + menu! + """ + form_class = InviteUsersForm + template_name = "tcms_tenants/invite_users.html" + + def get(self, request, *args, **kwargs): + if not utils.owns_tenant(request.user, request.tenant): + messages.add_message( + request, + messages.ERROR, + _("Only users who are authorized for this tenant can invite others"), + ) + return HttpResponseRedirect('/') + + return super().get(self, request, *args, **kwargs) + + def form_valid(self, form): + utils.invite_users(self.request, form.cleaned_data["emails"]) + + return HttpResponseRedirect('/')