forked from GithubBackups/healthchecks
Team access WIP
This commit is contained in:
parent
e0f38ad18d
commit
1bc0f82d25
@ -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()
|
||||
|
44
hc/accounts/migrations/0005_auto_20160509_0801.py
Normal file
44
hc/accounts/migrations/0005_auto_20160509_0801.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
13
static/js/profile.js
Normal file
13
static/js/profile.js
Normal file
@ -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;
|
||||
});
|
||||
|
||||
});
|
@ -96,8 +96,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body settings-block">
|
||||
<h2>Team Access</h2>
|
||||
{% if profile.team_access_allowed %}
|
||||
{% if profile.member_set.count %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>{{ profile.user.email }}</td>
|
||||
<td>Owner</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% for member in profile.member_set.all %}
|
||||
<tr>
|
||||
<td>{{ member.user.email }} </td>
|
||||
<td>Member</td>
|
||||
<td>
|
||||
<a
|
||||
href="#"
|
||||
data-email="{{ member.user.email }}"
|
||||
class="pull-right member-remove">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>Invite team members to your account.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Share access to your checks and configured integrations
|
||||
without having to share a login.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-primary pull-right"
|
||||
data-toggle="modal"
|
||||
data-target="#invite-team-member-modal">Invite a Team Member</a>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>Invite team members to your account.</strong>
|
||||
Share access to your checks and configured integrations
|
||||
without having to share a login.</p>
|
||||
<p>
|
||||
To enable team access, please upgrade to
|
||||
one of the <a href="{% url 'hc-pricing' %}">paid plans</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="revoke-api-key-modal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
<form id="revoke-api-key-form" method="post">
|
||||
@ -127,5 +181,78 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="remove-team-member-modal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
<form id="remove-team-member-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</span></button>
|
||||
<h4 class="remove-check-title">Remove Team Member</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You are about to remove <span id="rtm-email"></span> from the team.</p>
|
||||
<p>Are you sure?</p>
|
||||
<input
|
||||
type="hidden"
|
||||
name="email"
|
||||
id="remove-team-member-email" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
name="remove_team_member"
|
||||
class="btn btn-danger">Remove Member from Team</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="invite-team-member-modal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</span></button>
|
||||
<h4 class="remove-check-title">Invite a Team Member</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul>
|
||||
<li>Team Members can create and manage Checks and Integrations</li>
|
||||
<li>Only the team owner (you) can view and edit billing settings</li>
|
||||
</ul>
|
||||
<div class="form-group">
|
||||
<label for="itm-email" class="col-sm-2 control-label">Email</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="itm-email"
|
||||
name="email"
|
||||
placeholder="friend@example.org">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
name="invite_team_member"
|
||||
class="btn btn-primary">Send Invite</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/profile.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
@ -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 %}
|
||||
|
@ -54,6 +54,7 @@
|
||||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Checks</li>
|
||||
<li class="list-group-item"><i class="fa fa-check"></i> Unlimited Alerts</li>
|
||||
<li class="list-group-item">100 log entries / check</li>
|
||||
<li class="list-group-item"> </li>
|
||||
</ul>
|
||||
<div class="panel-footer">
|
||||
{% if request.user.is_authenticated %}
|
||||
@ -87,6 +88,7 @@
|
||||
<li class="list-group-item">Unlimited Checks</li>
|
||||
<li class="list-group-item">Unlimited Alerts</li>
|
||||
<li class="list-group-item">1000 log entries / check</li>
|
||||
<li class="list-group-item">Team Access</li>
|
||||
</ul>
|
||||
<div class="panel-footer">
|
||||
{% if request.user.is_authenticated %}
|
||||
@ -127,6 +129,7 @@
|
||||
<li class="list-group-item">Unlimited Checks</li>
|
||||
<li class="list-group-item">Unlimited Alerts</li>
|
||||
<li class="list-group-item">10'000 log entries / check</li>
|
||||
<li class="list-group-item">Team Access</li>
|
||||
</ul>
|
||||
<div class="panel-footer">
|
||||
{% if request.user.is_authenticated %}
|
||||
@ -191,9 +194,8 @@
|
||||
|
||||
<h2>If I cancel my paid plan, do I get a refund?</h2>
|
||||
<p>
|
||||
You can easily cancel your subscription at any time.
|
||||
There are no cancellation fees, though no refunds are
|
||||
provided for prorated periods.
|
||||
You can easily cancel your subscription at any time, but
|
||||
no refunds are provided for prorated periods.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user