diff --git a/hc/accounts/migrations/0007_profile_check_limit.py b/hc/accounts/migrations/0007_profile_check_limit.py new file mode 100644 index 00000000..8436c3b2 --- /dev/null +++ b/hc/accounts/migrations/0007_profile_check_limit.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-07 13:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_profile_current_team'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='check_limit', + field=models.IntegerField(default=20), + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 9789886d..912f5177 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -18,6 +18,10 @@ class ProfileManager(models.Manager): profile = self.filter(user=user).first() if profile is None: profile = Profile(user=user, team_access_allowed=user.is_superuser) + if not settings.USE_PAYMENTS: + # If not using payments, set a high check_limit + profile.check_limit = 500 + profile.save() return profile @@ -30,6 +34,7 @@ class Profile(models.Model): next_report_date = models.DateTimeField(null=True, blank=True) reports_allowed = models.BooleanField(default=True) ping_log_limit = models.IntegerField(default=100) + check_limit = models.IntegerField(default=20) token = models.CharField(max_length=128, blank=True) api_key = models.CharField(max_length=128, blank=True) current_team = models.ForeignKey("self", null=True) diff --git a/hc/api/migrations/0029_auto_20170507_1251.py b/hc/api/migrations/0029_auto_20170507_1251.py new file mode 100644 index 00000000..9bfa42dd --- /dev/null +++ b/hc/api/migrations/0029_auto_20170507_1251.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-07 12:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0028_auto_20170305_1907'), + ] + + operations = [ + migrations.AlterField( + model_name='notification', + name='code', + field=models.UUIDField(default=uuid.uuid4, editable=False, null=True), + ), + ] diff --git a/hc/api/tests/test_create_check.py b/hc/api/tests/test_create_check.py index 3a91534a..7c981b81 100644 --- a/hc/api/tests/test_create_check.py +++ b/hc/api/tests/test_create_check.py @@ -179,3 +179,10 @@ class CreateCheckTestCase(BaseTestCase): doc = r.json() self.assertEqual(doc["timeout"], 86400) + + def test_it_obeys_check_limit(self): + self.profile.check_limit = 0 + self.profile.save() + + r = self.post({"api_key": "abc"}) + self.assertEqual(r.status_code, 403) diff --git a/hc/api/views.py b/hc/api/views.py index 177016c2..0e272024 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -111,6 +111,10 @@ def checks(request): created = False check = _lookup(request.user, request.json) if check is None: + num_checks = Check.objects.filter(user=request.user).count() + if num_checks >= request.user.profile.check_limit: + return HttpResponseForbidden() + check = Check(user=request.user) created = True diff --git a/hc/front/tests/test_add_check.py b/hc/front/tests/test_add_check.py index 553d71f5..5f85d4c1 100644 --- a/hc/front/tests/test_add_check.py +++ b/hc/front/tests/test_add_check.py @@ -25,3 +25,12 @@ class AddCheckTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.get(url) self.assertEqual(r.status_code, 405) + + def test_it_obeys_check_limit(self): + self.profile.check_limit = 0 + self.profile.save() + + url = "/checks/add/" + self.client.login(username="alice@example.org", password="password") + r = self.client.post(url) + self.assertEqual(r.status_code, 400) diff --git a/hc/front/tests/test_my_checks.py b/hc/front/tests/test_my_checks.py index c759d1f5..2228adbf 100644 --- a/hc/front/tests/test_my_checks.py +++ b/hc/front/tests/test_my_checks.py @@ -58,3 +58,11 @@ class MyChecksTestCase(BaseTestCase): # Mobile self.assertContains(r, "label-warning") + + def test_it_hides_add_check_button(self): + self.profile.check_limit = 0 + self.profile.save() + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/checks/") + self.assertContains(r, "Check limit reached", status_code=200) diff --git a/hc/front/views.py b/hc/front/views.py index c409dd23..1c76567b 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -54,6 +54,8 @@ def my_checks(request): elif check.in_grace_period(): grace_tags.add(tag) + can_add_more = len(checks) < request.team.check_limit + ctx = { "page": "checks", "checks": checks, @@ -62,7 +64,8 @@ def my_checks(request): "down_tags": down_tags, "grace_tags": grace_tags, "ping_endpoint": settings.PING_ENDPOINT, - "timezones": all_timezones + "timezones": all_timezones, + "can_add_more": can_add_more } return render(request, "front/my_checks.html", ctx) @@ -135,6 +138,10 @@ def about(request): @require_POST @login_required def add_check(request): + num_checks = Check.objects.filter(user=request.team.user).count() + if num_checks >= request.team.check_limit: + return HttpResponseBadRequest() + check = Check(user=request.team.user) check.save() diff --git a/hc/payments/tests/test_cancel_plan.py b/hc/payments/tests/test_cancel_plan.py index 38edb031..47aae62b 100644 --- a/hc/payments/tests/test_cancel_plan.py +++ b/hc/payments/tests/test_cancel_plan.py @@ -1,5 +1,6 @@ from mock import patch +from hc.accounts.models import Profile from hc.payments.models import Subscription from hc.test import BaseTestCase @@ -13,6 +14,10 @@ class CancelPlanTestCase(BaseTestCase): self.sub.plan_id = "P5" self.sub.save() + self.profile.ping_log_limit = 1000 + self.profile.check_limit = 500 + self.profile.save() + @patch("hc.payments.models.braintree") def test_it_works(self, mock_braintree): @@ -23,3 +28,9 @@ class CancelPlanTestCase(BaseTestCase): self.sub.refresh_from_db() self.assertEqual(self.sub.subscription_id, "") self.assertEqual(self.sub.plan_id, "") + + # User's profile should have standard limits + profile = Profile.objects.get(user=self.alice) + self.assertEqual(profile.ping_log_limit, 100) + self.assertEqual(profile.check_limit, 20) + self.assertFalse(profile.team_access_allowed) diff --git a/hc/payments/tests/test_create_plan.py b/hc/payments/tests/test_create_plan.py index 29f73765..be13164f 100644 --- a/hc/payments/tests/test_create_plan.py +++ b/hc/payments/tests/test_create_plan.py @@ -38,9 +38,11 @@ class CreatePlanTestCase(BaseTestCase): self.assertEqual(sub.subscription_id, "t-sub-id") self.assertEqual(sub.plan_id, "P5") - # User's profile should have a higher ping log limit: + # User's profile should have a higher limits profile = Profile.objects.get(user=self.alice) self.assertEqual(profile.ping_log_limit, 1000) + self.assertEqual(profile.check_limit, 500) + self.assertTrue(profile.team_access_allowed) # braintree.Subscription.cancel should have not been called assert not mock.Subscription.cancel.called diff --git a/hc/payments/views.py b/hc/payments/views.py index 3f85d7b6..c9e8726e 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -108,10 +108,12 @@ def create_plan(request): profile = request.user.profile if plan_id == "P5": profile.ping_log_limit = 1000 + profile.check_limit = 500 profile.team_access_allowed = True profile.save() elif plan_id == "P75": profile.ping_log_limit = 1000 + profile.check_limit = 500 profile.team_access_allowed = True profile.save() @@ -157,6 +159,14 @@ def update_payment_method(request): def cancel_plan(request): sub = Subscription.objects.get(user=request.user) sub.cancel() + + # Revert to default limits-- + profile = request.user.profile + profile.ping_log_limit = 100 + profile.check_limit = 20 + profile.team_access_allowed = False + profile.save() + return redirect("hc-pricing") diff --git a/templates/front/docs_api.html b/templates/front/docs_api.html index 19d3010e..f29a987a 100644 --- a/templates/front/docs_api.html +++ b/templates/front/docs_api.html @@ -191,6 +191,11 @@ To create a "cron" check, specify the "schedule" and "tz" parameters. Returned if the unique parameter was used and an existing check was matched. + + 403 Forbidden + Returned if the account's check limit has been reached. + For free accounts, the limit is 20 checks per account. +

Example Request

diff --git a/templates/front/my_checks.html b/templates/front/my_checks.html index 2c36dd63..5d0e5da2 100644 --- a/templates/front/my_checks.html +++ b/templates/front/my_checks.html @@ -42,10 +42,18 @@
+ {% if can_add_more %}
{% csrf_token %}
+ {% else %} +
+ Check limit reached. + To add more checks, please + upgrade your account! +
+ {% endif %}
diff --git a/templates/front/snippets/crontab.txt b/templates/front/snippets/crontab.txt index 1ff49e6f..64c6ecc4 100644 --- a/templates/front/snippets/crontab.txt +++ b/templates/front/snippets/crontab.txt @@ -1,2 +1,2 @@ -# m h dom mon dow command - 8 6 * * * /home/user/backup.sh && curl -fsS --retry 3 PING_URL > /dev/null \ No newline at end of file +# m h dom mon dow command + 8 6 * * * /home/user/backup.sh && curl -fsS --retry 3 PING_URL > /dev/null \ No newline at end of file diff --git a/templates/payments/pricing.html b/templates/payments/pricing.html index 8d58349c..1b750696 100644 --- a/templates/payments/pricing.html +++ b/templates/payments/pricing.html @@ -67,11 +67,10 @@

free

Premium Features

-

What is the "log entries / check" number?

+

What is the "log entries per check" number?

For each of your checks, healthchecks.io keeps a historic log of the received pings. The log can be useful