Browse Source

Per-profile, per-month counters of sent SMS messages.

pull/133/head
Pēteris Caune 7 years ago
parent
commit
5f2da08d7e
16 changed files with 202 additions and 10 deletions
  1. +6
    -2
      hc/accounts/admin.py
  2. +30
    -0
      hc/accounts/migrations/0009_auto_20170714_1734.py
  3. +17
    -0
      hc/accounts/migrations/0010_profile_sms_defaults.py
  4. +33
    -1
      hc/accounts/models.py
  5. +20
    -0
      hc/api/migrations/0033_auto_20170714_1715.py
  6. +44
    -0
      hc/api/tests/test_notify.py
  7. +5
    -0
      hc/api/transports.py
  8. +8
    -0
      hc/front/tests/test_add_sms.py
  9. +6
    -1
      hc/front/views.py
  10. +2
    -0
      hc/payments/tests/test_cancel_plan.py
  11. +11
    -4
      hc/payments/tests/test_create_plan.py
  12. +5
    -0
      hc/payments/views.py
  13. +1
    -0
      hc/test.py
  14. +1
    -1
      static/css/channels.css
  15. +4
    -1
      templates/front/channels.html
  16. +9
    -0
      templates/integrations/add_sms.html

+ 6
- 2
hc/accounts/admin.py View File

@ -26,7 +26,8 @@ class ProfileFieldset(Fieldset):
class TeamFieldset(Fieldset):
name = "Team"
fields = ("team_name", "team_access_allowed", "check_limit",
"ping_log_limit", "bill_to")
"ping_log_limit", "sms_limit", "sms_sent", "last_sms_date",
"bill_to")
@admin.register(Profile)
@ -41,7 +42,7 @@ class ProfileAdmin(admin.ModelAdmin):
raw_id_fields = ("current_team", )
list_select_related = ("user", )
list_display = ("id", "users", "checks", "team_access_allowed",
"reports_allowed", "ping_log_limit")
"reports_allowed", "ping_log_limit", "sms")
search_fields = ["id", "user__email"]
list_filter = ("team_access_allowed", "reports_allowed",
"check_limit", "next_report_date")
@ -68,6 +69,9 @@ class ProfileAdmin(admin.ModelAdmin):
  %d of %d
""" % (pct, num_checks, obj.check_limit)
def sms(self, obj):
return "%d of %d" % (obj.sms_sent, obj.sms_limit)
def email(self, obj):
return obj.user.email


+ 30
- 0
hc/accounts/migrations/0009_auto_20170714_1734.py View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-07-14 17:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0008_profile_bill_to'),
]
operations = [
migrations.AddField(
model_name='profile',
name='last_sms_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='profile',
name='sms_limit',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='profile',
name='sms_sent',
field=models.IntegerField(default=0),
),
]

+ 17
- 0
hc/accounts/migrations/0010_profile_sms_defaults.py View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-07-14 17:45
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0009_auto_20170714_1734'),
]
operations = [
migrations.RunSQL("ALTER TABLE accounts_profile ALTER COLUMN sms_sent SET DEFAULT 0"),
migrations.RunSQL("ALTER TABLE accounts_profile ALTER COLUMN sms_limit SET DEFAULT 0")
]

+ 33
- 1
hc/accounts/models.py View File

@ -13,14 +13,20 @@ from django.utils import timezone
from hc.lib import emails
def month(dt):
""" For a given datetime, return the matching first-day-of-month date. """
return dt.date().replace(day=1)
class ProfileManager(models.Manager):
def for_user(self, user):
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
# If not using payments, set high limits
profile.check_limit = 500
profile.sms_limit = 500
profile.save()
return profile
@ -39,6 +45,9 @@ class Profile(models.Model):
api_key = models.CharField(max_length=128, blank=True)
current_team = models.ForeignKey("self", models.SET_NULL, null=True)
bill_to = models.TextField(blank=True)
last_sms_date = models.DateTimeField(null=True, blank=True)
sms_limit = models.IntegerField(default=0)
sms_sent = models.IntegerField(default=0)
objects = ProfileManager()
@ -103,6 +112,29 @@ class Profile(models.Model):
user.profile.send_instant_login_link(self)
def sms_sent_this_month(self):
# IF last_sms_date was never set, we have not sent any messages yet.
if not self.last_sms_date:
return 0
# If last sent date is not from this month, we've sent 0 this month.
if month(timezone.now()) > month(self.last_sms_date):
return 0
return self.sms_sent
def authorize_sms(self):
""" If monthly limit not exceeded, increase counter and return True """
sent_this_month = self.sms_sent_this_month()
if sent_this_month >= self.sms_limit:
return False
self.sms_sent = sent_this_month + 1
self.last_sms_date = timezone.now()
self.save()
return True
class Member(models.Model):
team = models.ForeignKey(Profile, models.CASCADE)


+ 20
- 0
hc/api/migrations/0033_auto_20170714_1715.py View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-07-14 17:15
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0032_auto_20170608_1158'),
]
operations = [
migrations.AlterField(
model_name='channel',
name='kind',
field=models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('hipchat', 'HipChat'), ('slack', 'Slack'), ('pd', 'PagerDuty'), ('po', 'Pushover'), ('pushbullet', 'Pushbullet'), ('opsgenie', 'OpsGenie'), ('victorops', 'VictorOps'), ('discord', 'Discord'), ('telegram', 'Telegram'), ('sms', 'SMS')], max_length=20),
),
]

+ 44
- 0
hc/api/tests/test_notify.py View File

@ -310,3 +310,47 @@ class NotifyTestCase(BaseTestCase):
payload = kwargs["json"]
self.assertEqual(payload["chat_id"], 123)
self.assertTrue("The check" in payload["text"])
@patch("hc.api.transports.requests.request")
def test_sms(self, mock_post):
self._setup_data("sms", "+1234567890")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertEqual(payload["To"], "+1234567890")
# sent SMS counter should go up
self.profile.refresh_from_db()
self.assertEqual(self.profile.sms_sent, 1)
@patch("hc.api.transports.requests.request")
def test_sms_limit(self, mock_post):
# At limit already:
self.profile.last_sms_date = now()
self.profile.sms_sent = 50
self.profile.save()
self._setup_data("sms", "+1234567890")
self.channel.notify(self.check)
self.assertFalse(mock_post.called)
n = Notification.objects.get()
self.assertTrue("Monthly SMS limit exceeded" in n.error)
@patch("hc.api.transports.requests.request")
def test_sms_limit_reset(self, mock_post):
# At limit, but also into a new month
self.profile.sms_sent = 50
self.profile.last_sms_date = now() - td(days=100)
self.profile.save()
self._setup_data("sms", "+1234567890")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
self.assertTrue(mock_post.called)

+ 5
- 0
hc/api/transports.py View File

@ -5,6 +5,7 @@ import json
import requests
from six.moves.urllib.parse import quote
from hc.accounts.models import Profile
from hc.lib import emails
@ -305,6 +306,10 @@ class Sms(HttpTransport):
return check.status != "down"
def notify(self, check):
profile = Profile.objects.for_user(self.channel.user)
if not profile.authorize_sms():
return "Monthly SMS limit exceeded"
url = self.URL % settings.TWILIO_ACCOUNT
auth = (settings.TWILIO_ACCOUNT, settings.TWILIO_AUTH)
text = tmpl("sms_message.html", check=check,


+ 8
- 0
hc/front/tests/test_add_sms.py View File

@ -12,6 +12,14 @@ class AddSmsTestCase(BaseTestCase):
r = self.client.get(self.url)
self.assertContains(r, "Get a SMS message")
def test_it_warns_about_limits(self):
self.profile.sms_limit = 0
self.profile.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "upgrade to a")
def test_it_creates_channel(self):
form = {"value": "+1234567890"}


+ 6
- 1
hc/front/views.py View File

@ -346,6 +346,7 @@ def channels(request):
ctx = {
"page": "channels",
"profile": request.team,
"channels": channels,
"num_checks": num_checks,
"enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
@ -830,7 +831,11 @@ def add_sms(request):
else:
form = AddSmsForm()
ctx = {"page": "channels", "form": form}
ctx = {
"page": "channels",
"form": form,
"profile": request.team
}
return render(request, "integrations/add_sms.html", ctx)


+ 2
- 0
hc/payments/tests/test_cancel_plan.py View File

@ -16,6 +16,7 @@ class CancelPlanTestCase(BaseTestCase):
self.profile.ping_log_limit = 1000
self.profile.check_limit = 500
self.profile.sms_limit = 50
self.profile.save()
@patch("hc.payments.models.braintree")
@ -33,4 +34,5 @@ class CancelPlanTestCase(BaseTestCase):
profile = Profile.objects.get(user=self.alice)
self.assertEqual(profile.ping_log_limit, 100)
self.assertEqual(profile.check_limit, 20)
self.assertEqual(profile.sms_limit, 0)
self.assertFalse(profile.team_access_allowed)

+ 11
- 4
hc/payments/tests/test_create_plan.py View File

@ -28,6 +28,11 @@ class CreatePlanTestCase(BaseTestCase):
def test_it_works(self, mock):
self._setup_mock(mock)
self.profile.team_access_allowed = False
self.profile.sms_limit = 0
self.profile.sms_sent = 1
self.profile.save()
r = self.run_create_plan()
self.assertRedirects(r, "/pricing/")
@ -39,10 +44,12 @@ class CreatePlanTestCase(BaseTestCase):
self.assertEqual(sub.plan_id, "P5")
# 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)
self.profile.refresh_from_db()
self.assertEqual(self.profile.ping_log_limit, 1000)
self.assertEqual(self.profile.check_limit, 500)
self.assertEqual(self.profile.sms_limit, 50)
self.assertEqual(self.profile.sms_sent, 0)
self.assertTrue(self.profile.team_access_allowed)
# braintree.Subscription.cancel should have not been called
assert not mock.Subscription.cancel.called


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

@ -109,11 +109,15 @@ def create_plan(request):
if plan_id == "P5":
profile.ping_log_limit = 1000
profile.check_limit = 500
profile.sms_limit = 50
profile.sms_sent = 0
profile.team_access_allowed = True
profile.save()
elif plan_id == "P75":
profile.ping_log_limit = 1000
profile.check_limit = 500
profile.sms_limit = 500
profile.sms_sent = 0
profile.team_access_allowed = True
profile.save()
@ -164,6 +168,7 @@ def cancel_plan(request):
profile = request.user.profile
profile.ping_log_limit = 100
profile.check_limit = 20
profile.sms_limit = 0
profile.team_access_allowed = False
profile.save()


+ 1
- 0
hc/test.py View File

@ -16,6 +16,7 @@ class BaseTestCase(TestCase):
self.profile = Profile(user=self.alice, api_key="abc")
self.profile.team_access_allowed = True
self.profile.sms_limit = 50
self.profile.save()
# Bob is on Alice's team and should have access to her stuff


+ 1
- 1
static/css/channels.css View File

@ -3,7 +3,7 @@
}
.channels-table .channel-row > td {
line-height: 40px;
padding: 10px 0;
}
.channels-table .value-cell {


+ 4
- 1
templates/front/channels.html View File

@ -113,6 +113,9 @@
{% else %}
Never
{% endif %}
{% if ch.kind == "sms" %}
<p>Used {{ profile.sms_sent_this_month }} of {{ profile.sms_limit }} sends this month.</p>
{% endif %}
{% endwith %}
</td>
<td>
@ -157,7 +160,7 @@
<img src="{% static 'img/integrations/sms.png' %}"
class="icon" alt="SMS icon" />
<h2>SMS</h2>
<h2>SMS {% if show_pricing %}<small>(paid plans)</small>{% endif %}</h2>
<p>Get a text message to your phone when check goes down.</p>
<a href="{% url 'hc-add-sms' %}" class="btn btn-primary">Add Integration</a>


+ 9
- 0
templates/integrations/add_sms.html View File

@ -11,6 +11,15 @@
<p>Get a SMS message to your specified number when check goes down.</p>
{% if show_pricing and profile.sms_limit == 0 %}
<p class="alert alert-success">
<strong>Paid plan required.</strong>
SMS messaging is not available on the free plan–sending the messages
costs too much! Please upgrade to a
<a href="{% url 'hc-pricing' %}">paid plan</a> to enable SMS messaging.
</p>
{% endif %}
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal" action="{% url 'hc-add-sms' %}">


Loading…
Cancel
Save