Browse Source

Team access WIP

pull/60/head
Pēteris Caune 9 years ago
parent
commit
1bc0f82d25
10 changed files with 284 additions and 9 deletions
  1. +8
    -0
      hc/accounts/forms.py
  2. +44
    -0
      hc/accounts/migrations/0005_auto_20160509_0801.py
  3. +24
    -2
      hc/accounts/models.py
  4. +34
    -1
      hc/accounts/tests/test_profile.py
  5. +22
    -2
      hc/accounts/views.py
  6. +2
    -0
      hc/payments/views.py
  7. +13
    -0
      static/js/profile.js
  8. +127
    -0
      templates/accounts/profile.html
  9. +5
    -1
      templates/emails/login-subject.html
  10. +5
    -3
      templates/payments/pricing.html

+ 8
- 0
hc/accounts/forms.py View File

@ -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
- 0
hc/accounts/migrations/0005_auto_20160509_0801.py View 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),
),
]

+ 24
- 2
hc/accounts/models.py View File

@ -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)

+ 34
- 1
hc/accounts/tests/test_profile.py View File

@ -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="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
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, "[email protected]")
# And an email should have been sent
subj = ('You have been invited to join'
' [email protected] on healthchecks.io')
self.assertEqual(mail.outbox[0].subject, subj)
def test_it_removes_team_member(self):
self.client.login(username="[email protected]", password="password")
bob = User(username="bob", email="[email protected]")
bob.save()
m = Member(team=Profile.objects.for_user(self.alice), user=bob)
m.save()
form = {"remove_team_member": "1", "email": "[email protected]"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.assertEqual(Member.objects.count(), 0)

+ 22
- 2
hc/accounts/views.py View File

@ -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,


+ 2
- 0
hc/payments/views.py View File

@ -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
- 0
static/js/profile.js View 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;
});
});

+ 127
- 0
templates/accounts/profile.html View File

@ -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">&times;</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">&times;</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="[email protected]">
</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 %}

+ 5
- 1
templates/emails/login-subject.html View File

@ -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 %}

+ 5
- 3
templates/payments/pricing.html View File

@ -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">&nbsp;</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…
Cancel
Save