forked from GithubBackups/healthchecks
All plans now have team access, but different team size limits.
This commit is contained in:
parent
1bae89e405
commit
0723476a0c
@ -1,6 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Count
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@ -25,7 +26,7 @@ class ProfileFieldset(Fieldset):
|
|||||||
|
|
||||||
class TeamFieldset(Fieldset):
|
class TeamFieldset(Fieldset):
|
||||||
name = "Team"
|
name = "Team"
|
||||||
fields = ("team_name", "team_access_allowed", "check_limit",
|
fields = ("team_name", "team_limit", "check_limit",
|
||||||
"ping_log_limit", "sms_limit", "sms_sent", "last_sms_date",
|
"ping_log_limit", "sms_limit", "sms_sent", "last_sms_date",
|
||||||
"bill_to")
|
"bill_to")
|
||||||
|
|
||||||
@ -41,17 +42,23 @@ class ProfileAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = ("user", "email")
|
readonly_fields = ("user", "email")
|
||||||
raw_id_fields = ("current_team", )
|
raw_id_fields = ("current_team", )
|
||||||
list_select_related = ("user", )
|
list_select_related = ("user", )
|
||||||
list_display = ("id", "users", "checks", "team_access_allowed",
|
list_display = ("id", "users", "checks", "invited",
|
||||||
"reports_allowed", "ping_log_limit", "sms")
|
"reports_allowed", "ping_log_limit", "sms")
|
||||||
search_fields = ["id", "user__email"]
|
search_fields = ["id", "user__email"]
|
||||||
list_filter = ("team_access_allowed", "reports_allowed",
|
list_filter = ("team_access_allowed", "team_limit", "reports_allowed",
|
||||||
"check_limit", "next_report_date")
|
"check_limit", "next_report_date")
|
||||||
|
|
||||||
fieldsets = (ProfileFieldset.tuple(), TeamFieldset.tuple())
|
fieldsets = (ProfileFieldset.tuple(), TeamFieldset.tuple())
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super(ProfileAdmin, self).get_queryset(request)
|
||||||
|
qs = qs.annotate(Count("member", distinct=True))
|
||||||
|
qs = qs.annotate(Count("user__check", distinct=True))
|
||||||
|
return qs
|
||||||
|
|
||||||
@mark_safe
|
@mark_safe
|
||||||
def users(self, obj):
|
def users(self, obj):
|
||||||
if obj.member_set.count() == 0:
|
if obj.member__count == 0:
|
||||||
return obj.user.email
|
return obj.user.email
|
||||||
else:
|
else:
|
||||||
return render_to_string("admin/profile_list_team.html", {
|
return render_to_string("admin/profile_list_team.html", {
|
||||||
@ -60,7 +67,7 @@ class ProfileAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@mark_safe
|
@mark_safe
|
||||||
def checks(self, obj):
|
def checks(self, obj):
|
||||||
num_checks = Check.objects.filter(user=obj.user).count()
|
num_checks = obj.user__check__count
|
||||||
pct = 100 * num_checks / max(obj.check_limit, 1)
|
pct = 100 * num_checks / max(obj.check_limit, 1)
|
||||||
pct = min(100, int(pct))
|
pct = min(100, int(pct))
|
||||||
|
|
||||||
@ -69,6 +76,9 @@ class ProfileAdmin(admin.ModelAdmin):
|
|||||||
%d of %d
|
%d of %d
|
||||||
""" % (pct, num_checks, obj.check_limit)
|
""" % (pct, num_checks, obj.check_limit)
|
||||||
|
|
||||||
|
def invited(self, obj):
|
||||||
|
return "%d of %d" % (obj.member__count, obj.team_limit)
|
||||||
|
|
||||||
def sms(self, obj):
|
def sms(self, obj):
|
||||||
return "%d of %d" % (obj.sms_sent, obj.sms_limit)
|
return "%d of %d" % (obj.sms_sent, obj.sms_limit)
|
||||||
|
|
||||||
|
20
hc/accounts/migrations/0010_profile_team_limit.py
Normal file
20
hc/accounts/migrations/0010_profile_team_limit.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-09-02 11:52
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0009_auto_20170714_1734'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='team_limit',
|
||||||
|
field=models.IntegerField(default=2),
|
||||||
|
),
|
||||||
|
]
|
@ -28,6 +28,7 @@ class ProfileManager(models.Manager):
|
|||||||
# If not using payments, set high limits
|
# If not using payments, set high limits
|
||||||
profile.check_limit = 500
|
profile.check_limit = 500
|
||||||
profile.sms_limit = 500
|
profile.sms_limit = 500
|
||||||
|
profile.team_limit = 500
|
||||||
|
|
||||||
profile.save()
|
profile.save()
|
||||||
return profile
|
return profile
|
||||||
@ -49,6 +50,7 @@ class Profile(models.Model):
|
|||||||
last_sms_date = models.DateTimeField(null=True, blank=True)
|
last_sms_date = models.DateTimeField(null=True, blank=True)
|
||||||
sms_limit = models.IntegerField(default=0)
|
sms_limit = models.IntegerField(default=0)
|
||||||
sms_sent = models.IntegerField(default=0)
|
sms_sent = models.IntegerField(default=0)
|
||||||
|
team_limit = models.IntegerField(default=2)
|
||||||
|
|
||||||
objects = ProfileManager()
|
objects = ProfileManager()
|
||||||
|
|
||||||
@ -121,6 +123,9 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
emails.report(self.user.email, ctx)
|
emails.report(self.user.email, ctx)
|
||||||
|
|
||||||
|
def can_invite(self):
|
||||||
|
return self.member_set.count() < self.team_limit
|
||||||
|
|
||||||
def invite(self, user):
|
def invite(self, user):
|
||||||
member = Member(team=self, user=user)
|
member = Member(team=self, user=user)
|
||||||
member.save()
|
member.save()
|
||||||
|
@ -75,15 +75,18 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
|
|
||||||
# And an email should have been sent
|
# And an email should have been sent
|
||||||
subj = ('You have been invited to join'
|
subj = ('You have been invited to join'
|
||||||
' alice@example.org on {0}'.format(getattr(settings, "SITE_NAME")))
|
' alice@example.org on %s' % settings.SITE_NAME)
|
||||||
self.assertEqual(mail.outbox[0].subject, subj)
|
self.assertEqual(mail.outbox[0].subject, subj)
|
||||||
|
|
||||||
def test_add_team_member_checks_team_access_allowed_flag(self):
|
def test_it_checks_team_size(self):
|
||||||
self.client.login(username="charlie@example.org", password="password")
|
self.profile.team_limit = 0
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
form = {"invite_team_member": "1", "email": "frank@example.org"}
|
form = {"invite_team_member": "1", "email": "frank@example.org"}
|
||||||
r = self.client.post("/accounts/profile/", form)
|
r = self.client.post("/accounts/profile/", form)
|
||||||
assert r.status_code == 403
|
self.assertEqual(r.status_code, 403)
|
||||||
|
|
||||||
def test_it_removes_team_member(self):
|
def test_it_removes_team_member(self):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
@ -107,13 +110,6 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
self.alice.profile.refresh_from_db()
|
self.alice.profile.refresh_from_db()
|
||||||
self.assertEqual(self.alice.profile.team_name, "Alpha Team")
|
self.assertEqual(self.alice.profile.team_name, "Alpha Team")
|
||||||
|
|
||||||
def test_set_team_name_checks_team_access_allowed_flag(self):
|
|
||||||
self.client.login(username="charlie@example.org", password="password")
|
|
||||||
|
|
||||||
form = {"set_team_name": "1", "team_name": "Charlies Team"}
|
|
||||||
r = self.client.post("/accounts/profile/", form)
|
|
||||||
assert r.status_code == 403
|
|
||||||
|
|
||||||
def test_it_switches_to_own_team(self):
|
def test_it_switches_to_own_team(self):
|
||||||
self.client.login(username="bob@example.org", password="password")
|
self.client.login(username="bob@example.org", password="password")
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ def profile(request):
|
|||||||
elif "show_api_key" in request.POST:
|
elif "show_api_key" in request.POST:
|
||||||
ctx["show_api_key"] = True
|
ctx["show_api_key"] = True
|
||||||
elif "invite_team_member" in request.POST:
|
elif "invite_team_member" in request.POST:
|
||||||
if not profile.team_access_allowed:
|
if not profile.can_invite():
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
form = InviteTeamMemberForm(request.POST)
|
form = InviteTeamMemberForm(request.POST)
|
||||||
@ -220,9 +220,6 @@ def profile(request):
|
|||||||
ctx["team_member_removed"] = email
|
ctx["team_member_removed"] = email
|
||||||
ctx["team_status"] = "info"
|
ctx["team_status"] = "info"
|
||||||
elif "set_team_name" in request.POST:
|
elif "set_team_name" in request.POST:
|
||||||
if not profile.team_access_allowed:
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
|
|
||||||
form = TeamNameForm(request.POST)
|
form = TeamNameForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
profile.team_name = form.cleaned_data["team_name"]
|
profile.team_name = form.cleaned_data["team_name"]
|
||||||
|
@ -34,5 +34,5 @@ class CancelPlanTestCase(BaseTestCase):
|
|||||||
profile = Profile.objects.get(user=self.alice)
|
profile = Profile.objects.get(user=self.alice)
|
||||||
self.assertEqual(profile.ping_log_limit, 100)
|
self.assertEqual(profile.ping_log_limit, 100)
|
||||||
self.assertEqual(profile.check_limit, 20)
|
self.assertEqual(profile.check_limit, 20)
|
||||||
|
self.assertEqual(profile.team_limit, 2)
|
||||||
self.assertEqual(profile.sms_limit, 0)
|
self.assertEqual(profile.sms_limit, 0)
|
||||||
self.assertFalse(profile.team_access_allowed)
|
|
||||||
|
@ -28,7 +28,6 @@ class CreatePlanTestCase(BaseTestCase):
|
|||||||
def test_it_works(self, mock):
|
def test_it_works(self, mock):
|
||||||
self._setup_mock(mock)
|
self._setup_mock(mock)
|
||||||
|
|
||||||
self.profile.team_access_allowed = False
|
|
||||||
self.profile.sms_limit = 0
|
self.profile.sms_limit = 0
|
||||||
self.profile.sms_sent = 1
|
self.profile.sms_sent = 1
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
@ -47,9 +46,9 @@ class CreatePlanTestCase(BaseTestCase):
|
|||||||
self.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertEqual(self.profile.ping_log_limit, 1000)
|
self.assertEqual(self.profile.ping_log_limit, 1000)
|
||||||
self.assertEqual(self.profile.check_limit, 500)
|
self.assertEqual(self.profile.check_limit, 500)
|
||||||
|
self.assertEqual(self.profile.team_limit, 9)
|
||||||
self.assertEqual(self.profile.sms_limit, 50)
|
self.assertEqual(self.profile.sms_limit, 50)
|
||||||
self.assertEqual(self.profile.sms_sent, 0)
|
self.assertEqual(self.profile.sms_sent, 0)
|
||||||
self.assertTrue(self.profile.team_access_allowed)
|
|
||||||
|
|
||||||
# braintree.Subscription.cancel should have not been called
|
# braintree.Subscription.cancel should have not been called
|
||||||
assert not mock.Subscription.cancel.called
|
assert not mock.Subscription.cancel.called
|
||||||
|
@ -110,6 +110,7 @@ def create_plan(request):
|
|||||||
if plan_id == "P5":
|
if plan_id == "P5":
|
||||||
profile.ping_log_limit = 1000
|
profile.ping_log_limit = 1000
|
||||||
profile.check_limit = 500
|
profile.check_limit = 500
|
||||||
|
profile.team_limit = 9
|
||||||
profile.sms_limit = 50
|
profile.sms_limit = 50
|
||||||
profile.sms_sent = 0
|
profile.sms_sent = 0
|
||||||
profile.team_access_allowed = True
|
profile.team_access_allowed = True
|
||||||
@ -117,6 +118,7 @@ def create_plan(request):
|
|||||||
elif plan_id == "P50":
|
elif plan_id == "P50":
|
||||||
profile.ping_log_limit = 1000
|
profile.ping_log_limit = 1000
|
||||||
profile.check_limit = 500
|
profile.check_limit = 500
|
||||||
|
profile.team_limit = 500
|
||||||
profile.sms_limit = 500
|
profile.sms_limit = 500
|
||||||
profile.sms_sent = 0
|
profile.sms_sent = 0
|
||||||
profile.team_access_allowed = True
|
profile.team_access_allowed = True
|
||||||
@ -169,6 +171,7 @@ def cancel_plan(request):
|
|||||||
profile = request.user.profile
|
profile = request.user.profile
|
||||||
profile.ping_log_limit = 100
|
profile.ping_log_limit = 100
|
||||||
profile.check_limit = 20
|
profile.check_limit = 20
|
||||||
|
profile.team_limit = 2
|
||||||
profile.sms_limit = 0
|
profile.sms_limit = 0
|
||||||
profile.team_access_allowed = False
|
profile.team_access_allowed = False
|
||||||
profile.save()
|
profile.save()
|
||||||
|
@ -15,7 +15,6 @@ class BaseTestCase(TestCase):
|
|||||||
self.alice.save()
|
self.alice.save()
|
||||||
|
|
||||||
self.profile = Profile(user=self.alice, api_key="abc")
|
self.profile = Profile(user=self.alice, api_key="abc")
|
||||||
self.profile.team_access_allowed = True
|
|
||||||
self.profile.sms_limit = 50
|
self.profile.sms_limit = 50
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
|
@ -134,23 +134,25 @@
|
|||||||
Share access to your checks and configured integrations
|
Share access to your checks and configured integrations
|
||||||
without having to share a login.
|
without having to share a login.
|
||||||
</p>
|
</p>
|
||||||
{% if not profile.team_access_allowed %}
|
|
||||||
<p>
|
|
||||||
To enable team access, please upgrade to
|
|
||||||
one of the <a href="{% url 'hc-pricing' %}">paid plans</a>.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
{% if profile.team_access_allowed %}
|
{% if not profile.can_invite %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Team size limit reached.</strong>
|
||||||
|
To invite more members to your team, please
|
||||||
|
<a href="{% url 'hc-pricing' %}">upgrade your account!</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
data-target="#set-team-name-modal">Set Team Name</a>
|
data-target="#set-team-name-modal">Set Team Name</a>
|
||||||
|
|
||||||
|
{% if profile.can_invite %}
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-primary pull-right"
|
class="btn btn-primary pull-right"
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
<li class="list-group-item"><i class="fa fa-check"></i> 20 Checks</li>
|
<li class="list-group-item"><i class="fa fa-check"></i> 20 Checks</li>
|
||||||
<li class="list-group-item">100 log entries per check</li>
|
<li class="list-group-item">100 log entries per check</li>
|
||||||
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li>
|
<li class="list-group-item"><i class="fa fa-check"></i> Personal or Commercial use</li>
|
||||||
<li class="list-group-item">Single User</li>
|
<li class="list-group-item">3 Team Members</li>
|
||||||
<li class="list-group-item"> </li>
|
<li class="list-group-item"> </li>
|
||||||
<li class="list-group-item"> </li>
|
<li class="list-group-item"> </li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -105,7 +105,7 @@
|
|||||||
<li class="list-group-item">Unlimited Checks</li>
|
<li class="list-group-item">Unlimited Checks</li>
|
||||||
<li class="list-group-item">1000 log entries per check</li>
|
<li class="list-group-item">1000 log entries per check</li>
|
||||||
<li class="list-group-item">Personal or Commercial use</li>
|
<li class="list-group-item">Personal or Commercial use</li>
|
||||||
<li class="list-group-item">Team Access</li>
|
<li class="list-group-item">10 Team Members</li>
|
||||||
<li class="list-group-item">50 SMS alerts per month</li>
|
<li class="list-group-item">50 SMS alerts per month</li>
|
||||||
<li class="list-group-item">Email Support</li>
|
<li class="list-group-item">Email Support</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -147,7 +147,7 @@
|
|||||||
<li class="list-group-item">Unlimited Checks</li>
|
<li class="list-group-item">Unlimited Checks</li>
|
||||||
<li class="list-group-item">1000 log entries per check</li>
|
<li class="list-group-item">1000 log entries per check</li>
|
||||||
<li class="list-group-item">Personal or Commercial use</li>
|
<li class="list-group-item">Personal or Commercial use</li>
|
||||||
<li class="list-group-item">Team Access</li>
|
<li class="list-group-item">Unlimited Team Members</li>
|
||||||
<li class="list-group-item">500 SMS alerts per month</li>
|
<li class="list-group-item">500 SMS alerts per month</li>
|
||||||
<li class="list-group-item">Priority Email Support</li>
|
<li class="list-group-item">Priority Email Support</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -213,6 +213,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h1>Premium Features</h1>
|
<h1>Premium Features</h1>
|
||||||
|
<h2>What's "3 / 10 / Unlimited Team Members"?</h2>
|
||||||
|
<p>
|
||||||
|
Invite your colleagues
|
||||||
|
to your account so they can access your checks,
|
||||||
|
logs, and configured integrations. Inviting team members
|
||||||
|
is <strong>more convenient and more secure</strong>
|
||||||
|
than sharing a single login and password.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Each plan has a specific team size limit. When you reach
|
||||||
|
the limit, you cannot invite more team members.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>What is the "log entries per check" number?</h2>
|
<h2>What is the "log entries per check" number?</h2>
|
||||||
<p>
|
<p>
|
||||||
For each of your checks, healthchecks.io keeps a
|
For each of your checks, healthchecks.io keeps a
|
||||||
@ -236,16 +249,6 @@
|
|||||||
log entries will only cover 8 hours.
|
log entries will only cover 8 hours.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>What's Team Access?</h2>
|
|
||||||
<p>
|
|
||||||
With Team Access enabled, you can "invite" your colleagues
|
|
||||||
to your account. They will be able to access your checks,
|
|
||||||
logs, and configured integrations.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Team Access is more convenient and more secure than
|
|
||||||
sharing a single login and password.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user