aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rpkid/rpki/gui/app/check_expired.py22
-rw-r--r--rpkid/rpki/gui/app/glue.py22
-rw-r--r--rpkid/rpki/gui/app/migrations/0008_add_alerts.py176
-rw-r--r--rpkid/rpki/gui/app/models.py52
-rw-r--r--rpkid/rpki/gui/app/templates/app/alert_confirm_delete.html17
-rw-r--r--rpkid/rpki/gui/app/templates/app/alert_detail.html31
-rw-r--r--rpkid/rpki/gui/app/templates/app/alert_list.html26
-rw-r--r--rpkid/rpki/gui/app/templates/app/app_base.html4
-rw-r--r--rpkid/rpki/gui/app/templatetags/app_extras.py27
-rw-r--r--rpkid/rpki/gui/app/urls.py5
-rw-r--r--rpkid/rpki/gui/app/views.py31
-rw-r--r--rpkid/rpki/gui/cacheview/util.py7
12 files changed, 378 insertions, 42 deletions
diff --git a/rpkid/rpki/gui/app/check_expired.py b/rpkid/rpki/gui/app/check_expired.py
index 418e031a..fcf5ecae 100644
--- a/rpkid/rpki/gui/app/check_expired.py
+++ b/rpkid/rpki/gui/app/check_expired.py
@@ -22,8 +22,8 @@ import logging
import datetime
from rpki.gui.cacheview.models import Cert
-from rpki.gui.app.models import Conf, ResourceCert, Timestamp
-from rpki.gui.app.glue import list_received_resources, get_email_list
+from rpki.gui.app.models import Conf, ResourceCert, Timestamp, Alert
+from rpki.gui.app.glue import list_received_resources
from rpki.irdb import Zookeeper
from rpki.left_right import report_error_elt, list_published_objects_elt
from rpki.x509 import X509
@@ -200,14 +200,10 @@ def notify_expired(expire_days=14, from_email=None):
if s:
logger.info(s)
- notify_emails = get_email_list(h)
-
- if notify_emails:
- t = """This is an automated notice about the upcoming expiration of RPKI resources for the handle %s on %s. You are receiving this notification because your email address is either registered in a Ghostbuster record, or as the default email address for the account.\n\n""" % (h.handle, host)
-
- send_mail(
- subject='RPKI expiration notice for %s' % h.handle,
- message=t + s,
- from_email=from_email,
- recipient_list=notify_emails
- )
+ t = """This is an automated notice about the upcoming expiration of RPKI resources for the handle %s on %s. You are receiving this notification because your email address is either registered in a Ghostbuster record, or as the default email address for the account.\n\n""" % (h.handle, host)
+ h.send_alert(
+ subject='RPKI expiration notice for %s' % h.handle,
+ message=t + s,
+ from_email=from_email,
+ severity=Alert.WARNING
+ )
diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py
index 03225de7..f6ec4344 100644
--- a/rpkid/rpki/gui/app/glue.py
+++ b/rpkid/rpki/gui/app/glue.py
@@ -108,25 +108,3 @@ def list_received_resources(log, conf):
prefix_max=rng.max)
else:
print >>log, "error: unexpected pdu from rpkid type=%s" % type(pdu)
-
-
-def get_email_list(conf):
- """Return a list of the contact emails for this user.
-
- Contact emails are extract from any ghostbuster requests, and if there are
- none, returns the default email for the web portal account.
-
- """
- notify_emails = []
- qs = models.GhostbusterRequest.objects.filter(issuer=conf)
- for gbr in qs:
- if gbr.email_address:
- notify_emails.append(gbr.email_address)
-
- if len(notify_emails) == 0:
- # fall back to the email address registered for this user
- user = User.objects.get(username=conf.handle)
- if user.email:
- notify_emails.append(user.email)
-
- return notify_emails
diff --git a/rpkid/rpki/gui/app/migrations/0008_add_alerts.py b/rpkid/rpki/gui/app/migrations/0008_add_alerts.py
new file mode 100644
index 00000000..77af68d2
--- /dev/null
+++ b/rpkid/rpki/gui/app/migrations/0008_add_alerts.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'Alert'
+ db.create_table('app_alert', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('conf', self.gf('django.db.models.fields.related.ForeignKey')(related_name='alerts', to=orm['irdb.ResourceHolderCA'])),
+ ('severity', self.gf('django.db.models.fields.SmallIntegerField')(default=0)),
+ ('when', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('seen', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('subject', self.gf('django.db.models.fields.CharField')(max_length=66)),
+ ('text', self.gf('django.db.models.fields.TextField')()),
+ ))
+ db.send_create_signal('app', ['Alert'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'Alert'
+ db.delete_table('app_alert')
+
+
+ models = {
+ 'app.alert': {
+ 'Meta': {'object_name': 'Alert'},
+ 'conf': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'alerts'", 'to': "orm['irdb.ResourceHolderCA']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'seen': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'severity': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'subject': ('django.db.models.fields.CharField', [], {'max_length': '66'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'app.confacl': {
+ 'Meta': {'unique_together': "(('user', 'conf'),)", 'object_name': 'ConfACL'},
+ 'conf': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['irdb.ResourceHolderCA']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'app.ghostbusterrequest': {
+ 'Meta': {'ordering': "('family_name', 'given_name')", 'object_name': 'GhostbusterRequest', '_ormbases': ['irdb.GhostbusterRequest']},
+ 'additional_name': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
+ 'box': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'code': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'extended': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'family_name': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
+ 'full_name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'ghostbusterrequest_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['irdb.GhostbusterRequest']", 'unique': 'True', 'primary_key': 'True'}),
+ 'given_name': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
+ 'honorific_prefix': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'honorific_suffix': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'organization': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'region': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'street': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'telephone': ('rpki.gui.app.models.TelephoneField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'})
+ },
+ 'app.resourcecert': {
+ 'Meta': {'object_name': 'ResourceCert'},
+ 'conf': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'certs'", 'to': "orm['irdb.ResourceHolderCA']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'not_after': ('django.db.models.fields.DateTimeField', [], {}),
+ 'not_before': ('django.db.models.fields.DateTimeField', [], {}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'certs'", 'null': 'True', 'to': "orm['irdb.Parent']"}),
+ 'uri': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'app.resourcerangeaddressv4': {
+ 'Meta': {'ordering': "('prefix_min',)", 'object_name': 'ResourceRangeAddressV4'},
+ 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'address_ranges'", 'to': "orm['app.ResourceCert']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'prefix_max': ('rpki.gui.models.IPv4AddressField', [], {'db_index': 'True'}),
+ 'prefix_min': ('rpki.gui.models.IPv4AddressField', [], {'db_index': 'True'})
+ },
+ 'app.resourcerangeaddressv6': {
+ 'Meta': {'ordering': "('prefix_min',)", 'object_name': 'ResourceRangeAddressV6'},
+ 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'address_ranges_v6'", 'to': "orm['app.ResourceCert']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'prefix_max': ('rpki.gui.models.IPv6AddressField', [], {'db_index': 'True'}),
+ 'prefix_min': ('rpki.gui.models.IPv6AddressField', [], {'db_index': 'True'})
+ },
+ 'app.resourcerangeas': {
+ 'Meta': {'ordering': "('min', 'max')", 'object_name': 'ResourceRangeAS'},
+ 'cert': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'asn_ranges'", 'to': "orm['app.ResourceCert']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'max': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'min': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'app.timestamp': {
+ 'Meta': {'object_name': 'Timestamp'},
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}),
+ 'ts': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'irdb.ghostbusterrequest': {
+ 'Meta': {'object_name': 'GhostbusterRequest'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ghostbuster_requests'", 'to': "orm['irdb.ResourceHolderCA']"}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ghostbuster_requests'", 'null': 'True', 'to': "orm['irdb.Parent']"}),
+ 'vcard': ('django.db.models.fields.TextField', [], {})
+ },
+ 'irdb.parent': {
+ 'Meta': {'unique_together': "(('issuer', 'handle'),)", 'object_name': 'Parent', '_ormbases': ['irdb.Turtle']},
+ 'certificate': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}),
+ 'child_handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}),
+ 'handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}),
+ 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parents'", 'to': "orm['irdb.ResourceHolderCA']"}),
+ 'parent_handle': ('rpki.irdb.models.HandleField', [], {'max_length': '120'}),
+ 'referral_authorization': ('rpki.irdb.models.SignedReferralField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'referrer': ('rpki.irdb.models.HandleField', [], {'max_length': '120', 'null': 'True', 'blank': 'True'}),
+ 'repository_type': ('rpki.irdb.models.EnumField', [], {}),
+ 'ta': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}),
+ 'turtle_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['irdb.Turtle']", 'unique': 'True', 'primary_key': 'True'})
+ },
+ 'irdb.resourceholderca': {
+ 'Meta': {'object_name': 'ResourceHolderCA'},
+ 'certificate': ('rpki.irdb.models.CertificateField', [], {'default': 'None', 'blank': 'True'}),
+ 'handle': ('rpki.irdb.models.HandleField', [], {'unique': 'True', 'max_length': '120'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_crl_update': ('rpki.irdb.models.SundialField', [], {}),
+ 'latest_crl': ('rpki.irdb.models.CRLField', [], {'default': 'None', 'blank': 'True'}),
+ 'next_crl_number': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}),
+ 'next_crl_update': ('rpki.irdb.models.SundialField', [], {}),
+ 'next_serial': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}),
+ 'private_key': ('rpki.irdb.models.RSAKeyField', [], {'default': 'None', 'blank': 'True'})
+ },
+ 'irdb.turtle': {
+ 'Meta': {'object_name': 'Turtle'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'service_uri': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['app'] \ No newline at end of file
diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py
index 5e74d27c..5b33be5e 100644
--- a/rpkid/rpki/gui/app/models.py
+++ b/rpkid/rpki/gui/app/models.py
@@ -17,6 +17,7 @@ __version__ = '$Id$'
from django.db import models
from django.contrib.auth.models import User
+from django.core.mail import send_mail
import rpki.resource_set
import rpki.exceptions
@@ -84,6 +85,31 @@ class ChildNet(rpki.irdb.models.ChildNet):
return u'%s' % self.as_resource_range()
+class Alert(models.Model):
+ """Stores alert messages intended to be consumed by the user."""
+
+ INFO = 0
+ WARNING = 1
+ ERROR = 2
+
+ SEVERITY_CHOICES = (
+ (INFO, 'info'),
+ (WARNING, 'warning'),
+ (ERROR, 'error'),
+ )
+
+ conf = models.ForeignKey('Conf', related_name='alerts')
+ severity = models.SmallIntegerField(choices=SEVERITY_CHOICES, default=INFO)
+ when = models.DateTimeField(auto_now_add=True)
+ seen = models.BooleanField(default=False)
+ subject = models.CharField(max_length=66)
+ text = models.TextField()
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('alert-detail', [str(self.pk)])
+
+
class Conf(rpki.irdb.models.ResourceHolderCA):
"""This is the center of the universe, also known as a place to
have a handle on a resource-holding entity. It's the <self>
@@ -146,6 +172,32 @@ class Conf(rpki.irdb.models.ResourceHolderCA):
prefix_max__lte=p.prefix_max)
return RouteOriginV6.objects.filter(q)
+ def send_alert(self, subject, message, from_email, severity=Alert.INFO):
+ """Store an alert for this resource holder."""
+ self.alerts.create(subject=subject, text=message, severity=severity)
+
+ send_mail(
+ subject=subject,
+ message=message,
+ from_email=from_email,
+ recipient_list=self.email_list
+ )
+
+ @property
+ def email_list(self):
+ """Return a list of the contact emails for this resource holder.
+
+ Contact emails are extract from any ghostbuster requests, and any
+ linked user accounts.
+
+ """
+ notify_emails = [gbr.email_address for gbr in self.ghostbusters if gbr.email_address]
+ notify_emails.extend(
+ [acl.user.email for acl in ConfACL.objects.filter(conf=self) if acl.user.email]
+ )
+ return notify_emails
+
+
class Meta:
proxy = True
diff --git a/rpkid/rpki/gui/app/templates/app/alert_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/alert_confirm_delete.html
new file mode 100644
index 00000000..78c84917
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/alert_confirm_delete.html
@@ -0,0 +1,17 @@
+{% extends "app/alert_detail.html" %}
+{% load url from future %}
+
+{% block action %}
+<div class="row-fluid">
+ <div class="alert">
+ Please confirm that you would like to delete this alert.
+ </div>
+ <form method="POST">
+ {% csrf_token %}
+ <div class="form-actions">
+ <button class="btn btn-danger" type="submit"><i class="icon-trash"></i> Delete</button>
+ <a class="btn" href="{{ object.get_absolute_url }}">Cancel</a>
+ </div>
+ </form>
+</div>
+{% endblock action %}
diff --git a/rpkid/rpki/gui/app/templates/app/alert_detail.html b/rpkid/rpki/gui/app/templates/app/alert_detail.html
new file mode 100644
index 00000000..b3a73b7e
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/alert_detail.html
@@ -0,0 +1,31 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+{% load app_extras %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Alert Detail <small>{{ object.subject }}</small></h1>
+</div>
+
+<div class="row-fluid">
+<table class="table">
+ <tr>
+ <th>Date:</th><td> {{ object.when }}</td>
+ </tr>
+ <tr>
+ <th>Severity:</th><td><span class="label {% severity_class object.severity %}">{{ object.get_severity_display }}</span></td>
+ </tr>
+</table>
+
+<p>
+{{ object.text }}
+
+</div>
+
+{% block action %}
+<div class="row-fluid">
+<a class="btn btn-danger" title="delete this alert" href="{% url "alert-delete" object.pk %}"><i class="icon-trash"></i> Delete</a>
+</div>
+{% endblock action %}
+
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/alert_list.html b/rpkid/rpki/gui/app/templates/app/alert_list.html
new file mode 100644
index 00000000..83c8232e
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/alert_list.html
@@ -0,0 +1,26 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Alerts</h1>
+</div>
+
+<table class="table table-striped">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>Date</th>
+ <th>Subject</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for obj in object_list %}
+ <tr {% if not obj.seen %}style="font-weight: bold" {% endif %}class="{% if obj.severity == 1 %}warning{% endif %} {% if obj.severity == 2 %}error{% endif %}">
+ <td>{# <input type="checkbox"> #}</td>
+ <td>{{ obj.when }}</td>
+ <td><a href="{{ obj.get_absolute_url }}" title="view text of alert">{{ obj.subject }}</a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/app_base.html b/rpkid/rpki/gui/app/templates/app/app_base.html
index c50490a4..4fb5f731 100644
--- a/rpkid/rpki/gui/app/templates/app/app_base.html
+++ b/rpkid/rpki/gui/app/templates/app/app_base.html
@@ -1,6 +1,7 @@
{% extends "base.html" %}
{# this can be removed when django 1.4 is EOL, because it is the default behavior in 1.5 #}
{% load url from future %}
+{% load app_extras %}
{# This template defines the common structure for the rpki.gui.app application. #}
@@ -11,9 +12,12 @@
{# common navigation #}
<ul class='nav nav-list'>
+ {% if request.session.handle %}
<li><a href="{% url "rpki.gui.app.views.dashboard" %}">dashboard</a></li>
<li><a href="{% url "rpki.gui.app.views.route_view" %}">routes</a></li>
+ <li><a href="{% url "alert-list" %}">alerts {% alert_count request.session.handle %}</a></li>
<li class="divider"></li>
+ {% endif %}
<li><a href="{% url "rpki.gui.app.views.conf_list" %}" title="select a different resource handle to manage">select identity</a></li>
{% if request.user.is_superuser %}
<li class="divider"></li>
diff --git a/rpkid/rpki/gui/app/templatetags/app_extras.py b/rpkid/rpki/gui/app/templatetags/app_extras.py
index e7b90b38..93d3a9ad 100644
--- a/rpkid/rpki/gui/app/templatetags/app_extras.py
+++ b/rpkid/rpki/gui/app/templatetags/app_extras.py
@@ -23,3 +23,30 @@ css = {
@register.simple_tag
def validity_label(validity):
return '<span class="label %s">%s</span>' % (css.get(validity, ''), validity)
+
+
+@register.simple_tag
+def severity_class(severity):
+ css = {
+ 0: 'label-info',
+ 1: 'label-warning',
+ 2: 'label-important',
+ }
+ return css.get(severity)
+
+
+@register.simple_tag
+def alert_count(conf):
+ qs = conf.alerts.filter(seen=False)
+ unread = len(qs)
+ if unread:
+ severity = max([x.severity for x in qs])
+ css = {
+ 0: 'badge-info',
+ 1: 'badge-warning',
+ 2: 'badge-important'
+ }
+ css_class = css.get(severity)
+ else:
+ css_class = 'badge-default'
+ return u'<span class="badge %s">%d</span>' % (css_class, unread)
diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py
index 5f7c214c..8e61337c 100644
--- a/rpkid/rpki/gui/app/urls.py
+++ b/rpkid/rpki/gui/app/urls.py
@@ -21,6 +21,11 @@ from rpki.gui.app import views
urlpatterns = patterns(
'',
(r'^$', views.dashboard),
+ url(r'^alert/$', views.AlertListView.as_view(), name='alert-list'),
+ url(r'^alert/(?P<pk>\d+)/$', views.AlertDetailView.as_view(),
+ name='alert-detail'),
+ url(r'^alert/(?P<pk>\d+)/delete$', views.AlertDeleteView.as_view(),
+ name='alert-delete'),
(r'^conf/export$', views.conf_export),
(r'^conf/list$', views.conf_list),
(r'^conf/select$', views.conf_select),
diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py
index 759da48e..088df8a3 100644
--- a/rpkid/rpki/gui/app/views.py
+++ b/rpkid/rpki/gui/app/views.py
@@ -32,9 +32,9 @@ from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, render, redirect
from django.utils.http import urlquote
from django import http
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib.auth.models import User
-from django.views.generic import DetailView
+from django.views.generic import DetailView, ListView, DeleteView
from django.core.paginator import Paginator
from django.forms.formsets import formset_factory, BaseFormSet
import django.db.models
@@ -1248,3 +1248,30 @@ def user_edit(request, pk):
'form_title': 'Edit User: ' + user.username,
'cancel_url': reverse(user_list)
})
+
+
+class AlertListView(ListView):
+ def get_queryset(self, **kwargs):
+ conf = self.request.session['handle']
+ return conf.alerts.all()
+
+
+class AlertDetailView(DetailView):
+ def get_queryset(self, **kwargs):
+ conf = self.request.session['handle']
+ return conf.alerts.all()
+
+ def get_object(self, **kwargs):
+ obj = super(AlertDetailView, self).get_object(**kwargs)
+ # mark alert as read by the user
+ obj.seen = True
+ obj.save()
+ return obj
+
+
+class AlertDeleteView(DeleteView):
+ success_url = reverse_lazy('alert-list')
+
+ def get_queryset(self, **kwargs):
+ conf = self.request.session['handle']
+ return conf.alerts.all()
diff --git a/rpkid/rpki/gui/cacheview/util.py b/rpkid/rpki/gui/cacheview/util.py
index 8ba7d89f..fdb3e370 100644
--- a/rpkid/rpki/gui/cacheview/util.py
+++ b/rpkid/rpki/gui/cacheview/util.py
@@ -29,12 +29,10 @@ from cStringIO import StringIO
from django.db import transaction
import django.db.models
-from django.core.mail import send_mail
import rpki
import rpki.gui.app.timestamp
-from rpki.gui.app.models import Conf
-from rpki.gui.app.glue import get_email_list
+from rpki.gui.app.models import Conf, Alert
from rpki.gui.cacheview import models
from rpki.rcynic import rcynic_xml_iterator, label_iterator
from rpki.sundial import datetime
@@ -358,7 +356,6 @@ def notify_invalid():
for handle, v in notify.iteritems():
conf = Conf.objects.get(handle)
- emails = get_email_list(conf)
msg = StringIO()
msg.write('This is an alert about problems with objects published by '
@@ -394,7 +391,7 @@ record, or is the default email address for this resource holder account on
from_email = 'root@' + getfqdn()
subj = 'invalid RPKI object alert for resource handle %s' % conf.handle
- send_mail(subj, msg.getvalue(), from_email, emails)
+ conf.send_alert(subj, msg.getvalue(), from_email, severity=Alert.ERROR)
def import_rcynic_xml(root=default_root, logfile=default_logfile):