diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index d4f045dc..48aeabfb 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -19,3 +19,11 @@ class ReportSettingsForm(forms.Form): class SetPasswordForm(forms.Form): password = forms.CharField() + + +class InviteTeamMemberForm(forms.Form): + email = LowercaseEmailField() + + +class RemoveTeamMemberForm(forms.Form): + email = LowercaseEmailField() diff --git a/hc/accounts/migrations/0005_auto_20160509_0801.py b/hc/accounts/migrations/0005_auto_20160509_0801.py new file mode 100644 index 00000000..63dbb184 --- /dev/null +++ b/hc/accounts/migrations/0005_auto_20160509_0801.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-05-09 08:01 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0004_profile_api_key'), + ] + + operations = [ + migrations.CreateModel( + name='Member', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.AddField( + model_name='profile', + name='team_access_allowed', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profile', + name='team_name', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='member', + name='team', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Profile'), + ), + migrations.AddField( + model_name='member', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 422a4989..9a229314 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -21,7 +21,12 @@ class ProfileManager(models.Manager): class Profile(models.Model): + # Owner: user = models.OneToOneField(User, blank=True, null=True) + + team_name = models.CharField(max_length=200, blank=True) + team_access_allowed = models.BooleanField(default=False) + next_report_date = models.DateTimeField(null=True, blank=True) reports_allowed = models.BooleanField(default=True) ping_log_limit = models.IntegerField(default=100) @@ -30,13 +35,19 @@ class Profile(models.Model): objects = ProfileManager() - def send_instant_login_link(self): + def __str__(self): + return self.team_name or self.user.email + + def send_instant_login_link(self, inviting_profile=None): token = str(uuid.uuid4()) self.token = make_password(token) self.save() path = reverse("hc-check-token", args=[self.user.username, token]) - ctx = {"login_link": settings.SITE_ROOT + path} + ctx = { + "login_link": settings.SITE_ROOT + path, + "inviting_profile": inviting_profile + } emails.login(self.user.email, ctx) def send_set_password_link(self): @@ -69,3 +80,14 @@ class Profile(models.Model): } emails.report(self.user.email, ctx) + + def invite(self, user): + member = Member(team=self, user=user) + member.save() + + Profile.objects.for_user(user).send_instant_login_link(self) + + +class Member(models.Model): + team = models.ForeignKey(Profile) + user = models.ForeignKey(User) diff --git a/hc/accounts/tests/test_profile.py b/hc/accounts/tests/test_profile.py index 43dc2452..04441461 100644 --- a/hc/accounts/tests/test_profile.py +++ b/hc/accounts/tests/test_profile.py @@ -1,7 +1,8 @@ +from django.contrib.auth.models import User from django.core import mail from hc.test import BaseTestCase -from hc.accounts.models import Profile +from hc.accounts.models import Profile, Member from hc.api.models import Check @@ -56,3 +57,35 @@ class LoginTestCase(BaseTestCase): self.assertEqual(message.subject, 'Monthly Report') self.assertIn("Test Check", message.body) + + def test_it_adds_team_member(self): + self.client.login(username="alice@example.org", password="password") + + form = {"invite_team_member": "1", "email": "bob@example.org"} + r = self.client.post("/accounts/profile/", form) + assert r.status_code == 200 + + profile = Profile.objects.for_user(self.alice) + member = profile.member_set.get() + + self.assertEqual(member.user.email, "bob@example.org") + + # And an email should have been sent + subj = ('You have been invited to join' + ' alice@example.org on healthchecks.io') + self.assertEqual(mail.outbox[0].subject, subj) + + def test_it_removes_team_member(self): + self.client.login(username="alice@example.org", password="password") + + bob = User(username="bob", email="bob@example.org") + bob.save() + + m = Member(team=Profile.objects.for_user(self.alice), user=bob) + m.save() + + form = {"remove_team_member": "1", "email": "bob@example.org"} + r = self.client.post("/accounts/profile/", form) + assert r.status_code == 200 + + self.assertEqual(Member.objects.count(), 0) diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 76f4edbe..2efbfdc6 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -10,9 +10,10 @@ from django.contrib.auth.models import User from django.core import signing from django.http import HttpResponseBadRequest from django.shortcuts import redirect, render -from hc.accounts.forms import (EmailPasswordForm, ReportSettingsForm, +from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm, + RemoveTeamMemberForm, ReportSettingsForm, SetPasswordForm) -from hc.accounts.models import Profile +from hc.accounts.models import Profile, Member from hc.api.models import Channel, Check @@ -141,6 +142,25 @@ def profile(request): profile.reports_allowed = form.cleaned_data["reports_allowed"] profile.save() messages.info(request, "Your settings have been updated!") + elif "invite_team_member" in request.POST: + form = InviteTeamMemberForm(request.POST) + if form.is_valid(): + + email = form.cleaned_data["email"] + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + user = _make_user(email) + + profile.invite(user) + messages.info(request, "Invitation to %s sent!" % email) + elif "remove_team_member" in request.POST: + form = RemoveTeamMemberForm(request.POST) + if form.is_valid(): + + email = form.cleaned_data["email"] + Member.objects.filter(team=profile, user__email=email).delete() + messages.info(request, "%s removed from team!" % email) ctx = { "profile": profile, diff --git a/hc/payments/views.py b/hc/payments/views.py index 0f6765af..d9f8a4b6 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -109,9 +109,11 @@ def create_plan(request): profile = Profile.objects.for_user(request.user) if plan_id == "P5": profile.ping_log_limit = 1000 + profile.team_access_allowed = True profile.save() elif plan_id == "P20": profile.ping_log_limit = 10000 + profile.team_access_allowed = True profile.save() request.session["first_charge"] = True diff --git a/static/js/profile.js b/static/js/profile.js new file mode 100644 index 00000000..74d34433 --- /dev/null +++ b/static/js/profile.js @@ -0,0 +1,13 @@ +$(function() { + + $(".member-remove").click(function() { + var $this = $(this); + + $("#rtm-email").text($this.data("email")); + $("#remove-team-member-email").val($this.data("email")); + $('#remove-team-member-modal').modal("show"); + + return false; + }); + +}); \ No newline at end of file diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 901d8965..194daaa1 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -96,8 +96,62 @@ +
+
+
+

Team Access

+ {% if profile.team_access_allowed %} + {% if profile.member_set.count %} + + + + + + + {% for member in profile.member_set.all %} + + + + + + {% endfor %} +
{{ profile.user.email }}Owner
{{ member.user.email }} Member + Remove +
+ {% else %} +

+ Invite team members to your account. +

+

+ Share access to your checks and configured integrations + without having to share a login. +

+ {% endif %} + + Invite a Team Member + {% else %} +

+ Invite team members to your account. + Share access to your checks and configured integrations + without having to share a login.

+

+ To enable team access, please upgrade to + one of the paid plans. +

+ {% endif %} +
+
+
+ + + + {% endblock %} +{% block scripts %} +{% compress js %} + + + +{% endcompress %} +{% endblock %} \ No newline at end of file diff --git a/templates/emails/login-subject.html b/templates/emails/login-subject.html index 701a0b78..3884d6dc 100644 --- a/templates/emails/login-subject.html +++ b/templates/emails/login-subject.html @@ -1 +1,5 @@ -Log in to healthchecks.io +{% if inviting_profile %} + You have been invited to join {{ inviting_profile }} on healthchecks.io +{% else %} + Log in to healthchecks.io +{% endif %} diff --git a/templates/payments/pricing.html b/templates/payments/pricing.html index 6921bcac..5e78287f 100644 --- a/templates/payments/pricing.html +++ b/templates/payments/pricing.html @@ -54,6 +54,7 @@
  • Unlimited Checks
  • Unlimited Alerts
  • 100 log entries / check
  • +
  •