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 %} +
+
+

Settings

+
+ {% if messages %} +
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+ {% endif %} +
+ +
+
+
+
+
+ {% csrf_token %} +

Monthly Reports

+ + +
+
+
+
+
+{% 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 %} +
+
+
+

You have been unsubscribed

+
+
+
+ +{% 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 @@ + {% endcompress %} @@ -68,7 +69,7 @@ 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 %} unnamed {% endif %} + {% if check.tags %} +
+ {{ check.tags }} + {% endif %} {{ check.url }} 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 %} + + + +

Hello,

+

This is a monthly report sent by healthchecks.io.

+ + + + + + + + + + {% for check in checks %} + + + + + + + + {% endfor %} +
NameURLPeriodLast Ping
+ {% if check.get_status == "new" %} + NEW + {% elif check.get_status == "up" %} + UP + {% elif check.get_status == "grace" %} + LATE + {% elif check.get_status == "down" %} + DOWN + {% elif check.get_status == "paused" %} + PAUSED + {% endif %} + + {% if check.name %} + {{ check.name }} + {% else %} + unnamed + {% endif %} + {% if check.tags %} +
+ {{ check.tags }} + {% endif %} +
+ {{ check.url }} + + {{ check.timeout|hc_duration }} + + {% if check.last_ping %} + {{ check.last_ping|naturaltime }} + {% else %} + Never + {% endif %} +
+ +

Just one more thing to check: +Do you have more cron jobs, +not yet on this list, that would benefit from monitoring? +Get the ball rolling by adding one more!

+ +

+ --
+ Regards,
+ healthchecks.io +

+ +

+ Unsubscribe from future monthly reports +

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 +