@ -0,0 +1,26 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Generated by Django 1.11.6 on 2017-10-14 10:02 | |||||
from __future__ import unicode_literals | |||||
import datetime | |||||
from django.db import migrations, models | |||||
class Migration(migrations.Migration): | |||||
dependencies = [ | |||||
('accounts', '0011_profile_sort'), | |||||
] | |||||
operations = [ | |||||
migrations.AddField( | |||||
model_name='profile', | |||||
name='nag_period', | |||||
field=models.DurationField(choices=[(datetime.timedelta(0), 'Disabled'), (datetime.timedelta(0, 3600), 'Hourly'), (datetime.timedelta(1), 'Daily')], default=datetime.timedelta(0)), | |||||
), | |||||
migrations.AddField( | |||||
model_name='profile', | |||||
name='next_nag_date', | |||||
field=models.DateTimeField(blank=True, null=True), | |||||
), | |||||
] |
@ -1,24 +1,60 @@ | |||||
from datetime import timedelta as td | |||||
from django.utils.timezone import now | |||||
from hc.test import BaseTestCase | from hc.test import BaseTestCase | ||||
class NotificationsTestCase(BaseTestCase): | class NotificationsTestCase(BaseTestCase): | ||||
def test_it_saves_reports_allowed_true(self): | def test_it_saves_reports_allowed_true(self): | ||||
self.profile.reports_allowed = False | |||||
self.profile.save() | |||||
self.client.login(username="[email protected]", password="password") | self.client.login(username="[email protected]", password="password") | ||||
form = {"reports_allowed": "on"} | |||||
form = {"reports_allowed": "on", "nag_period": "0"} | |||||
r = self.client.post("/accounts/profile/notifications/", form) | r = self.client.post("/accounts/profile/notifications/", form) | ||||
assert r.status_code == 200 | |||||
self.assertEqual(r.status_code, 200) | |||||
self.alice.profile.refresh_from_db() | |||||
self.assertTrue(self.alice.profile.reports_allowed) | |||||
self.profile.refresh_from_db() | |||||
self.assertTrue(self.profile.reports_allowed) | |||||
self.assertIsNotNone(self.profile.next_report_date) | |||||
def test_it_saves_reports_allowed_false(self): | def test_it_saves_reports_allowed_false(self): | ||||
self.profile.reports_allowed = True | |||||
self.profile.next_report_date = now() | |||||
self.profile.save() | |||||
self.client.login(username="[email protected]", password="password") | |||||
form = {"nag_period": "0"} | |||||
r = self.client.post("/accounts/profile/notifications/", form) | |||||
self.assertEqual(r.status_code, 200) | |||||
self.profile.refresh_from_db() | |||||
self.assertFalse(self.profile.reports_allowed) | |||||
self.assertIsNone(self.profile.next_report_date) | |||||
def test_it_saves_hourly_nag_period(self): | |||||
self.client.login(username="[email protected]", password="password") | |||||
form = {"nag_period": "3600"} | |||||
r = self.client.post("/accounts/profile/notifications/", form) | |||||
self.assertEqual(r.status_code, 200) | |||||
self.profile.refresh_from_db() | |||||
self.assertEqual(self.profile.nag_period.total_seconds(), 3600) | |||||
self.assertIsNotNone(self.profile.next_nag_date) | |||||
def test_it_does_not_save_nonstandard_nag_period(self): | |||||
self.profile.nag_period = td(seconds=3600) | |||||
self.profile.save() | |||||
self.client.login(username="[email protected]", password="password") | self.client.login(username="[email protected]", password="password") | ||||
form = {} | |||||
form = {"nag_period": "1234"} | |||||
r = self.client.post("/accounts/profile/notifications/", form) | r = self.client.post("/accounts/profile/notifications/", form) | ||||
assert r.status_code == 200 | |||||
self.assertEqual(r.status_code, 200) | |||||
self.alice.profile.refresh_from_db() | |||||
self.assertFalse(self.alice.profile.reports_allowed) | |||||
self.profile.refresh_from_db() | |||||
self.assertEqual(self.profile.nag_period.total_seconds(), 3600) |
@ -1,9 +1,11 @@ | |||||
from datetime import timedelta as td | |||||
from django.core import mail | from django.core import mail | ||||
from django.conf import settings | |||||
from django.utils.timezone import now | |||||
from hc.test import BaseTestCase | from hc.test import BaseTestCase | ||||
from hc.accounts.models import Member | from hc.accounts.models import Member | ||||
from hc.api.models import Check | from hc.api.models import Check | ||||
from django.conf import settings | |||||
class ProfileTestCase(BaseTestCase): | class ProfileTestCase(BaseTestCase): | ||||
@ -16,8 +18,8 @@ class ProfileTestCase(BaseTestCase): | |||||
assert r.status_code == 302 | assert r.status_code == 302 | ||||
# profile.token should be set now | # profile.token should be set now | ||||
self.alice.profile.refresh_from_db() | |||||
token = self.alice.profile.token | |||||
self.profile.refresh_from_db() | |||||
token = self.profile.token | |||||
self.assertTrue(len(token) > 10) | self.assertTrue(len(token) > 10) | ||||
# And an email should have been sent | # And an email should have been sent | ||||
@ -32,8 +34,8 @@ class ProfileTestCase(BaseTestCase): | |||||
r = self.client.post("/accounts/profile/", form) | r = self.client.post("/accounts/profile/", form) | ||||
self.assertEqual(r.status_code, 200) | self.assertEqual(r.status_code, 200) | ||||
self.alice.profile.refresh_from_db() | |||||
api_key = self.alice.profile.api_key | |||||
self.profile.refresh_from_db() | |||||
api_key = self.profile.api_key | |||||
self.assertTrue(len(api_key) > 10) | self.assertTrue(len(api_key) > 10) | ||||
def test_it_revokes_api_key(self): | def test_it_revokes_api_key(self): | ||||
@ -43,14 +45,16 @@ class ProfileTestCase(BaseTestCase): | |||||
r = self.client.post("/accounts/profile/", form) | r = self.client.post("/accounts/profile/", form) | ||||
assert r.status_code == 200 | assert r.status_code == 200 | ||||
self.alice.profile.refresh_from_db() | |||||
self.assertEqual(self.alice.profile.api_key, "") | |||||
self.profile.refresh_from_db() | |||||
self.assertEqual(self.profile.api_key, "") | |||||
def test_it_sends_report(self): | def test_it_sends_report(self): | ||||
check = Check(name="Test Check", user=self.alice) | check = Check(name="Test Check", user=self.alice) | ||||
check.last_ping = now() | |||||
check.save() | check.save() | ||||
self.alice.profile.send_report() | |||||
sent = self.profile.send_report() | |||||
self.assertTrue(sent) | |||||
# And an email should have been sent | # And an email should have been sent | ||||
self.assertEqual(len(mail.outbox), 1) | self.assertEqual(len(mail.outbox), 1) | ||||
@ -59,6 +63,38 @@ class ProfileTestCase(BaseTestCase): | |||||
self.assertEqual(message.subject, 'Monthly Report') | self.assertEqual(message.subject, 'Monthly Report') | ||||
self.assertIn("Test Check", message.body) | self.assertIn("Test Check", message.body) | ||||
def test_it_sends_nag(self): | |||||
check = Check(name="Test Check", user=self.alice) | |||||
check.status = "down" | |||||
check.last_ping = now() | |||||
check.save() | |||||
self.profile.nag_period = td(hours=1) | |||||
self.profile.save() | |||||
sent = self.profile.send_report(nag=True) | |||||
self.assertTrue(sent) | |||||
# And an email should have been sent | |||||
self.assertEqual(len(mail.outbox), 1) | |||||
message = mail.outbox[0] | |||||
self.assertEqual(message.subject, 'Reminder: 1 check still down') | |||||
self.assertIn("Test Check", message.body) | |||||
def test_it_skips_nag_if_none_down(self): | |||||
check = Check(name="Test Check", user=self.alice) | |||||
check.last_ping = now() | |||||
check.save() | |||||
self.profile.nag_period = td(hours=1) | |||||
self.profile.save() | |||||
sent = self.profile.send_report(nag=True) | |||||
self.assertFalse(sent) | |||||
self.assertEqual(len(mail.outbox), 0) | |||||
def test_it_adds_team_member(self): | def test_it_adds_team_member(self): | ||||
self.client.login(username="[email protected]", password="password") | self.client.login(username="[email protected]", password="password") | ||||
@ -67,7 +103,7 @@ class ProfileTestCase(BaseTestCase): | |||||
self.assertEqual(r.status_code, 200) | self.assertEqual(r.status_code, 200) | ||||
member_emails = set() | member_emails = set() | ||||
for member in self.alice.profile.member_set.all(): | |||||
for member in self.profile.member_set.all(): | |||||
member_emails.add(member.user.email) | member_emails.add(member.user.email) | ||||
self.assertEqual(len(member_emails), 2) | self.assertEqual(len(member_emails), 2) | ||||
@ -107,8 +143,8 @@ class ProfileTestCase(BaseTestCase): | |||||
r = self.client.post("/accounts/profile/", form) | r = self.client.post("/accounts/profile/", form) | ||||
self.assertEqual(r.status_code, 200) | self.assertEqual(r.status_code, 200) | ||||
self.alice.profile.refresh_from_db() | |||||
self.assertEqual(self.alice.profile.team_name, "Alpha Team") | |||||
self.profile.refresh_from_db() | |||||
self.assertEqual(self.profile.team_name, "Alpha Team") | |||||
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") | ||||
@ -128,8 +164,8 @@ class ProfileTestCase(BaseTestCase): | |||||
assert r.status_code == 302 | assert r.status_code == 302 | ||||
# profile.token should be set now | # profile.token should be set now | ||||
self.alice.profile.refresh_from_db() | |||||
token = self.alice.profile.token | |||||
self.profile.refresh_from_db() | |||||
token = self.profile.token | |||||
self.assertTrue(len(token) > 10) | self.assertTrue(len(token) > 10) | ||||
# And an email should have been sent | # And an email should have been sent | ||||
@ -0,0 +1,67 @@ | |||||
/* Customize the label (the container) */ | |||||
.checkbox-container { | |||||
display: block; | |||||
position: relative; | |||||
padding-left: 30px; | |||||
margin-left: 20px; | |||||
margin-bottom: 12px; | |||||
cursor: pointer; | |||||
-webkit-user-select: none; | |||||
-moz-user-select: none; | |||||
-ms-user-select: none; | |||||
user-select: none; | |||||
font-weight: normal; | |||||
} | |||||
/* Hide the browser's default checkbox */ | |||||
.checkbox-container input { | |||||
position: absolute; | |||||
opacity: 0; | |||||
} | |||||
/* Create a custom checkbox */ | |||||
.checkmark { | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
height: 20px; | |||||
width: 20px; | |||||
border-radius: 3px; | |||||
border: 1px solid #DDD; | |||||
} | |||||
/* On mouse-over tint the border */ | |||||
.checkmark:hover { | |||||
border-color: #5db4ea; | |||||
} | |||||
/* When the checkbox is checked, add a colored background */ | |||||
.checkbox-container input:checked ~ .checkmark { | |||||
border-color: #0091EA; | |||||
background-color: #0091EA; | |||||
} | |||||
/* Create the checkmark/indicator (hidden when not checked) */ | |||||
.checkmark:after { | |||||
content: ""; | |||||
position: absolute; | |||||
display: none; | |||||
} | |||||
/* Show the checkmark when checked */ | |||||
.checkbox-container input:checked ~ .checkmark:after { | |||||
display: block; | |||||
} | |||||
/* Style the checkmark/indicator */ | |||||
.checkbox-container .checkmark:after { | |||||
left: 7px; | |||||
top: 3px; | |||||
width: 5px; | |||||
height: 10px; | |||||
border: solid white; | |||||
border-width: 0 2px 2px 0; | |||||
-webkit-transform: rotate(45deg); | |||||
-ms-transform: rotate(45deg); | |||||
transform: rotate(45deg); | |||||
} |
@ -0,0 +1,64 @@ | |||||
/* Customize the label (the container) */ | |||||
.radio-container { | |||||
display: block; | |||||
position: relative; | |||||
padding-left: 30px; | |||||
margin-bottom: 12px; | |||||
margin-left: 20px; | |||||
cursor: pointer; | |||||
-webkit-user-select: none; | |||||
-moz-user-select: none; | |||||
-ms-user-select: none; | |||||
user-select: none; | |||||
font-weight: normal; | |||||
} | |||||
/* Hide the browser's default radio button */ | |||||
.radio-container input { | |||||
position: absolute; | |||||
opacity: 0; | |||||
} | |||||
/* Create a custom radio button */ | |||||
.radiomark { | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
height: 20px; | |||||
width: 20px; | |||||
border-radius: 50%; | |||||
border: 1px solid #DDD; | |||||
} | |||||
/* On mouse-over, tint the border */ | |||||
.radiomark:hover { | |||||
border-color: #5db4ea; | |||||
} | |||||
/* When the radio button is checked, add a colored background */ | |||||
.radio-container input:checked ~ .radiomark { | |||||
border-color: #0091EA; | |||||
background-color: #0091EA; | |||||
} | |||||
/* Create the indicator (the dot/circle - hidden when not checked) */ | |||||
.radiomark:after { | |||||
content: ""; | |||||
position: absolute; | |||||
display: none; | |||||
} | |||||
/* Show the indicator (dot/circle) when checked */ | |||||
.radio-container input:checked ~ .radiomark:after { | |||||
display: block; | |||||
} | |||||
/* Style the indicator (dot/circle) */ | |||||
.radio-container .radiomark:after { | |||||
top: 6px; | |||||
left: 6px; | |||||
width: 6px; | |||||
height: 6px; | |||||
border-radius: 50%; | |||||
background: white; | |||||
} |
@ -1,2 +1,6 @@ | |||||
Monthly Report | |||||
{% if nag %} | |||||
Reminder: {{ num_down }} check{{ num_down|pluralize }} still down | |||||
{% else %} | |||||
Monthly Report | |||||
{% endif %} | |||||