Skip to content

Commit

Permalink
New page to allow user invites for tenant. Fixes #116
Browse files Browse the repository at this point in the history
users will be invited via email. If they don't have accounts
already such will be created automatically based on their
addresses.
  • Loading branch information
atodorov committed Dec 23, 2020
1 parent bc0ed5f commit 6edacc4
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 16 deletions.
24 changes: 24 additions & 0 deletions tcms_tenants/forms.py
Expand Up @@ -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)
51 changes: 40 additions & 11 deletions tcms_tenants/locale/en/LC_MESSAGES/django.po
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand All @@ -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 ""

Expand All @@ -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 ""

Expand All @@ -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 ""
Expand All @@ -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 ""
Expand All @@ -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 ""
1 change: 1 addition & 0 deletions tcms_tenants/menu.py
Expand Up @@ -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/'),
]),
]
6 changes: 6 additions & 0 deletions 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 %}
27 changes: 27 additions & 0 deletions 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 %}
<div class="container-fluid container-cards-pf">
<form class="form-horizontal" action="{% url 'tcms_tenants:invite-users' %}" method="post">
{% for i in form.range %}
<div class="form-group">
<label class="col-md-1 col-lg-1" for="email_{{ i }}">{% trans "Email" %}</label>
<div class="col-sm-4 col-md-4 col-lg-4">
<input type="text" id="email_{{ i }}" name="email_{{ i }}" class="form-control">
</div>
</div>
{% endfor %}
<button type="submit" class="btn btn-default btn-lg">{% trans "Save" %}</button>
</form>
</div>
{% endblock %}
1 change: 1 addition & 0 deletions tcms_tenants/urls.py
Expand Up @@ -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<tenant>\w+)/(?P<path>.*)$',
views.RedirectTo.as_view(), name='redirect-to'),
]
69 changes: 67 additions & 2 deletions tcms_tenants/utils.py
@@ -1,8 +1,9 @@
# Copyright (c) 2019 Alexander Todorov <atodorov@MrSenko.com>
# Copyright (c) 2019-2020 Alexander Todorov <atodorov@MrSenko.com>

# 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
Expand All @@ -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':
Expand Down Expand Up @@ -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,
Expand All @@ -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),
}
)
39 changes: 36 additions & 3 deletions tcms_tenants/views.py
Expand Up @@ -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')
Expand Down Expand Up @@ -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('/')

0 comments on commit 6edacc4

Please sign in to comment.