diff --git a/hc/accounts/admin.py b/hc/accounts/admin.py index 1f87c4d3..4b573b5b 100644 --- a/hc/accounts/admin.py +++ b/hc/accounts/admin.py @@ -1,10 +1,22 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User +from hc.accounts.models import Profile from hc.api.models import Channel, Check +@admin.register(Profile) +class ProfileAdmin(admin.ModelAdmin): + + list_display = ("id", "email", "reports_allowed", "next_report_date") + search_fields = ["user__email"] + + def email(self, obj): + return obj.user.email + + class HcUserAdmin(UserAdmin): + actions = ["send_report"] list_display = ('id', 'username', 'email', 'date_joined', 'involvement', 'is_staff') @@ -33,6 +45,13 @@ class HcUserAdmin(UserAdmin): involvement.allow_tags = True + def send_report(self, request, qs): + for user in qs: + profile = Profile.objects.for_user(user) + profile.send_report() + + self.message_user(request, "%d email(s) sent" % qs.count()) + admin.site.unregister(User) admin.site.register(User, HcUserAdmin) diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index d4738965..7936ade3 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -2,6 +2,7 @@ from django import forms class LowercaseEmailField(forms.EmailField): + def clean(self, value): value = super(LowercaseEmailField, self).clean(value) return value.lower() @@ -9,3 +10,7 @@ class LowercaseEmailField(forms.EmailField): class EmailForm(forms.Form): email = LowercaseEmailField() + + +class ReportSettingsForm(forms.Form): + reports_allowed = forms.BooleanField(required=False) diff --git a/hc/accounts/management/__init__.py b/hc/accounts/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hc/accounts/management/commands/__init__.py b/hc/accounts/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hc/accounts/management/commands/createprofiles.py b/hc/accounts/management/commands/createprofiles.py new file mode 100644 index 00000000..c1fbf7a8 --- /dev/null +++ b/hc/accounts/management/commands/createprofiles.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User +from hc.accounts.models import Profile + + +class Command(BaseCommand): + help = 'Make sure all users have profiles' + + def handle(self, *args, **options): + for user in User.objects.all(): + # this should create profile object if it does not exist + Profile.objects.for_user(user) + + print("Done.") diff --git a/hc/accounts/migrations/0001_initial.py b/hc/accounts/migrations/0001_initial.py new file mode 100644 index 00000000..a71ce40b --- /dev/null +++ b/hc/accounts/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)), + ('next_report_date', models.DateTimeField(null=True, blank=True)), + ('reports_allowed', models.BooleanField(default=True)), + ('user', models.OneToOneField(blank=True, to=settings.AUTH_USER_MODEL, null=True)), + ], + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 71a83623..086febd0 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -1,3 +1,47 @@ +from datetime import timedelta +from django.conf import settings +from django.contrib.auth.models import User +from django.core import signing +from django.core.urlresolvers import reverse from django.db import models +from django.utils import timezone +from hc.lib import emails +import uuid -# Create your models here. + +class ProfileManager(models.Manager): + + def for_user(self, user): + try: + profile = self.get(user_id=user.id) + except Profile.DoesNotExist: + profile = Profile(user=user) + profile.save() + + return profile + + +class Profile(models.Model): + user = models.OneToOneField(User, blank=True, null=True) + next_report_date = models.DateTimeField(null=True, blank=True) + reports_allowed = models.BooleanField(default=True) + + objects = ProfileManager() + + def send_report(self): + # reset next report date first: + now = timezone.now() + self.next_report_date = now + timedelta(days=30) + self.save() + + token = signing.Signer().sign(uuid.uuid4()) + path = reverse("hc-unsubscribe-reports", args=[self.user.username]) + unsub_link = "%s%s?token=%s" % (settings.SITE_ROOT, path, token) + + ctx = { + "checks": self.user.check_set.order_by("created"), + "now": now, + "unsub_link": unsub_link + } + + emails.report(self.user.email, ctx) diff --git a/hc/accounts/urls.py b/hc/accounts/urls.py index 679837c4..7ddc9631 100644 --- a/hc/accounts/urls.py +++ b/hc/accounts/urls.py @@ -2,8 +2,17 @@ from django.conf.urls import url from hc.accounts import views urlpatterns = [ - url(r'^login/$', views.login, name="hc-login"), - url(r'^logout/$', views.logout, name="hc-logout"), - url(r'^login_link_sent/$', views.login_link_sent, name="hc-login-link-sent"), - url(r'^check_token/([\w-]+)/([\w-]+)/$', views.check_token, name="hc-check-token"), + url(r'^login/$', views.login, name="hc-login"), + url(r'^logout/$', views.logout, name="hc-logout"), + url(r'^login_link_sent/$', + views.login_link_sent, name="hc-login-link-sent"), + + url(r'^check_token/([\w-]+)/([\w-]+)/$', + views.check_token, name="hc-check-token"), + + url(r'^profile/$', views.profile, name="hc-profile"), + + url(r'^unsubscribe_reports/([\w-]+)/$', + views.unsubscribe_reports, name="hc-unsubscribe-reports"), + ] diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 297969b9..12ea0bcd 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -1,14 +1,18 @@ import uuid from django.conf import settings +from django.contrib import messages from django.contrib.auth import login as auth_login from django.contrib.auth import logout as auth_logout from django.contrib.auth import authenticate +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User +from django.core import signing from django.core.urlresolvers import reverse from django.http import HttpResponseBadRequest from django.shortcuts import redirect, render -from hc.accounts.forms import EmailForm +from hc.accounts.forms import EmailForm, ReportSettingsForm +from hc.accounts.models import Profile from hc.api.models import Channel, Check from hc.lib import emails @@ -108,3 +112,35 @@ def check_token(request, username, token): ctx = {"bad_link": True} return render(request, "accounts/login.html", ctx) + + +@login_required +def profile(request): + profile = Profile.objects.for_user(request.user) + + if request.method == "POST": + form = ReportSettingsForm(request.POST) + if form.is_valid(): + profile.reports_allowed = form.cleaned_data["reports_allowed"] + profile.save() + messages.info(request, "Your settings have been updated!") + + ctx = { + "profile": profile + } + + return render(request, "accounts/profile.html", ctx) + + +def unsubscribe_reports(request, username): + try: + signing.Signer().unsign(request.GET.get("token")) + except signing.BadSignature: + return HttpResponseBadRequest() + + user = User.objects.get(username=username) + profile = Profile.objects.for_user(user) + profile.reports_allowed = False + profile.save() + + return render(request, "accounts/unsubscribed.html") diff --git a/hc/lib/emails.py b/hc/lib/emails.py index acd76a39..8ba8a6e9 100644 --- a/hc/lib/emails.py +++ b/hc/lib/emails.py @@ -14,3 +14,8 @@ def alert(to, ctx): def verify_email(to, ctx): o = InlineCSSTemplateMail("verify-email") o.send(to, ctx) + + +def report(to, ctx): + o = InlineCSSTemplateMail("report") + o.send(to, ctx) diff --git a/static/css/base.css b/static/css/base.css index 0bc73002..da3d363d 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -24,14 +24,13 @@ body { margin-top: -16px; } -.navbar-default .navbar-nav > li > a { +#nav-main-sections > li > a { text-transform: uppercase; font-size: small; } @media (min-width: 768px) { .navbar-default .navbar-nav > li.active > a, .navbar-default .navbar-nav > li > a:hover { - background: none; border-bottom: 5px solid #eee; padding-bottom: 25px; } diff --git a/static/css/settings.css b/static/css/settings.css new file mode 100644 index 00000000..37c0468f --- /dev/null +++ b/static/css/settings.css @@ -0,0 +1,12 @@ +#settings-title { + padding-bottom: 24px; +} + +.settings-block { + padding: 24px; +} + +.settings-block h2 { + margin: 0; + padding-bottom: 24px; +} \ No newline at end of file diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html new file mode 100644 index 00000000..67426cf1 --- /dev/null +++ b/templates/accounts/profile.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load compress staticfiles %} + +{% block title %}Account Settings - healthchecks.io{% endblock %} + + +{% block content %} +<div class="row"> + <div class="col-sm-12"> + <h1 id="settings-title">Settings</h1> + </div> + {% if messages %} + <div class="col-sm-12"> + {% for message in messages %} + <p class="alert alert-success">{{ message }}</p> + {% endfor %} + </div> + {% endif %} +</div> + +<div class="row"> + <div class="col-sm-6"> + <div class="panel panel-default"> + <div class="panel-body settings-block"> + <form method="post"> + {% csrf_token %} + <h2>Monthly Reports</h2> + <label> + <input + name="reports_allowed" + type="checkbox" + {% if profile.reports_allowed %} checked {% endif %}> + Each month send me a summary of my checks + </label> + <button + type="submit" + class="btn btn-default pull-right">Save</button> + </form> + </div> + </div> + </div> +</div> +{% endblock %} + diff --git a/templates/accounts/unsubscribed.html b/templates/accounts/unsubscribed.html new file mode 100644 index 00000000..0f2e4fe0 --- /dev/null +++ b/templates/accounts/unsubscribed.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block content %} +<div class="row"> + <div class="col-sm-6 col-sm-offset-3"> + <div class="hc-dialog"> + <h1>You have been unsubscribed</h1> + </div> + </div> +</div> + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 051453b7..022dd3f4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,6 +26,7 @@ <link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/log.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css"> + <link rel="stylesheet" href="{% static 'css/settings.css' %}" type="text/css"> {% endcompress %} </head> <body class="page-{{ page }}"> @@ -68,7 +69,7 @@ </div> <div id="navbar" class="navbar-collapse collapse"> - <ul class="nav navbar-nav"> + <ul id="nav-main-sections" class="nav navbar-nav"> {% if request.user.is_authenticated %} <li {% if page == 'checks' %} class="active" {% endif %}> <a href="{% url 'hc-checks' %}">Checks</a> @@ -105,10 +106,18 @@ {% endif %} {% if request.user.is_authenticated %} - <p class="navbar-text navbar-right"> - {{ request.user.email }} - <a href="{% url 'hc-logout' %}">Log Out</a> - </p> + <ul class="nav navbar-nav navbar-right"> + <li class="dropdown"> + <a id="nav-email" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"> + {{ request.user.email }} <span class="caret"></span> + </a> + <ul class="dropdown-menu"> + <li><a href="{% url 'hc-profile' %}">Settings</a></li> + <li role="separator" class="divider"></li> + <li><a href="{% url 'hc-logout' %}">Log Out</a></li> + </ul> + </li> + </ul> {% endif %} </div> diff --git a/templates/emails/alert-body-html.html b/templates/emails/alert-body-html.html index cc5c60ed..ba621494 100644 --- a/templates/emails/alert-body-html.html +++ b/templates/emails/alert-body-html.html @@ -66,6 +66,10 @@ {% else %} <span class="unnamed">unnamed</span> {% endif %} + {% if check.tags %} + <br /> + <small>{{ check.tags }}</small> + {% endif %} </td> <td class="url-cell"> <code>{{ check.url }}</code> diff --git a/templates/emails/report-body-html.html b/templates/emails/report-body-html.html new file mode 100644 index 00000000..6501426b --- /dev/null +++ b/templates/emails/report-body-html.html @@ -0,0 +1,102 @@ +{% load humanize hc_extras %} + +<style> + th { + text-align: left; + padding: 8px; + } + + td { + border-top: 1px solid #ddd; + padding: 8px; + } + + .badge { + font-size: 10px; + color: white; + padding: 4px; + font-family: sans; + } + + .new { background: #AAA; } + .paused { background: #AAA; } + .up { background: #5cb85c; } + .grace { background: #f0ad4e; } + .down { background: #d9534f; } + + + .unnamed { + color: #888; + font-style: italic; + } + +</style> + +<p>Hello,</p> +<p>This is a monthly report sent by <a href="https://healthchecks.io">healthchecks.io</a>.</p> + +<table> + <tr> + <th></th> + <th>Name</th> + <th>URL</th> + <th>Period</th> + <th>Last Ping</th> + </tr> + {% for check in checks %} + <tr> + <td> + {% if check.get_status == "new" %} + <span class="badge new">NEW</span> + {% elif check.get_status == "up" %} + <span class="badge up">UP</span> + {% elif check.get_status == "grace" %} + <span class="badge grace">LATE</span> + {% elif check.get_status == "down" %} + <span class="badge down">DOWN</span> + {% elif check.get_status == "paused" %} + <span class="badge paused">PAUSED</span> + {% endif %} + </td> + <td> + {% if check.name %} + {{ check.name }} + {% else %} + <span class="unnamed">unnamed</span> + {% endif %} + {% if check.tags %} + <br /> + <small>{{ check.tags }}</small> + {% endif %} + </td> + <td class="url-cell"> + <code>{{ check.url }}</code> + </td> + <td> + {{ check.timeout|hc_duration }} + </td> + <td> + {% if check.last_ping %} + {{ check.last_ping|naturaltime }} + {% else %} + Never + {% endif %} + </td> + </tr> + {% endfor %} +</table> + +<p><strong>Just one more thing to check:</strong> +Do you have more cron jobs, +not yet on this list, that would benefit from monitoring? +Get the ball rolling by adding one more!</p> + +<p> + --<br /> + Regards,<br /> + healthchecks.io +</p> + +<p> + <a href="{{ unsub_link }}">Unsubscribe from future monthly reports</a> +</p> diff --git a/templates/emails/report-subject.html b/templates/emails/report-subject.html new file mode 100644 index 00000000..743aaf34 --- /dev/null +++ b/templates/emails/report-subject.html @@ -0,0 +1,2 @@ +Monthly Report +