Browse Source

All plans now have team access, but different team size limits.

pull/140/head
Pēteris Caune 7 years ago
parent
commit
0723476a0c
11 changed files with 78 additions and 44 deletions
  1. +15
    -5
      hc/accounts/admin.py
  2. +20
    -0
      hc/accounts/migrations/0010_profile_team_limit.py
  3. +5
    -0
      hc/accounts/models.py
  4. +7
    -11
      hc/accounts/tests/test_profile.py
  5. +1
    -4
      hc/accounts/views.py
  6. +1
    -1
      hc/payments/tests/test_cancel_plan.py
  7. +1
    -2
      hc/payments/tests/test_create_plan.py
  8. +3
    -0
      hc/payments/views.py
  9. +0
    -1
      hc/test.py
  10. +9
    -7
      templates/accounts/profile.html
  11. +16
    -13
      templates/payments/pricing.html

+ 15
- 5
hc/accounts/admin.py View File

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

+ 5
- 0
hc/accounts/models.py View File

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


+ 7
- 11
hc/accounts/tests/test_profile.py View File

@ -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'
' [email protected] on {0}'.format(getattr(settings, "SITE_NAME")))
' [email protected] 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):
self.client.login(username="[email protected]", password="password")
def test_it_checks_team_size(self):
self.profile.team_limit = 0
self.profile.save()
self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"} form = {"invite_team_member": "1", "email": "[email protected]"}
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="[email protected]", password="password") self.client.login(username="[email protected]", 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="[email protected]", 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="[email protected]", password="password") self.client.login(username="[email protected]", password="password")


+ 1
- 4
hc/accounts/views.py View File

@ -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"]


+ 1
- 1
hc/payments/tests/test_cancel_plan.py View File

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

+ 1
- 2
hc/payments/tests/test_create_plan.py View File

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


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

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


+ 0
- 1
hc/test.py View File

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


+ 9
- 7
templates/accounts/profile.html View File

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


+ 16
- 13
templates/payments/pricing.html View File

@ -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">&nbsp;</li> <li class="list-group-item">&nbsp;</li>
<li class="list-group-item">&nbsp;</li> <li class="list-group-item">&nbsp;</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…
Cancel
Save